Handle tenant API auth failures gracefully and drop Server Actions - #1464
Open
camilb wants to merge 2 commits into
Open
Handle tenant API auth failures gracefully and drop Server Actions#1464camilb wants to merge 2 commits into
camilb wants to merge 2 commits into
Conversation
Production pods log a flood of "API Error (401): Unauthorized" entries when a tenant's access token is revoked or rotated, with no way to tell which newsroom is affected. Separately, the -reverse pods log "Failed to find Server Action" warnings after deploys due to build-scoped action IDs. - Log API auth failures (401/403) once per newsroom per minute with the request host and newsroom UUID, so the broken tenant is identifiable from the logs. - Short-circuit with 503 + retry-after in the middleware when the tenant token is rejected, instead of letting the render crash with a 500. - Strip API response headers (multi-KB CSP dump) from serialized error logs via a next-logger pino serializer (~6.5KB -> ~700B per line). - Replace the getStoryPdfUrl Server Action with a route handler (/api/stories/[uuid]/pdf) and run htmlToText in the browser, leaving zero Server Actions in the build - eliminating deploy-skew "Failed to find Server Action" errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
camilb
requested review from
boki-prezly,
digitalbase,
edvinas2025,
fgyimah and
zawropati
July 17, 2026 10:12
- PDF route: map upstream failures to proper statuses (404 passthrough, 502 otherwise) instead of returning 200 with a null URL, log auth failures with tenant identification, and validate the upstream Location header as an absolute https URL before returning it. - getStoryPdfUrl: validate the response shape structurally instead of a blind type assertion; document the helper as browser-only. - next-logger serializer: only strip headers from HTTP-response-shaped errors (status + headers present). - Auth log throttle map: bound at 1,000 entries with expired-entry pruning and oldest-entry eviction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| * newsroom per minute. | ||
| */ | ||
| export function logApiAuthFailure(status: number, requestHeaders: Headers) { | ||
| const host = requestHeaders.get('x-forwarded-host') ?? requestHeaders.get('host') ?? 'unknown'; |
There was a problem hiding this comment.
Consider using request.headers.get('host') as the primary fallback instead of x-forwarded-host, since the latter can be spoofed by clients. The host header is more reliable in most deployment scenarios.
| // If every tracked entry is still within the throttle window, drop the | ||
| // oldest one (Map iterates in insertion order) to keep the size bounded. | ||
| if (lastLoggedAt.size >= MAX_TRACKED_TENANTS) { | ||
| const oldest = lastLoggedAt.keys().next().value; |
There was a problem hiding this comment.
The oldest variable could theoretically be undefined if the Map is empty, but this is protected by the size check above. Consider adding a comment explaining this invariant for clarity.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Production pods (
theme-nextjs-bea,-preview) log a flood ofAPI Error (401): Unauthorizedentries whenever a tenant's access token is revoked or rotated:Separately, the
-reversepods logFailed to find Server Actionwarnings — stale browser tabs POSTing build-scoped action IDs after a redeploy.Changes
Tenant auth failures (401/403)
src/adapters/server/api-error.ts— wraps the content-delivery client so any 401/403 from the Prezly API logs a warning with the request host and newsroom UUID, throttled to once per newsroom per minute, then rethrows. Covers allapp()calls, API routes, and the middleware.middleware.ts— when locale resolution hits an auth error, respond503+retry-after: 300before rendering starts. 503 (not 404) so search engines treat the outage as temporary. When the gateway suppliesX-Newsroom-Locales, the middleware skips the API and render-path failures still surface as 500 with the branded error page — an RSC render cannot emit a custom status; the log spam was the actionable problem.Log noise
next-logger.config.js— pinoerrserializer that strips the API response headers from serialized errors while keepingstatus,payload,stack, anddigest. Picked up automatically by the existingNODE_OPTIONS='-r next-logger'in the Dockerfile. Error lines: ~6,500 → ~690 bytes.Server Actions eliminated
app/api/stories/[uuid]/pdf/route.ts— replaces thegetStoryPdfUrlServer Action with a stable-URL route handler (same logic, plus UUID validation → 400 on malformed input). The client util now fetches it.htmlToText— dropped'use server';html-to-textis pure JS and runs in the browser inside the already-lazy "Copy text" chunk (First Load JS unchanged, story page 265 kB).Verification
tsc,biome check, and fullnext buildall clean.next-loggerpreloaded and a forgedX-Prezly-Envcarrying an invalid token:retry-after: 300;Prezly API rejected the access token (401) for newsroom=… host=broken-tenant.example.comlogged once;GET /api/stories/<uuid>/pdf→{"url":null}gracefully; malformed uuid → 400;Follow-up (outside this repo)
The root cause is upstream: the routing layer keeps sending traffic (with a stale token) for a tenant the API rejects. Once deployed, the new warn logs identify exactly which newsroom/host it is; the durable fix is to stop routing revoked tenants at the gateway and make token rotation update the env store atomically.