{"record":{"id":"2b9758b3f49d251e","repo":"santifer/career-ops","slug":"http-res-status-res-statustext","errorCode":null,"errorMessage":"HTTP ${res.status} ${res.statusText}","messagePattern":"HTTP \\$\\{res\\.status\\} \\$\\{res\\.statusText\\}","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"upskill.mjs","lineNumber":982,"sourceCode":"            } catch (err) {\n              console.error(`Security Violation on Redirect: ${err.message}`);\n              await route.abort('blockedbyclient');\n              process.exit(1);\n            }\n          });\n\n          await page.goto(secureUrl, { waitUntil: 'networkidle', timeout: 30000 });\n          targetText = await page.innerText('body');\n        } catch (err) {\n          console.warn('Playwright extraction failed or blocked, trying fallback WebFetch...', err.message);\n          try {\n            const secureUrl = await validateUrlSecurity(inputSource);\n            // validateUrlSecurity only vets the initial URL; a redirect could still\n            // steer the fetch at an internal host (SSRF). The Playwright path\n            // re-validates per hop, but this plain fetch must refuse redirects\n            // outright — fail closed rather than follow an unvetted Location (#1851).\n            const res = await fetch(secureUrl, { signal: AbortSignal.timeout(30000), redirect: 'error' });\n            if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);\n            targetText = await res.text();\n          } catch (fetchErr) {\n            console.error(`Fatal: Failed to fetch JD from URL: ${fetchErr.message}`);\n            process.exit(1);\n          }\n        } finally {\n          if (browser) await browser.close();\n        }\n\n        // Whitespace-collapse + length-cap the fetched page text. Use compactText\n        // (string -> string), NOT normalizeJd: normalizeJd expects the { title,\n        // text } DOM-read object and returns { url, title, text }, so feeding it\n        // the innerText/fetch STRING silently produced { text: '' } — destroying\n        // the JD and then throwing `text.matchAll is not a function` downstream\n        // (#1894). compactText is the string-in/string-out helper this wants.\n        try {\n          const { compactText } = await import('./browser-extract.mjs');\n          targetText = compactText(targetText);","sourceCodeStart":964,"sourceCodeEnd":1000,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/upskill.mjs#L964-L1000","documentation":"upskill.mjs fetches JD URLs with fetch(..., { redirect: 'error' }) — it refuses to follow redirects (SSRF guard: validateUrlSecurity only vets the initial URL, so an unvetted Location hop is untrusted) — and throws `HTTP <status> <statusText>` whenever the response is not ok. The error is caught by the surrounding handler, which prints 'Fatal: Failed to fetch JD from URL' and exits 1.","triggerScenarios":"The initial (security-vetted) URL returns a non-2xx status: 301/302/307/308 redirects (turned into hard errors by redirect:'error'), 403 bot-blocking, 404 gone postings, 429 rate limits, or 5xx server errors.","commonSituations":"Job boards redirecting to an ATS (e.g. LinkedIn → company ATS) — the tool deliberately refuses to follow; Cloudflare or WAF returning 403 to non-browser clients; expired postings returning 404; transient 502/503 from the host.","solutions":["Find and use the final, direct ATS posting URL (e.g. the Greenhouse/Lever/Ashby URL) instead of the redirecting link.","For 404/410, the posting is gone — fetch an archived copy or use the report's archived JD.","For 403, download the page in a browser and pass the saved file/text instead of the URL.","For 429/5xx, wait and retry; the fetch also has a 30s timeout, so slow hosts may need the file path too.","Note redirects are blocked by design (#1851); there is no flag to enable following them."],"exampleFix":"// before\nnode upskill.mjs --url https://www.linkedin.com/jobs/view/12345  # 302 → blocked\n// after\nnode upskill.mjs --url https://boards.greenhouse.io/company/jobs/12345","handlingStrategy":"try-catch","validationCode":"const res = await fetch(url, { method: 'HEAD', redirect: 'manual' });\nif ([301,302,303,307,308].includes(res.status)) console.error('URL redirects; resolve to the final ATS URL first');\nelse if (!res.ok) console.error(`URL returns ${res.status}; use an archived copy or a direct link`);","typeGuard":null,"tryCatchPattern":"try {\n  await upskillFromUrl(url);\n} catch (e) {\n  const m = e.message.match(/HTTP (\\d+)/);\n  if (m && ['301','302','307','308'].includes(m[1])) console.error('blocked redirect: use the final posting URL');\n  else if (m && m[1] === '404') console.error('posting gone: use archived JD');\n  else if (m && ['429'].includes(m[1])) console.error('rate limited: retry later');\n  else throw e;\n}","preventionTips":["Always pass the direct ATS (Greenhouse/Lever/Ashby) URL, not aggregator links that redirect.","Remember redirect:'error' is intentional (#1851) — never try to follow redirects through the tool.","Archive the JD at evaluation time so a later 404 doesn't block re-analysis.","For bot-protected sites (403), save the page and use file input."],"tags":["http","network","ssrf","redirect"],"backgroundTag":"http-error-response","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}