fix(downloads): don't 500 the jobs endpoint on an orphaned BullMQ job (#1191)

A job id can outlive its payload hash. BullMQ still returns an entry for
it with empty `data`, so `normalize(job.data.filepath)` threw and took the
whole response with it.

`fetchJobsWithStates()` includes failed jobs, and failed jobs are retained
deliberately, so one orphan made GET /api/downloads/jobs throw on every
call, permanently. Content Explorer polls that endpoint every ~3s, so the
symptom was a 500 loop and a page that never loaded. It survived restarts
and only cleared by editing Redis by hand.

Drop entries with no usable payload at the source, and guard both
normalize() calls (the pmtiles-extract map had the same pattern). With no
payload there is nothing to render anyway.

Seen on NOMAD3 running rc.3: 93 occurrences in 15 minutes.

Closes #1190

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
chriscrosstalk 2026-08-02 12:58:53 -07:00 committed by GitHub
parent 379f3def86
commit de6f453fa7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 8 additions and 3 deletions

View File

@ -47,7 +47,12 @@ export class DownloadService {
...active.map((j) => ({ job: j, state: 'active' as const })),
...delayed.map((j) => ({ job: j, state: 'delayed' as const })),
...failed.map((j) => ({ job: j, state: 'failed' as const })),
]
// A job id can outlive its payload hash — BullMQ still returns an entry for
// it, with `data` empty. One of those in the failed set used to throw on
// every poll of this endpoint (normalize(undefined)), and because failed
// jobs are never evicted the endpoint stayed broken until Redis was cleared
// by hand. Drop them: with no payload there is nothing to show anyway.
].filter(({ job }) => job?.id != null && job.data != null)
}
async listDownloadJobs(filetype?: string): Promise<DownloadJobWithProgress[]> {
@ -65,7 +70,7 @@ export class DownloadService {
jobId: job.id!.toString(),
url: job.data.url,
progress: parsed.percent,
filepath: normalize(job.data.filepath),
filepath: job.data.filepath ? normalize(job.data.filepath) : '',
filetype: job.data.filetype,
title: job.data.title || undefined,
downloadedBytes: parsed.downloadedBytes,
@ -82,7 +87,7 @@ export class DownloadService {
jobId: job.id!.toString(),
url: job.data.sourceUrl,
progress: parsed.percent,
filepath: normalize(job.data.outputFilepath),
filepath: job.data.outputFilepath ? normalize(job.data.outputFilepath) : '',
filetype: job.data.filetype || 'map',
title: job.data.title || undefined,
downloadedBytes: parsed.downloadedBytes,