ruvnet/ruflo · error · Error
File not found
Error message
File not found
What it means
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).
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
Example fix
// before
const [buf] = await collections.bucket.openDownloadStream(fileId).toArray(); // throws: File not found
// after
let buf: Buffer;
try {
[buf] = await collections.bucket.openDownloadStream(fileId).toArray();
} catch (e) {
if (e instanceof Error && e.message === "File not found") {
throw error(404, "Attachment no longer available");
}
throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
const existing = await collections.bucket.find({ _id: fileId }).toArray();
if (existing.length === 0) throw error(404, "File no longer available");
const [buf] = await collections.bucket.openDownloadStream(fileId).toArray(); Type guard
null
Try / catch
try {
const [buf] = await collections.bucket.openDownloadStream(fileId).toArray();
} catch (e) {
if (e instanceof Error && e.message === "File not found") {
// degrade gracefully: 404 / placeholder instead of a 500
throw error(404, "Attachment no longer available");
}
throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.
Related errors
- Failed to initialize connection pool
- File not found
- Index not found
- pg (node-postgres) package not found. Install it with: npm…
- Savepoint ' ' does not exist
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/ddb75fac346d1c5b.
Report an issue: GitHub.
Appendix: source
Thrown at ruflo/src/ruvocal/src/lib/server/database/postgres.ts:668
data,
JSON.stringify(options?.metadata ?? {}),
]
);
},
};
}
openDownloadStream(id: ObjectId | string) {
const fileId = typeof id === "string" ? id : id.toString();
// Return a readable-like object
return {
async toArray(): Promise<Buffer[]> {
const pool = getPool();
const result = await pool.query(
`SELECT data FROM files WHERE _id = $1`,
[fileId]
);
if (result.rows.length === 0) throw new Error("File not found");
return [result.rows[0].data];
},
};
}
async delete(id: ObjectId | string) {
const fileId = typeof id === "string" ? id : id.toString();
const pool = getPool();
await pool.query(`DELETE FROM files WHERE _id = $1`, [fileId]);
}
async find(filter: Record<string, unknown> = {}) {
const w = filterToWhere(filter);
const pool = getPool();
const result = await pool.query(
`SELECT _id, filename, content_type, length, metadata, created_at FROM files WHERE ${w.text}`,
w.values
);View on GitHub (pinned to fa13ee4ad6)