From a962654a3b96e9822df2aca387d8c9e525d39e1c Mon Sep 17 00:00:00 2001 From: Anso Date: Fri, 24 Apr 2026 23:37:54 -0400 Subject: [PATCH] fix(env): return empty body for missing .env files; surface non-OK responses cleanly (#767) Previously, fetching the .env file for a stack with no env files at all returned a 404 with a JSON error body. The frontend's secondary loader (changeEnvFile) called res.text() without checking res.ok, which caused the error body to be stuffed directly into the editor as if it were file content. Two-part fix: Backend (routes/stacks.ts): - For the default GET /stacks/:name/env (no ?file= query) when the stack has no env files, respond 200 with an empty body and an X-Env-Exists: false header instead of 404. - For an explicit ?file= query that resolves to a missing file, keep the 404 (the caller asked for something specific). - Catch a TOCTOU ENOENT between access() and readFile() and return the same friendly empty-body shape, not a generic 500. Frontend (EditorLayout.tsx::changeEnvFile): - Check res.ok before reading the body. On a non-OK response, clear the editor content and surface a friendly toast instead of pasting the server's JSON error string into the file. --- backend/src/routes/stacks.ts | 37 +++++++++++++++++++++--- frontend/src/components/EditorLayout.tsx | 9 ++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/backend/src/routes/stacks.ts b/backend/src/routes/stacks.ts index 304d1b7d..edfbbf34 100644 --- a/backend/src/routes/stacks.ts +++ b/backend/src/routes/stacks.ts @@ -185,7 +185,7 @@ stacksRouter.get('/:stackName/env', async (req: Request, res: Response) => { const requestedFile = req.query.file as string | undefined; const envPaths = await resolveAllEnvFilePaths(req.nodeId, stackName); - let envPath = envPaths[0]; + let envPath: string | undefined = envPaths[0]; if (requestedFile) { if (envPaths.includes(requestedFile)) { @@ -195,6 +195,15 @@ stacksRouter.get('/:stackName/env', async (req: Request, res: Response) => { } } + // Default path with no env files yet: reply 200 with an empty body and a + // header the frontend can read. This avoids surfacing a 404 for the + // legitimate "stack has no .env yet" case, which previous flows + // sometimes echoed back to the user as a confusing error string. + if (!envPath) { + res.setHeader('X-Env-Exists', 'false'); + return res.send(''); + } + const fsService = FileSystemService.getInstance(req.nodeId); try { @@ -204,11 +213,31 @@ stacksRouter.get('/:stackName/env', async (req: Request, res: Response) => { if (code !== 'ENOENT') { console.error('[Sencho] Unexpected error checking env file existence:', (e as Error).message); } - return res.status(404).json({ error: 'Env file not found' }); + // No env file at the resolved path. For an explicit ?file= query we + // surface a 404 (the caller asked for something specific). Otherwise + // treat it as the empty-stack case above. + if (requestedFile) { + return res.status(404).json({ error: 'Env file not found' }); + } + res.setHeader('X-Env-Exists', 'false'); + return res.send(''); } - const content = await fsService.readFile(envPath, 'utf-8'); - res.send(content); + try { + const content = await fsService.readFile(envPath, 'utf-8'); + res.setHeader('X-Env-Exists', 'true'); + return res.send(content); + } catch (e: unknown) { + // TOCTOU: the file existed at access() but vanished before readFile(). + // Return the same friendly empty-body shape rather than a generic 500 + // that the frontend would otherwise echo as an opaque error. + const code = (e as NodeJS.ErrnoException)?.code; + if (code === 'ENOENT' && !requestedFile) { + res.setHeader('X-Env-Exists', 'false'); + return res.send(''); + } + throw e; + } } catch (error) { console.error('Failed to read env file:', error); res.status(500).json({ error: 'Failed to read env file' }); diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx index de14d3c9..44d25f65 100644 --- a/frontend/src/components/EditorLayout.tsx +++ b/frontend/src/components/EditorLayout.tsx @@ -1159,11 +1159,20 @@ export default function EditorLayout() { setIsFileLoading(true); try { const res = await apiFetch(`/stacks/${selectedFile}/env?file=${encodeURIComponent(file)}`); + if (!res.ok) { + // Don't stuff a JSON error body into the editor on a non-OK response. + setEnvContent(''); + setOriginalEnvContent(''); + toast.error('Could not load env file'); + return; + } const text = await res.text(); setEnvContent(text || ''); setOriginalEnvContent(text || ''); } catch (e) { console.error('Failed to switch env file', e); + setEnvContent(''); + setOriginalEnvContent(''); } finally { setIsFileLoading(false); }