fix(downloads): don't retry a rejected entitlement for four hours (#1205)
The 401/403 branch added in #1172 threw a plain Error, and RunDownloadJob is registered attempts: 10 with exponential backoff from 30s. A permanent "this build has no entitlement key" rejection therefore retried nine more times over roughly 4h15m, during which the job reads `delayed` rather than `failed` - so the user sees a stuck download instead of the clear message #1172 was written to give them. Each retry also re-hits our own rate-limited Worker. Throw a named GatedContentAuthError from the download util and translate it to UnrecoverableError at the queue boundary in RunDownloadJob, alongside the existing cancellation case. Declaring the class in downloads.ts rather than throwing UnrecoverableError directly keeps BullMQ out of a module that docker_service and map_service also use. Harmless today because no catalog entry uses `auth`, but that ends with the first gated card - see #1204. Fixes #1195
This commit is contained in:
parent
2159c9dec6
commit
65bfd4f45a
|
|
@ -1,7 +1,7 @@
|
|||
import { Job, UnrecoverableError } from 'bullmq'
|
||||
import { RunDownloadJobParams, DownloadProgressData } from '../../types/downloads.js'
|
||||
import { QueueService } from '#services/queue_service'
|
||||
import { doResumableDownload } from '../utils/downloads.js'
|
||||
import { doResumableDownload, GatedContentAuthError } from '../utils/downloads.js'
|
||||
import { createHash } from 'crypto'
|
||||
import { DockerService } from '#services/docker_service'
|
||||
import { ZimService } from '#services/zim_service'
|
||||
|
|
@ -316,6 +316,13 @@ export class RunDownloadJob {
|
|||
if (userCancelled || abortController.signal.reason === 'user-cancel') {
|
||||
throw new UnrecoverableError(`Download cancelled: ${error.message}`)
|
||||
}
|
||||
// A rejected entitlement is permanent - this build either has the key or it
|
||||
// doesn't. Left as a plain Error it consumes all 10 attempts with exponential
|
||||
// backoff from 30s, so the job reads `delayed` for ~4h15m and the user sees a
|
||||
// stuck download instead of the message above.
|
||||
if (error instanceof GatedContentAuthError) {
|
||||
throw new UnrecoverableError(error.message)
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
if (cancelPollInterval !== null) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,21 @@ import { rename } from 'fs/promises'
|
|||
import path from 'path'
|
||||
import logger from '@adonisjs/core/services/logger'
|
||||
|
||||
/**
|
||||
* A gated source rejected this install's credentials (401/403).
|
||||
*
|
||||
* Permanent by nature: whether the entitlement key is baked in is a property of
|
||||
* the build, so no amount of retrying changes the answer. Declared here rather
|
||||
* than thrown as an UnrecoverableError directly so this module stays free of a
|
||||
* BullMQ dependency — RunDownloadJob translates it at the queue boundary.
|
||||
*/
|
||||
export class GatedContentAuthError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = 'GatedContentAuthError'
|
||||
}
|
||||
}
|
||||
|
||||
// Some upstream mirrors reject requests with a missing or generic User-Agent.
|
||||
// Notably, download.kiwix.org routes the large Wikimedia-family ZIMs (Wikipedia,
|
||||
// Wikiversity, Wikibooks — including the flagship full Wikipedia) to
|
||||
|
|
@ -73,7 +88,7 @@ export async function doResumableDownload({
|
|||
// failedReason is surfaced verbatim on the downloads UI.
|
||||
const status = error?.response?.status
|
||||
if (status === 401 || status === 403) {
|
||||
throw new Error(
|
||||
throw new GatedContentAuthError(
|
||||
'This content is hosted by Project NOMAD and requires an official release build. ' +
|
||||
`The download server rejected this install's credentials (HTTP ${status}).`
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue