Fix Invalid PDF structure after reload for uploaded documents.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Stop inventing /fixtures/pdfs/<title> URLs when blob bytes are missing;
rely on IndexedDB-hydrated byte store. Seeded tests set an explicit fixture
uri. Session metadata already survived; PDF bytes now survive too.
This commit is contained in:
tegwick 2026-07-30 20:05:26 +02:00
parent 9dbe806487
commit a73d45bfac
3 changed files with 26 additions and 8 deletions

View file

@ -38,11 +38,16 @@ describe("resolvePdfViewerUrl", () => {
); );
}); });
it("falls back to fixture path when only a stale blob uri is stored", () => { it("returns null for stale blob uri without live bytes (no fixture guess)", () => {
const store = createPdfByteStore(); const store = createPdfByteStore();
expect( expect(
resolvePdfViewerUrl(doc({ uri: "blob:revoked", title: "a.pdf" }), store), resolvePdfViewerUrl(doc({ uri: "blob:revoked", title: "a.pdf" }), store),
).toBe("/fixtures/pdfs/a.pdf"); ).toBeNull();
});
it("returns null when uri was stripped on persist and bytes are gone", () => {
const store = createPdfByteStore();
expect(resolvePdfViewerUrl(doc({ uri: undefined, title: "upload.pdf" }), store)).toBeNull();
}); });
}); });
@ -51,4 +56,4 @@ describe("isEphemeralBlobUri", () => {
expect(isEphemeralBlobUri("blob:http://localhost/x")).toBe(true); expect(isEphemeralBlobUri("blob:http://localhost/x")).toBe(true);
expect(isEphemeralBlobUri("/fixtures/pdfs/x.pdf")).toBe(false); expect(isEphemeralBlobUri("/fixtures/pdfs/x.pdf")).toBe(false);
}); });
}); });

View file

@ -4,6 +4,10 @@
* Uploaded PDFs are served via ephemeral `blob:` URLs owned by * Uploaded PDFs are served via ephemeral `blob:` URLs owned by
* `PdfByteStore`. Those URLs must not be persisted (see persistence * `PdfByteStore`. Those URLs must not be persisted (see persistence
* sanitization) and must be re-resolved from live bytes on every render. * sanitization) and must be re-resolved from live bytes on every render.
*
* After a reload, bytes come back via IndexedDB hydrate into the byte store.
* Never invent a `/fixtures/pdfs/<title>` URL for arbitrary uploads that
* path 404s (or returns HTML) and PDF.js reports "Invalid PDF structure".
*/ */
import type { Document } from "@shared/document"; import type { Document } from "@shared/document";
@ -20,7 +24,8 @@ export function isEphemeralBlobUri(uri: string | undefined): boolean {
/** /**
* Prefer the byte store's live blob URL for uploaded documents; fall back * Prefer the byte store's live blob URL for uploaded documents; fall back
* to a stable HTTP fixture path or a non-blob `document.uri`. * to a stable non-blob `document.uri` (e.g. fixture HTTP path). Returns
* `null` when bytes are not available ViewerShell shows re-upload copy.
*/ */
export function resolvePdfViewerUrl( export function resolvePdfViewerUrl(
document: Document, document: Document,
@ -33,8 +38,8 @@ export function resolvePdfViewerUrl(
return document.uri; return document.uri;
} }
const titleOrId = document.title ?? document.id; // Stale blob URI and no in-memory/IDB bytes: do not guess a fixture path.
return `/fixtures/pdfs/${encodeURIComponent(titleOrId)}`; return null;
} }
/** True when bytes exist in the store but the document record lacks a URI. */ /** True when bytes exist in the store but the document record lacks a URI. */
@ -43,4 +48,4 @@ export function documentHasUploadedBytes(
byteStore: PdfByteStore, byteStore: PdfByteStore,
): boolean { ): boolean {
return byteStore.has(documentId); return byteStore.has(documentId);
} }

View file

@ -49,10 +49,18 @@ export function seedSessionWithDoc(opts: SeedOptions): SeedResult {
lastOpenedAt: now, lastOpenedAt: now,
}; };
// Stable HTTP fixture URI so the viewer mounts without IndexedDB bytes.
// (resolvePdfViewerUrl no longer invents /fixtures paths from title alone —
// that produced "Invalid PDF structure" for arbitrary uploads after reload.)
const document: Document = { const document: Document = {
id: documentId, id: documentId,
mediaType: "application/pdf", mediaType: "application/pdf",
...(opts.documentTitle ? { title: opts.documentTitle } : {}), ...(opts.documentTitle
? {
title: opts.documentTitle,
uri: `/fixtures/pdfs/${encodeURIComponent(opts.documentTitle)}`,
}
: {}),
fingerprint: "seed-fingerprint", fingerprint: "seed-fingerprint",
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,