fix(api): surface browser pool exhaustion as 429 not 500

This commit is contained in:
germondai
2026-06-29 22:35:01 +02:00
parent a42b3a11af
commit 182d3dc1eb
+14 -42
View File
@@ -47,26 +47,8 @@ function getDeps() {
const startTime = Date.now()
// Concurrency limiter — gates in-flight scrape work at exactly POOL_SIZE so we
// reject immediately (HTTP 429) instead of letting BrowserPool.acquire() block
// for up to 5s. Prowlarr/Jackett use the 429 to back off cleanly.
let activeJobs = 0
const MAX_CONCURRENT = POOL_SIZE
function tryAcquireSlot(): boolean {
if (activeJobs < MAX_CONCURRENT) {
activeJobs++
return true
}
return false
}
function releaseSlot(): void {
if (activeJobs > 0) activeJobs--
}
// Build a FlareSolverr v2-shaped error envelope. Used by /v1, /scrape, and the
// second-line PoolExhaustedError catch-arm.
// Build a FlareSolverr v2-shaped error envelope. Used by /v1 for every error
// path and by /scrape when the pool is exhausted (PoolExhaustedError → 429).
function flareSolverrError(url: string, message: string): FlareSolverrResponse {
const now = Date.now()
return {
@@ -108,8 +90,6 @@ new Elysia()
busy: stats.busy,
restarts: stats.restarts,
queueDepth: 0,
activeJobs,
maxConcurrent: MAX_CONCURRENT,
}
})
@@ -129,11 +109,6 @@ new Elysia()
return flareSolverrError(req.url, "Browser pool initializing, retry in a few seconds")
}
if (!tryAcquireSlot()) {
set.status = 429
return flareSolverrError(req.url, "Browser pool saturated, retry shortly")
}
try {
const result = await scrape({ url: req.url, maxTimeout: req.maxTimeout ?? 60_000, headers: req.headers }, getDeps())
return {
@@ -154,32 +129,29 @@ new Elysia()
} catch (err) {
set.status = err instanceof PoolExhaustedError ? 429 : 500
return flareSolverrError(req.url, err instanceof Error ? err.message : String(err))
} finally {
releaseSlot()
}
})
// Native TRAWL API — richer response (tier, timings, sessionCached)
// Native TRAWL API — richer response (tier, timings, sessionCached).
// Error mapping:
// 503 — pool still initializing (native { error })
// 429 — pool exhausted (FlareSolverr envelope; uniform with /v1)
// 500 — other scrape exception (native { error })
.post("/scrape", async ({ body, set }) => {
if (!pool) {
set.status = 503
return flareSolverrError("", "Browser pool initializing, retry in a few seconds")
return { error: "Browser pool initializing, retry in a few seconds" }
}
const req = body as ScrapeRequest
if (!tryAcquireSlot()) {
set.status = 429
return flareSolverrError(req.url ?? "", "Browser pool saturated, retry shortly")
}
try {
return await scrape(req, getDeps())
} catch (err) {
set.status = err instanceof PoolExhaustedError ? 429 : 500
return flareSolverrError(req.url ?? "", err instanceof Error ? err.message : String(err))
} finally {
releaseSlot()
if (err instanceof PoolExhaustedError) {
set.status = 429
return flareSolverrError(req.url ?? "", "Browser pool saturated, retry shortly")
}
set.status = 500
return { error: err instanceof Error ? err.message : String(err) }
}
})