mastra-ai/mastra · error
Path is a directory
Error message
Path is a directory
What it means
readWorkspaceFile reads a text file from a workspace; after resolving and confining the path it lstats the target and refuses to read directories. This error is thrown when the requested path exists inside the workspace but is a directory rather than a regular file, because reading it as file content is meaningless. The caller should use a directory-listing endpoint instead.
Source
Thrown at mastracode/factory/src/routes/fs.ts:371
return {
workspacePath: workspace,
root: safeRoot,
rootPath: confinedRootPath,
entries: await listRenderedEntries(confinedRootPath),
};
}
export async function readWorkspaceFile(root: string, workspacePath: string, path: string): Promise<WorkspaceFile> {
const safePath = assertRelativePath(path, 'path');
const relativeRoot = safePath.split('/')[0] ?? '';
assertApprovedRenderedRoot(relativeRoot);
const {
workspace,
path: confinedPath,
relativePath,
} = await confinedWorkspaceRelativePath(root, workspacePath, path);
const info = await lstat(confinedPath);
if (info.isDirectory()) throw new Error('Path is a directory');
if (!info.isFile()) throw new Error('Unsupported file type');
const bytesToRead = Math.min(info.size, MAX_TEXT_FILE_BYTES);
const contentBuffer = Buffer.alloc(bytesToRead);
const handle = await open(confinedPath, 'r');
try {
await handle.read(contentBuffer, 0, bytesToRead, 0);
} finally {
await handle.close();
}
try {
const content = TEXT_DECODER.decode(contentBuffer);
return {
workspacePath: workspace,
path: relativePath,
name: relativePath.split('/').pop() ?? relativePath,
size: info.size,View on GitHub (pinned to 75dd419e61)
Solutions
- Check that the path targets a file, not a directory (ls or stat the path inside the workspace).
- If you want the directory contents, use the directory-listing / rendered-listing endpoint for that workspace instead.
- Fix client code to branch on the entry type from listing results before calling the read endpoint.
- If you expected a file, verify the file wasn't replaced by a directory of the same name during a build or checkout.
Example fix
// before: reading a directory GET /fs/file?path=.artifacts // → 'Path is a directory' // after: read a file inside it GET /fs/file?path=.artifacts/report.txt
Defensive patterns
Strategy: validation
Validate before calling
import { stat } from 'node:fs/promises';
async function isFileInWorkspace(workspace: string, rel: string): Promise<boolean> {
try {
const info = await stat(resolve(workspace, rel));
return info.isFile();
} catch {
return false;
}
}
// only call readWorkspaceFile when this returns true; else use the listing endpoint Type guard
function isFileEntry(entry: { type: string }): entry is { type: 'file' } {
return entry.type === 'file';
} Try / catch
try {
const file = await readWorkspaceFile(root, ws, path);
} catch (e) {
if (e instanceof Error && e.message === 'Path is a directory') {
return listWorkspaceRenderedPath(root, ws, path); // fall back to listing
}
throw e;
} Prevention
- Branch on entry.type from listing results before opening a file reader.
- Never request the rendered root itself ('.artifacts', 'src') as a file path.
- Verify the target with stat before read in scripts that build paths dynamically.
- Watch for directories replacing files of the same name after builds/checkouts.
When it happens
Trigger: Calling readWorkspaceFile (or the fs read route) with a path query that names an existing directory inside the workspace, e.g. path=src or path=.artifacts.
Common situations: Passing a folder path copied from a listing response instead of an entry file path; assuming the artifacts root itself is readable as a file; UI bugs that don't distinguish entry.type === 'directory' before opening a reader.
Related errors
- Missing required query param: ${label}
- ${label} must be relative
- ${label} escapes workspace
- Path escapes workspace
- Unsupported file type
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/1cf24790f394a95f.
Report an issue: GitHub.