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

  1. Log/handle certSpotter failures as a degraded source — SOURCES aggregation should continue via crt.sh/hackerTarget
  2. Verify CERTSPOTTER_TOKEN is set and valid (re-test the token with curl against api.certspotter.com)
  3. 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

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.