{"record":{"id":"dcfa044593c30634","repo":"DIYgod/RSSHub","slug":"fortnite-api-responded-with-statusmessage-for","errorCode":null,"errorMessage":"Fortnite API responded with ${statusMessage} for ${apiUrl}","messagePattern":"Fortnite API responded with (.+?) for (.+?)","errorType":"exception","errorClass":"Error","httpStatus":503,"severity":"error","filePath":"lib/routes/fortnite/news.ts","lineNumber":65,"sourceCode":"    await page.route('**/*', (route) => {\n        const request = route.request();\n        request.resourceType() === 'document' ? route.continue() : route.abort();\n    });\n\n    // log manually (necessary for Playwright)\n    logger.http(`Requesting ${apiUrl}`);\n    let data;\n    try {\n        const response = await page.goto(apiUrl, {\n            waitUntil: 'networkidle',\n        });\n        if (!response) {\n            throw new Error(`No response received from ${apiUrl}`);\n        }\n        if (!response.ok()) {\n            const statusText = response.statusText();\n            const statusMessage = [response.status(), statusText].filter(Boolean).join(' ');\n            throw new Error(`Fortnite API responded with ${statusMessage} for ${apiUrl}`);\n        }\n        const contentType = response.headers()['content-type'];\n        if (!contentType?.includes('application/json')) {\n            throw new Error(`Fortnite API returned non-JSON response with content-type ${contentType ?? 'unknown'}`);\n        }\n\n        data = await response.json();\n    } finally {\n        await page.close();\n        await context.close();\n    }\n\n    const { blogList: list } = data;\n\n    const items = await Promise.all(\n        list.map((item) =>\n            cache.tryGet(item.link, () =>\n                Promise.resolve({","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/DIYgod/RSSHub/blob/bed535e0879dc71c5aff6f1e7bd1ac21ede40115/lib/routes/fortnite/news.ts#L47-L83","documentation":"After Playwright successfully receives a response, response.ok() is checked — it returns false for any HTTP status outside the 200–299 range. The Fortnite API returning a 4xx/5xx means the endpoint rejected the request (rate limited, unauthorized, not found, or server error). The status code and status text are joined into a single descriptive message.","triggerScenarios":"Fortnite's API returns 401/403 when the request lacks required cookies or headers (Playwright must have completed the auth flow), 404 when the endpoint path changed, 429 when rate limited, or 5xx during an Epic Games outage. Because the route loads the API URL as a browser document, missing browser-side auth tokens cause 401/403.","commonSituations":"Epic Games rotating the API URL or adding new required query parameters; the Playwright context not retaining session cookies; hitting the API too frequently and triggering rate limits; regional blocks returning 451.","solutions":["Check the numeric status in the message: 401/403 → auth/cookie issue, 404 → endpoint moved, 429 → rate limit, 5xx → upstream outage.","Ensure the Playwright browser context is shared and not isolated per request so session cookies persist.","Add the Fortnite API host to the allowed request types in the interceptor so auth/XHR subrequests succeed.","Report or update the apiUrl if Epic changed the endpoint."],"exampleFix":"// before\nif (!response.ok()) {\n    const statusText = response.statusText();\n    const statusMessage = [response.status(), statusText].filter(Boolean).join(' ');\n    throw new Error(`Fortnite API responded with ${statusMessage} for ${apiUrl}`);\n}\n\n// after\nif (!response.ok()) {\n    const statusText = response.statusText();\n    const statusMessage = [response.status(), statusText].filter(Boolean).join(' ');\n    if (response.status() === 429) {\n        throw new Error(`Fortnite API rate-limited the request (429). Reduce request frequency.`);\n    }\n    throw new Error(`Fortnite API responded with ${statusMessage} for ${apiUrl}`);\n}","handlingStrategy":"try-catch","validationCode":"// cannot pre-validate a server-side status; pre-flight with a HEAD request\nimport ofetch from '@/utils/ofetch';\ntry {\n  await ofetch.raw(apiUrl, { method: 'HEAD' });\n} catch (e) { /* expect non-2xx, log for visibility */ }","typeGuard":"const isRateLimited = (status: number) => status === 429;","tryCatchPattern":"try {\n  // ... page.goto + ok() check\n} catch (e) {\n  if (/429/.test((e as Error).message)) {\n    await new Promise((r) => setTimeout(r, 5000)); // back off\n  }\n  throw e;\n}","preventionTips":["Reuse a persistent browser context so session cookies/auth persist.","Back off on 429 responses.","Monitor Epic Games status for outages causing 5xx."],"tags":["playwright","http-status","api"],"backgroundTag":null,"analyzedSha":"bed535e0879dc71c5aff6f1e7bd1ac21ede40115","analyzedAt":"2026-08-12T19:29:35.364Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}