{"record":{"id":"537b490020ca25b5","repo":"HeyPuter/puter","slug":"cannot-read-a-directory-537b49","errorCode":"cannot_read_a_directory","errorMessage":"Cannot read a directory","messagePattern":"Cannot read a directory","errorType":"http","errorClass":"HttpError","httpStatus":400,"severity":"error","filePath":"src/backend/controllers/fs/LegacyFSController.ts","lineNumber":1156,"sourceCode":"\n        // Legacy v1 /read aliased `file` onto either path or uid depending on\n        // whether the value starts with `/`. resolveV1Selector does the same\n        // dispatch when handed a raw string.\n        const selector =\n            typeof query.file === 'string' && query.file.length > 0\n                ? query.file\n                : query;\n        const entry = await resolveV1Selector(this.stores.fsEntry, selector);\n        await assertAccess(\n            this.services.acl,\n            this.services.fs,\n            actor,\n            entry.path,\n            'read',\n        );\n\n        if (entry.isDir) {\n            throw new HttpError(400, 'Cannot read a directory', {\n                legacyCode: 'cannot_read_a_directory',\n            });\n        }\n\n        const range =\n            typeof req.headers.range === 'string'\n                ? req.headers.range\n                : undefined;\n        const download = await this.services.fs.readContent(entry, {\n            range,\n        });\n\n        // Force `application/octet-stream` on this endpoint for wire parity\n        // with v1. puter-js's `parseResponse` branches on Content-Type —\n        // `application/octet-stream` returns the raw Blob while other\n        // types wrap in `{success, result: Blob}`. Clients (including the\n        // GUI) expect the raw-Blob shape. Use `/fs/read` for type-aware\n        // streaming.","sourceCodeStart":1138,"sourceCodeEnd":1174,"githubUrl":"https://github.com/HeyPuter/puter/blob/908ec23eda38526170322c3edf71ba45ecb1ca95/src/backend/controllers/fs/LegacyFSController.ts#L1138-L1174","documentation":"Thrown by LegacyFSController.read after the target entry is resolved and read access is confirmed, when entry.isDir is true. The read endpoint streams file content; directories have no byte content, so the request is rejected with legacyCode 'cannot_read_a_directory'. Use readdir to list directory contents instead.","triggerScenarios":"Calling /read (or puter.fs.read) with a path or uid that points to a directory; passing a folder uid where a file is expected.","commonSituations":"A generic 'open' handler that calls read regardless of entry type; a double-click on a folder inadvertently triggering a read; a stored uid that changed from file to folder.","solutions":["Check entry.isDir before reading; if true, call readdir instead.","In the UI, route folder selections to the listing flow and file selections to the read flow.","Resolve/stat the entry first to branch on its type."],"exampleFix":"// before\nconst blob = await puter.fs.read(entry);\n\n// after\nif (entry.is_dir) {\n  await puter.fs.readdir(entry);\n} else {\n  const blob = await puter.fs.read(entry);\n}","handlingStrategy":"type-guard","validationCode":"const entry = await stat(selector);\nif (entry.is_dir) throw new Error('Cannot read a directory; use readdir');","typeGuard":"function isFileEntry(e) { return !!e && !e.is_dir; }","tryCatchPattern":"null","preventionTips":["Branch on is_dir before calling read; route directories to readdir.","Resolve the entry first so you know its type.","In the UI, prevent read actions on folder selections."],"tags":["fs","legacy","read","validation"],"backgroundTag":null,"analyzedSha":"908ec23eda38526170322c3edf71ba45ecb1ca95","analyzedAt":"2026-08-12T20:53:15.911Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}