{"record":{"id":"cc8f9fbd93cdd6ae","repo":"DIYgod/RSSHub","slug":"fortnite-api-returned-non-json-response-with-conte","errorCode":null,"errorMessage":"Fortnite API returned non-JSON response with content-type ${contentType ?? 'unknown'}","messagePattern":"Fortnite API returned non-JSON response with content-type (.+?)","errorType":"exception","errorClass":"Error","httpStatus":503,"severity":"error","filePath":"lib/routes/fortnite/news.ts","lineNumber":69,"sourceCode":"\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({\n                    title: item.title,\n                    link: `${rootUrl}/${path}/${item.slug}?lang=${language}`,\n                    pubDate: parseDate(item.date),\n                    author: item.author,","sourceCodeStart":51,"sourceCodeEnd":87,"githubUrl":"https://github.com/DIYgod/RSSHub/blob/bed535e0879dc71c5aff6f1e7bd1ac21ede40115/lib/routes/fortnite/news.ts#L51-L87","documentation":"Even when the HTTP status is OK, the response may not be JSON. The route inspects the content-type header and requires it to contain 'application/json'; otherwise the body is assumed to be an HTML error page, interstitial, or CAPTCHA, and the route throws with the observed content-type. This guards against silently feeding HTML into JSON parsing downstream.","triggerScenarios":"Fortnite's infrastructure returns an HTML maintenance/CAPTCHA page with a 200 status, a CDN/WAF serves an HTML challenge page (Cloudflare, Akamai), the endpoint was replaced with an HTML redirect page, or the API now returns JSON-LD or a different content-type token. The contentType variable holds whatever was sent (or 'unknown' if the header was absent).","commonSituations":"Anti-bot/WAF challenge pages served before the real API responds; Epic Games rotating to a GraphQL or different content-type; CDN caching an old HTML version; the browser sending an Accept header that triggers HTML negotiation.","solutions":["Log or return the first 500 bytes of the body to identify whether it is a CAPTCHA, maintenance page, or redirect.","Set an explicit Accept: application/json header in the Playwright request context / interceptor.","Check whether a WAF challenge is involved and whether the route needs the Playwright context to solve it before the API call.","Update the content-type check if the API now serves application/vnd.api+json or similar."],"exampleFix":"// before\nconst contentType = response.headers()['content-type'];\nif (!contentType?.includes('application/json')) {\n    throw new Error(`Fortnite API returned non-JSON response with content-type ${contentType ?? 'unknown'}`);\n}\n\n// after\nconst contentType = response.headers()['content-type'] ?? '';\nif (!contentType.includes('json')) {\n    const bodySnippet = (await response.text()).slice(0, 200);\n    throw new Error(`Fortnite API returned non-JSON (${contentType || 'unknown'}). Body preview: ${bodySnippet}`);\n}","handlingStrategy":"type-guard","validationCode":"// set an explicit Accept header in the Playwright context\nawait context.setExtraHTTPHeaders({ Accept: 'application/json' });","typeGuard":"const isJsonContentType = (ct: string | undefined): boolean => !!ct && ct.includes('json');","tryCatchPattern":null,"preventionTips":["Set Accept: application/json so the server does not negotiate HTML.","Log a body snippet when content-type mismatches to identify CAPTCHA/maintenance pages.","Update the check to accept any 'json' subtype (application/vnd.api+json)."],"tags":["playwright","content-type","anti-crawler"],"backgroundTag":null,"analyzedSha":"bed535e0879dc71c5aff6f1e7bd1ac21ede40115","analyzedAt":"2026-08-12T19:29:35.364Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}