{"record":{"id":"ddb75fac346d1c5b","repo":"ruvnet/ruflo","slug":"file-not-found","errorCode":null,"errorMessage":"File not found","messagePattern":"File not found","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ruflo/src/ruvocal/src/lib/server/database/postgres.ts","lineNumber":668,"sourceCode":"\t\t\t\t\t\tdata,\n\t\t\t\t\t\tJSON.stringify(options?.metadata ?? {}),\n\t\t\t\t\t]\n\t\t\t\t);\n\t\t\t},\n\t\t};\n\t}\n\n\topenDownloadStream(id: ObjectId | string) {\n\t\tconst fileId = typeof id === \"string\" ? id : id.toString();\n\t\t// Return a readable-like object\n\t\treturn {\n\t\t\tasync toArray(): Promise<Buffer[]> {\n\t\t\t\tconst pool = getPool();\n\t\t\t\tconst result = await pool.query(\n\t\t\t\t\t`SELECT data FROM files WHERE _id = $1`,\n\t\t\t\t\t[fileId]\n\t\t\t\t);\n\t\t\t\tif (result.rows.length === 0) throw new Error(\"File not found\");\n\t\t\t\treturn [result.rows[0].data];\n\t\t\t},\n\t\t};\n\t}\n\n\tasync delete(id: ObjectId | string) {\n\t\tconst fileId = typeof id === \"string\" ? id : id.toString();\n\t\tconst pool = getPool();\n\t\tawait pool.query(`DELETE FROM files WHERE _id = $1`, [fileId]);\n\t}\n\n\tasync find(filter: Record<string, unknown> = {}) {\n\t\tconst w = filterToWhere(filter);\n\t\tconst pool = getPool();\n\t\tconst result = await pool.query(\n\t\t\t`SELECT _id, filename, content_type, length, metadata, created_at FROM files WHERE ${w.text}`,\n\t\t\tw.values\n\t\t);","sourceCodeStart":650,"sourceCodeEnd":686,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/ruflo/src/ruvocal/src/lib/server/database/postgres.ts#L650-L686","documentation":"In the PostgreSQL backend the GridFS bucket is emulated: openDownloadStream(id).toArray() runs SELECT data FROM files WHERE _id = $1 and throws \"File not found\" when zero rows match (postgres.ts:668). This is the Postgres equivalent of Mongo GridFS failing to open a stream for an unknown file id, used by file downloads (files/downloadFile.ts) and conversation attachment loading (conversation.ts:63).","triggerScenarios":"Awaiting collections.bucket.openDownloadStream(fileId).toArray() with an id that has no row in the files table: file was deleted by cleanup/TTL, conversation still references a file id from a previous database (or from the Mongo/rvf backend before switching to Postgres), id string was mutated (ObjectId vs toString mismatch), or the table was wiped.","commonSituations":"Migrating storage backends while keeping old conversations; resetting the Postgres DB in dev but keeping seeded conversations; a moderation/gdpr job deleting file rows but not message metadata; tests calling toArray() with random ids.","solutions":["Verify the id exists first: const found = await collections.bucket.find({ _id: id }).toArray() and 404 gracefully when empty","If ids mismatch, check whether the stored _id is the ObjectId string and you are passing an ObjectId (or vice versa) — both are accepted via toString(), so confirm the raw value","If the row was deleted legitimately, remove the dangling reference from the message/file metadata or re-upload the asset","Restore the files table from backup if the wipe was accidental"],"exampleFix":"// before\nconst [buf] = await collections.bucket.openDownloadStream(fileId).toArray(); // throws: File not found\n\n// after\nlet buf: Buffer;\ntry {\n\t[buf] = await collections.bucket.openDownloadStream(fileId).toArray();\n} catch (e) {\n\tif (e instanceof Error && e.message === \"File not found\") {\n\t\tthrow error(404, \"Attachment no longer available\");\n\t}\n\tthrow e;\n}","handlingStrategy":"try-catch","validationCode":"const existing = await collections.bucket.find({ _id: fileId }).toArray();\nif (existing.length === 0) throw error(404, \"File no longer available\");\nconst [buf] = await collections.bucket.openDownloadStream(fileId).toArray();","typeGuard":"null","tryCatchPattern":"try {\n\tconst [buf] = await collections.bucket.openDownloadStream(fileId).toArray();\n} catch (e) {\n\tif (e instanceof Error && e.message === \"File not found\") {\n\t\t// degrade gracefully: 404 / placeholder instead of a 500\n\t\tthrow error(404, \"Attachment no longer available\");\n\t}\n\tthrow e;\n}","preventionTips":["When deleting files, also strip their ids from message metadata to avoid dangling references","Keep all storage backends on one MONGODB_URL/Postgres instance per environment so uploads and reads match","Wrap download routes in the not-found catch so a missing blob never surfaces as a 500"],"tags":["postgres","gridfs","file-storage","not-found","database"],"backgroundTag":"file-not-found","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}