mirror of
https://github.com/germondai/trawl.git
synced 2026-08-17 12:11:23 +02:00
fix(api): surface browser pool exhaustion as 429 not 500
This commit is contained in:
+14
-42
@@ -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) }
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user