lissy93/web-check · warning · Error
crt.sh returned an unexpected response
Error message
crt.sh returned an unexpected response
What it means
crtSh fetches crt.sh's JSON issuance feed and expects an array; a non-array body (HTML error page, rate-limit text, empty body on crt.sh's frequent 503s) triggers this throw. crt.sh is a free, unauthenticated service with known instability under load.
Source
Thrown at api/subdomains.js:30
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,
});
const body = typeof res.data === 'string' ? res.data : '';
if (!body || /error|api count|quota/i.test(body)) throw new Error('hackerTarget unavailable');
return body.split('\n').map((line) => line.split(',')[0]);
};
const SOURCES = [
{ name: 'certSpotter', lookup: certSpotter },
{ name: 'crt.sh', lookup: crtSh },
{ name: 'hackerTarget', lookup: hackerTarget },
];View on GitHub (pinned to af1a97759f)
Solutions
- Treat crt.sh as best-effort: catch per-source and continue with other SOURCES (aggregation already exists)
- Throttle/retry crt.sh with exponential backoff (it is heavily rate-limited)
- If persistent, temporarily remove crt.sh from SOURCES until it recovers
Example fix
// before
const found = await crtSh(domain); // single-source dependency
// after
const safe = async (fn, name) => { try { return await fn(domain); } catch (e) { console.warn(`${name} unavailable: ${e.message}`); return []; } };
const found = [...await safe(certSpotter, 'certSpotter'), ...await safe(crtSh, 'crt.sh'), ...await safe(hackerTarget, 'hackerTarget')]; Defensive patterns
Strategy: retry
Validate before calling
null
Type guard
null
Try / catch
const withRetry = async (fn, tries = 3) => {
for (let i = 0; i < tries; i++) {
try { return await fn(domain); }
catch (e) { if (i === tries - 1) { console.warn(`crt.sh failed: ${e.message}`); return []; } await new Promise(r => setTimeout(r, 2 ** i * 1000)); }
}
}; Prevention
- Cache crt.sh results per domain to avoid repeat calls
- Throttle concurrent scans; crt.sh rate-limits aggressively
- Run multiple SOURCES concurrently with allSettled so one flaky provider never blocks results
When it happens
Trigger: crt.sh returns its common 'Server Error'/503 HTML page, rate-limits the caller, or returns a JSON error object instead of the issuance array.
Common situations: Bulk subdomain scans hitting crt.sh too fast, crt.sh being intermittently down (well-known), or timeout truncation producing a partial/empty body.
Related errors
AI-assisted analysis of lissy93/web-check@af1a97759f (2026-08-27).
Data as JSON: /api/errors/5873b07e3d2686bf.
Report an issue: GitHub.