lissy93/web-check · warning · Error
certSpotter returned an unexpected response
Error message
certSpotter returned an unexpected response
What it means
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.
Source
Thrown at api/subdomains.js:20
import middleware from './_common/middleware.js';
import { httpGet } from './_common/http.js';
import { parseTarget } from './_common/parse-target.js';
const MAX_SUBDOMAINS = 500;
const SOURCE_TIMEOUT = 6000;
const HOSTNAME_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
const baseDomain = (host) => psl.parse(host)?.domain || host;
const isIpAddress = (host) => /^\d{1,3}(\.\d{1,3}){3}$/.test(host) || host.includes(':');
const certSpotter = async (domain) => {
const token = process.env.CERTSPOTTER_TOKEN;
const res = await httpGet('https://api.certspotter.com/v1/issuances', {
params: { domain, include_subdomains: 'true', expand: 'dns_names' },
headers: { Accept: 'application/json', ...(token && { Authorization: `Bearer ${token}` }) },
timeout: SOURCE_TIMEOUT,
});
if (!Array.isArray(res.data)) throw new Error('certSpotter returned an unexpected response');
return res.data.flatMap((row) => (Array.isArray(row?.dns_names) ? row.dns_names : []));
};
const crtSh = async (domain) => {
const res = await httpGet('https://crt.sh/', {
params: { q: `%.${domain}`, output: 'json' },
headers: { Accept: 'application/json' },
timeout: SOURCE_TIMEOUT,
});
if (!Array.isArray(res.data)) throw new Error('crt.sh returned an unexpected response');
return res.data.flatMap((row) => String(row?.name_value ?? '').split('\n'));
};
const hackerTarget = async (domain) => {
const res = await httpGet('https://api.hackertarget.com/hostsearch/', {
params: { q: domain },
timeout: SOURCE_TIMEOUT,
});View on GitHub (pinned to af1a97759f)
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
Example fix
// before
const names = await certSpotter(domain); // throws on error-object response
// after
const SOURCES = [
{ name: 'certSpotter', lookup: certSpotter },
...
];
const results = await Promise.allSettled(SOURCES.map(s => s.lookup(domain)));
const names = results.flatMap((r, i) => r.status === 'fulfilled' ? r.value : (console.warn(`${SOURCES[i].name} failed: ${r.reason.message}`), [])); Defensive patterns
Strategy: fallback
Validate before calling
null
Type guard
const isIssuanceArray = (d) => Array.isArray(d);
Try / catch
const sources = [certSpotter, crtSh, hackerTarget]; const settled = await Promise.allSettled(sources.map(fn => fn(domain))); const names = settled.flatMap(r => r.status === 'fulfilled' ? r.value : []);
Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
Related errors
AI-assisted analysis of lissy93/web-check@af1a97759f (2026-08-27).
Data as JSON: /api/errors/c94d398dc2c67176.
Report an issue: GitHub.