{"record":{"id":"c94d398dc2c67176","repo":"lissy93/web-check","slug":"certspotter-returned-an-unexpected-response","errorCode":null,"errorMessage":"certSpotter returned an unexpected response","messagePattern":"certSpotter returned an unexpected response","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"api/subdomains.js","lineNumber":20,"sourceCode":"import middleware from './_common/middleware.js';\nimport { httpGet } from './_common/http.js';\nimport { parseTarget } from './_common/parse-target.js';\n\nconst MAX_SUBDOMAINS = 500;\nconst SOURCE_TIMEOUT = 6000;\nconst HOSTNAME_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;\n\nconst baseDomain = (host) => psl.parse(host)?.domain || host;\nconst isIpAddress = (host) => /^\\d{1,3}(\\.\\d{1,3}){3}$/.test(host) || host.includes(':');\n\nconst certSpotter = async (domain) => {\n  const token = process.env.CERTSPOTTER_TOKEN;\n  const res = await httpGet('https://api.certspotter.com/v1/issuances', {\n    params: { domain, include_subdomains: 'true', expand: 'dns_names' },\n    headers: { Accept: 'application/json', ...(token && { Authorization: `Bearer ${token}` }) },\n    timeout: SOURCE_TIMEOUT,\n  });\n  if (!Array.isArray(res.data)) throw new Error('certSpotter returned an unexpected response');\n  return res.data.flatMap((row) => (Array.isArray(row?.dns_names) ? row.dns_names : []));\n};\n\nconst crtSh = async (domain) => {\n  const res = await httpGet('https://crt.sh/', {\n    params: { q: `%.${domain}`, output: 'json' },\n    headers: { Accept: 'application/json' },\n    timeout: SOURCE_TIMEOUT,\n  });\n  if (!Array.isArray(res.data)) throw new Error('crt.sh returned an unexpected response');\n  return res.data.flatMap((row) => String(row?.name_value ?? '').split('\\n'));\n};\n\nconst hackerTarget = async (domain) => {\n  const res = await httpGet('https://api.hackertarget.com/hostsearch/', {\n    params: { q: domain },\n    timeout: SOURCE_TIMEOUT,\n  });","sourceCodeStart":2,"sourceCodeEnd":38,"githubUrl":"https://github.com/lissy93/web-check/blob/af1a97759fc8bcc43c876c94f2ccb018ce215f90/api/subdomains.js#L2-L38","documentation":"certSpotter queries certspotter.com's issuance API expecting a JSON array; if res.data is not an array it throws this error. Typical cause is an error payload (JSON object with an error field) returned with a 200-class response through the http helper, or an HTML/XML error page parsed into something other than an array.","triggerScenarios":"Invalid/expired CERTSPOTTER_TOKEN causing an auth error object; rate-limit response body; API schema change; domain value the API rejects (e.g. empty or malformed after parsing).","commonSituations":"CERTSPOTTER_TOKEN env var expired or mistyped in the deployment environment, hitting certspotter rate limits during bulk scans, or upstream API contract drift after an API version change.","solutions":["Log/handle certSpotter failures as a degraded source — SOURCES aggregation should continue via crt.sh/hackerTarget","Verify CERTSPOTTER_TOKEN is set and valid (re-test the token with curl against api.certspotter.com)","Retry with backoff if the body indicates rate limiting rather than auth failure"],"exampleFix":"// before\nconst names = await certSpotter(domain); // throws on error-object response\n\n// after\nconst SOURCES = [\n  { name: 'certSpotter', lookup: certSpotter },\n  ...\n];\nconst results = await Promise.allSettled(SOURCES.map(s => s.lookup(domain)));\nconst names = results.flatMap((r, i) => r.status === 'fulfilled' ? r.value : (console.warn(`${SOURCES[i].name} failed: ${r.reason.message}`), []));","handlingStrategy":"fallback","validationCode":"null","typeGuard":"const isIssuanceArray = (d) => Array.isArray(d);","tryCatchPattern":"const sources = [certSpotter, crtSh, hackerTarget];\nconst settled = await Promise.allSettled(sources.map(fn => fn(domain)));\nconst names = settled.flatMap(r => r.status === 'fulfilled' ? r.value : []);","preventionTips":["Verify CERTSPOTTER_TOKEN validity before deploying bulk scans","Degrade per-source, never fail the whole aggregation on one provider","Log raw response bodies (truncated) for unrecognised shapes to catch API drift"],"tags":["third-party-api","certspotter","subdomains","response-shape"],"backgroundTag":"third-party-api-unexpected-response","analyzedSha":"af1a97759fc8bcc43c876c94f2ccb018ce215f90","analyzedAt":"2026-08-27T11:44:27.410Z","schemaVersion":2},"datasetVersion":"2026-08-27T13:17:12.746Z"}