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.
## 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`.
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."
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.
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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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'sHttpClientand therefore never hitsJwtInterceptor. NoAuthorization: Bearerheader 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/filewith no token →HTTP 401,www-authenticate: Bearer. That is exactly the tokenless request pdf.js issues.Fix
book-reader.component.ts: fetch the file bytes throughHttpClient(responseType: 'blob') soJwtInterceptorattaches + refreshes the token, then hand the viewer the resulting Blob via[src](PdfSrcTypeacceptsBlob). Same tokenless-fetch bug fixed indownloadDocument()(was a barefetch()), 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.Timeline correction (verified via git blame): the
[Authorize]onGET /api/book/{id}/filepredates #618 — it dates to commit3a2314d(2026-04-09, "breaking files out"), when file serving was split into its own endpoint. So/filehas 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."Code fix merged: spikersoft-angular PR #216 →
master(051258e). Verified: unit tests (41 green),nx buildclean, 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, theebooksbucket 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.e2estaff creds are stale). Will close once it returns200 + bytesand a PDF renders in the browser.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 /filereturned200 + 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
.mjsworker asapplication/octet-stream(strict MIME blocks the module). Tracked + fixed in #627 (angular PR #217).Closing #626 — the auth defect is resolved and verified.