diff --git a/backend/src/__tests__/stack-compose-mtime.test.ts b/backend/src/__tests__/stack-compose-mtime.test.ts index 0623d0a8..7880060e 100644 --- a/backend/src/__tests__/stack-compose-mtime.test.ts +++ b/backend/src/__tests__/stack-compose-mtime.test.ts @@ -209,6 +209,24 @@ describe('PUT /api/stacks/:stackName/env optimistic concurrency', () => { expect(fs.readFileSync(envPath, 'utf-8')).toBe('FOO=2'); }); + it('returns a clean 404 (not a 500) when the stack has no env file yet', async () => { + // Compose exists with no env_file directive and no .env on disk, so + // resolveAllEnvFilePaths filters the synthesized default out and returns [], + // leaving the env path undefined. The save must surface a handled response + // rather than crashing on a write to an undefined path. + seedStack(STACK, 'services:\n web:\n image: nginx\n'); + + const putRes = await request(app) + .put(`/api/stacks/${STACK}/env`) + .set('Cookie', authCookie) + .send({ content: 'FOO=1' }); + + expect(putRes.status).toBe(404); + expect(putRes.body.error).toMatch(/no env file/i); + // The guard must short-circuit before any write touches disk. + expect(fs.existsSync(path.join(composeDir, STACK, '.env'))).toBe(false); + }); + it('returns 412 on env-file mtime mismatch', async () => { seedStack(STACK, 'services: {}'); const envPath = seedEnv(STACK, 'FOO=1'); diff --git a/backend/src/routes/stacks.ts b/backend/src/routes/stacks.ts index 6170473b..2e218e8e 100644 --- a/backend/src/routes/stacks.ts +++ b/backend/src/routes/stacks.ts @@ -644,6 +644,14 @@ stacksRouter.put('/:stackName/env', async (req: Request, res: Response) => { } } + // No env file resolved: the stack has no .env yet and the editor only edits + // an existing env file. GET treats this same case as an empty 200; PUT cannot, + // since there is no resolved path to write. Reply with a clean, handled response + // instead of writing to an undefined path, which would otherwise surface as an opaque 500. + if (!envPath) { + return res.status(404).json({ error: 'No env file exists for this stack' }); + } + const fsService = FileSystemService.getInstance(req.nodeId); const expectedMtimeMs = parseIfMatchMtime(req.header('if-match')); const result = await fsService.writeFileIfUnchanged(envPath, content, expectedMtimeMs);