[Bug][Prod][Reader] PDFs never open in the reader — pdf.js fetches /api/book/{id}/file tokenless (401); EPUBs unaffected #626

Closed
opened 2026-07-17 01:48:34 +00:00 by spikerj · 3 comments
Owner

Symptom

Opening a PDF in the reader (/reader/:bookId) fails — the viewer shows the load/"file unavailable" state — while EPUBs open fine. Reported as "PDF won't load" since the start of the reader epic.

Root cause

ngx-extended-pdf-viewer (pdf.js) fetches the [src] URL — ${baseUrl}/api/book/{id}/file — with its own internal XHR, which does not pass through Angular's HttpClient and therefore never hits JwtInterceptor. No Authorization: Bearer header is attached, so the endpoint (now [Authorize]-gated by the #618 part-1 read-authz work) returns 401 and the PDF never renders.

EPUBs are unaffected because they load their pages via HttpClient (GET /epub-page), which the interceptor tokenizes normally.

Verified against prod: GET https://api.spikersoft.com/api/book/6a58f2c2961260a7b51f8705/file with no tokenHTTP 401, www-authenticate: Bearer. That is exactly the tokenless request pdf.js issues.

Note: this is distinct from the earlier #612 ClamAV-quarantine investigation, which addressed a different (upload-side) failure. The reader-side "won't load" is this auth gap.

Fix

book-reader.component.ts: fetch the file bytes through HttpClient (responseType: 'blob') so JwtInterceptor attaches + refreshes the token, then hand the viewer the resulting Blob via [src] (PdfSrcType accepts Blob). Same tokenless-fetch bug fixed in downloadDocument() (was a bare fetch()), which now reuses the already-authorized Blob. Trade-off: progressive/range streaming is forfeited for a full authenticated download before first paint — acceptable for the reader, and it matches the WS-1 offline plan (PDFs feed the viewer as Blobs).

Fix PR: spikersoft-angular fix/pdf-viewer-auth.

## Symptom Opening a **PDF** in the reader (`/reader/:bookId`) fails — the viewer shows the load/"file unavailable" state — while **EPUBs open fine**. Reported as "PDF won't load" since the start of the reader epic. ## Root cause `ngx-extended-pdf-viewer` (pdf.js) fetches the `[src]` URL — `${baseUrl}/api/book/{id}/file` — with its **own internal XHR**, which does **not** pass through Angular's `HttpClient` and therefore never hits `JwtInterceptor`. No `Authorization: Bearer` header is attached, so the endpoint (now `[Authorize]`-gated by the #618 part-1 read-authz work) returns **401** and the PDF never renders. EPUBs are unaffected because they load their pages via `HttpClient` (`GET /epub-page`), which the interceptor tokenizes normally. Verified against prod: `GET https://api.spikersoft.com/api/book/6a58f2c2961260a7b51f8705/file` with **no token** → `HTTP 401`, `www-authenticate: Bearer`. That is exactly the tokenless request pdf.js issues. > Note: this is distinct from the earlier #612 ClamAV-quarantine investigation, which addressed a different (upload-side) failure. The reader-side "won't load" is this auth gap. ## Fix `book-reader.component.ts`: fetch the file bytes through `HttpClient` (`responseType: 'blob'`) so `JwtInterceptor` attaches + refreshes the token, then hand the viewer the resulting **Blob** via `[src]` (`PdfSrcType` accepts `Blob`). Same tokenless-fetch bug fixed in `downloadDocument()` (was a bare `fetch()`), which now reuses the already-authorized Blob. Trade-off: progressive/range streaming is forfeited for a full authenticated download before first paint — acceptable for the reader, and it matches the WS-1 offline plan (PDFs feed the viewer as Blobs). Fix PR: spikersoft-angular `fix/pdf-viewer-auth`.
Author
Owner

Timeline correction (verified via git blame): the [Authorize] on GET /api/book/{id}/file predates #618 — it dates to commit 3a2314d (2026-04-09, "breaking files out"), when file serving was split into its own endpoint. So /file has required a Bearer token since April; #618 part-1 (2026-07-16) only tightened which books an authenticated user may read (per-book authz), it did not introduce the token requirement.

This makes the story a clean single bug: pdf.js's tokenless XHR has hit a token-required endpoint since April, which is exactly why PDFs "never loaded from the start of the epic." The root-cause section above should read "[Authorize]-gated since 2026-04" rather than "now gated by #618 part-1."

**Timeline correction (verified via git blame):** the `[Authorize]` on `GET /api/book/{id}/file` predates #618 — it dates to commit `3a2314d` (2026-04-09, "breaking files out"), when file serving was split into its own endpoint. So `/file` has required a Bearer token since April; #618 part-1 (2026-07-16) only tightened *which* books an authenticated user may read (per-book authz), it did **not** introduce the token requirement. This makes the story a clean single bug: pdf.js's tokenless XHR has hit a token-required endpoint since April, which is exactly why PDFs "never loaded from the start of the epic." The root-cause section above should read "`[Authorize]`-gated since 2026-04" rather than "now gated by #618 part-1."
Author
Owner

Code fix merged: spikersoft-angular PR #216master (051258e). Verified: unit tests (41 green), nx build clean, and the prod tokenless-401 repro.

Holding this open for one last check before closing — the discriminating "200 vs 404 with a valid token" on the deployed /file. If it 404s, the ebooks bucket isn't backfilled for that book (the #618 part-2 deploy gate) and PDFs still won't paint even with the auth fix. That check needs an OpenBao login (.env.e2e staff creds are stale). Will close once it returns 200 + bytes and a PDF renders in the browser.

Code fix merged: spikersoft-angular PR #216 → `master` (`051258e`). Verified: unit tests (41 green), `nx build` clean, and the prod tokenless-401 repro. **Holding this open for one last check** before closing — the discriminating "200 vs 404 **with** a valid token" on the deployed `/file`. If it 404s, the `ebooks` bucket isn't backfilled for that book (the #618 part-2 deploy gate) and PDFs still won't paint even with the auth fix. That check needs an OpenBao login (`.env.e2e` staff creds are stale). Will close once it returns `200 + bytes` and a PDF renders in the browser.
Author
Owner

Auth fix confirmed working end-to-end: in the browser the PDF now begins to load (pdf.js fetched the document bytes and reached worker startup), which means GET /file returned 200 + bytes — the tokenless-401 is gone and the book is served (backfill is fine for this one). That closes the "200-vs-404" question I was holding this open for.

The PDF still doesn't finish rendering, but for a separate, newly-unmasked reason — nginx serves pdf.js's .mjs worker as application/octet-stream (strict MIME blocks the module). Tracked + fixed in #627 (angular PR #217).

Closing #626 — the auth defect is resolved and verified.

Auth fix **confirmed working end-to-end**: in the browser the PDF now begins to load (pdf.js fetched the document bytes and reached worker startup), which means `GET /file` returned `200 + bytes` — the tokenless-401 is gone and the book is served (backfill is fine for this one). That closes the "200-vs-404" question I was holding this open for. The PDF still doesn't finish rendering, but for a **separate, newly-unmasked** reason — nginx serves pdf.js's `.mjs` worker as `application/octet-stream` (strict MIME blocks the module). Tracked + fixed in **#627** (angular PR #217). Closing #626 — the auth defect is resolved and verified.
Sign in to join this conversation.