Four small follow-ups from the second-pass audit:
- N1: `manga_upload_rolls_back_when_cover_storage_fails` covers the
manga-side of the transactional rollback path. The chapter case had
a `FailingStorage` regression test already; this completes the
symmetric pair. With fail-on-put-index=0, the cover put fails on
the first call, the transaction aborts, and `SELECT count(*) FROM
mangas WHERE title = 'Berserk'` is 0.
- N2: The SvelteKit proxy now catches network-layer failures from the
upstream `fetch` (DNS / connection refused / TLS handshake) and
returns a 502 with the standard error envelope
(`code: 'upstream_unavailable'`) instead of letting SvelteKit's
generic 500 HTML page through. `client.ts` can `.json()` the result
cleanly so callers see a real ApiError with a meaningful code. The
underlying cause is logged via `console.error` for the operator.
Test in hooks.server.test.ts asserts the 502, the JSON envelope, and
that `resolve` is not called (the proxy short-circuits).
- N3: `GET /api/v1/files/*key` now sets
`X-Content-Type-Options: nosniff`. The upload-time magic-byte sniff
is authoritative for what we declare as Content-Type; `nosniff`
makes the contract explicit so older user-agents can't try to
re-detect HTML/JS in a polyglot file that survived the sniff. Test
in api_uploads.rs asserts the header.
- N4: The /bookmarks page used `{#if b.page}` to gate the "— page N"
display, which falsy-elided a legitimate `page == 0`. Backend now
rejects `page < 1` for new bookmarks (already shipped in 0.9.4),
but any pre-0.9.4 row with page=0 still rendered without its
number. Strengthened to `{#if b.page != null && b.page > 0}`.
Lockstep version bump to 0.10.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The compose deploy was unreachable because frontend code reads its
API base from `import.meta.env.VITE_API_BASE` at build time, but the
shipped image baked in the fallback `/api` and never picked up the
`PUBLIC_API_BASE` env var. The browser then hit
http://localhost:3000/api/...which the Node adapter doesn't serve, so
every request 404'd.
Fix the topology at the right layer: hooks.server.ts proxies /api/*
requests through to the backend container over docker's internal
network. The browser only ever talks to :3000, cookies stay
same-origin, and CORS can stay empty.
- frontend/src/hooks.server.ts: new proxy. Reads BACKEND_URL (defaults
to http://localhost:8080 for ad-hoc node builds). Strips `host` and
`content-length` so the backend sees the real client request and
recomputes the length. Sets `duplex: 'half'` for streamed POST
bodies. GET/HEAD have no body. Non-/api paths fall through to
SvelteKit normally.
- docker-compose.yml: drop the host port mapping on the backend
(browser doesn't reach it directly anymore — use `ports:` instead of
`expose:` if you want curl access). Set BACKEND_URL=http://backend:8080
on the frontend service. Drop PUBLIC_API_BASE which was unused.
- .env.example: replace PUBLIC_API_BASE with BACKEND_URL, with a note
on what it does.
- README: explain the new topology in Quick start, update the bot
curl examples to hit :3000 (since that's the only published port in
the default deploy), and call out that the TLS terminator only needs
one upstream now.
Lockstep version bump to 0.9.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>