Skip to content

fix(lotw): retry transient 5xx from LoTW + browser User-Agent#226

Merged
patrickrb merged 2 commits into
mainfrom
fix/lotw-503-retry
Jul 14, 2026
Merged

fix(lotw): retry transient 5xx from LoTW + browser User-Agent#226
patrickrb merged 2 commits into
mainfrom
fix/lotw-503-retry

Conversation

@patrickrb

Copy link
Copy Markdown
Owner

Problem

The unified sync cron was failing LoTW download with LoTW download failed: 503 OK. The 503 is transient on ARRL's side — the lotwreport.adi / upload front end returns 503 Service Unavailable when it's throttling or under load. A direct probe of the endpoint with our exact params returns a clean 200, so the request itself is fine; the 503 tracks the caller.

Why Wavelog works and we didn't

I compared our LoTW logic against Wavelog's application/controllers/Lotw.php. They're essentially identical — same endpoint, same query params, and Wavelog has no retry and no 503 handling either (it just reports "unexpected HTTP status 503" and stops). The difference isn't the code, it's the network origin:

  • Wavelog runs on the operator's own server behind one stable IP that ARRL is happy to serve hourly.
  • Nextlog runs on Vercel serverless, egressing from shared/rotating cloud IP ranges that ARRL throttles far more readily.

So the fix is to be more resilient than Wavelog, precisely because our egress is worse.

Changes

  • fetchLotwWithRetry() (new, in src/lib/lotw.ts): retries 5xx and network errors up to 3 attempts with exponential backoff (2s / 4s / 8s). Any 2xx is returned untouched — LoTW encodes auth and other failures inside a 200 body, so those still flow through existing detection. After retries are exhausted the final response is returned, so the existing !ok failure path and log rows are preserved.
  • Browser User-Agent replaces Nextlog/1.0.0 on every LoTW call — some WAF front ends 503 non-browser clients.
  • Applied to all LoTW HTTP calls:
    • cron upload + download (src/lib/lotw-sync.ts)
    • manual per-contact upload + download routes
    • validateLoTWCredentials + checkLotwCertCrl

Verification

  • npm run typecheck — clean
  • npm run lint — clean
  • npm run build — succeeds

Follow-up if 503s persist

If LoTW still 503s after this ships, it points to ARRL hard-throttling Vercel's IP range — no client tweak fixes that. The durable answer is a stable egress IP (a fixed-IP proxy, or Vercel dedicated egress), recreating Wavelog's "one steady IP" advantage.

🤖 Generated with Claude Code

LoTW's front end intermittently returns 503 Service Unavailable when it's
throttling or under load, and Vercel's serverless egress IPs get throttled
far more readily than a self-hosted box on a stable IP. That's why Wavelog
— whose LoTW request logic is otherwise equivalent and also has no retry —
doesn't hit this, but our cron does. A single 503 was failing the entire
upload/download leg with no retry.

Add fetchLotwWithRetry() in src/lib/lotw.ts: retries 5xx and network errors
up to 3 times with exponential backoff (2s/4s/8s) and returns any 2xx
untouched, since LoTW encodes auth and other failures inside a 200 body
rather than the status line. Swap the Nextlog/1.0.0 User-Agent for a
browser UA (some WAF front ends 503 non-browser clients).

Applied to every LoTW HTTP call: the cron upload/download (lotw-sync.ts),
the manual per-contact upload/download routes, credential validation, and
the CRL status check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nodelog Ready Ready Preview, Comment Jul 14, 2026 3:32pm

Request Review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Improves LoTW (Logbook of The World) reliability in Nextlog by adding a shared retry wrapper for transient failures and standardizing LoTW requests to use a browser-like User-Agent, addressing intermittent 5xx responses (notably 503) seen from cloud/serverless egress.

Changes:

  • Added fetchLotwWithRetry() and LOTW_USER_AGENT in src/lib/lotw.ts to retry transient 5xx and network errors with backoff.
  • Updated scheduled LoTW sync upload/download to use the retry wrapper and browser User-Agent.
  • Updated single-contact upload/download API routes to use the retry wrapper and browser User-Agent.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/lib/lotw.ts Introduces retry/backoff helper and browser UA constant; applies retry to credential validation + CRL checks.
src/lib/lotw-sync.ts Uses retry helper + UA for cron/scheduled LoTW upload and download operations.
src/app/api/lotw/upload-contact/route.ts Uses retry helper + UA for manual single-contact LoTW upload.
src/app/api/lotw/download-contact/route.ts Uses retry helper + UA for manual single-contact LoTW download/confirmation.

Comment thread src/lib/lotw.ts Outdated
Comment thread src/lib/lotw.ts
- Derive backoff from the attempt number (base·2^(N-1)) instead of a
  fixed 3-element array whose last slot was never reached with 3
  attempts — removes the dead config / off-by-one ambiguity.
- Cancel the discarded response body before retrying a 5xx so undici can
  reuse the connection rather than leaking sockets across retries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@patrickrb
patrickrb merged commit 205cfed into main Jul 14, 2026
7 checks passed
@patrickrb
patrickrb deleted the fix/lotw-503-retry branch July 14, 2026 15:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants