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); }