oven-sh/bun · error · CertError

UNABLE_TO_GET_ISSUER_CERT

Error message

UNABLE_TO_GET_ISSUER_CERT

What it means

BoringSSL X509 verify result 2 (X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT): during the TLS handshake the verifier could not find the certificate of the issuer of a certificate in the chain, so the chain cannot be built to a trusted root. Bun maps the numeric code via get_cert_error_from_no (src/http/lib.rs:1521) into http::Error::Cert(CertError::UNABLE_TO_GET_ISSUER_CERT) and rejects the request with code "UNABLE_TO_GET_ISSUER_CERT" and message "unable to get issuer certificate" (src/runtime/webcore/fetch/FetchTasklet.rs:1376).

Source

Thrown at src/http/error.rs:125

    Core(#[from] bun_core::Error),
    #[error(transparent)]
    Sys(#[from] bun_errno::SystemErrno),
    #[error(transparent)]
    Zlib(bun_zlib::ZlibError),
    #[error(transparent)]
    Brotli(bun_brotli::Error),
    #[error(transparent)]
    Zstd(bun_zstd::ZstdError),
    #[error(transparent)]
    Picohttp(bun_picohttp::ParseResponseError),
}

#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error, strum::IntoStaticStr)]
pub enum CertError {
    #[error("OK")]
    OK,
    #[error("UNABLE_TO_GET_ISSUER_CERT")]
    UNABLE_TO_GET_ISSUER_CERT,
    #[error("UNABLE_TO_GET_CRL")]
    UNABLE_TO_GET_CRL,
    #[error("UNABLE_TO_DECRYPT_CERT_SIGNATURE")]
    UNABLE_TO_DECRYPT_CERT_SIGNATURE,
    #[error("UNABLE_TO_DECRYPT_CRL_SIGNATURE")]
    UNABLE_TO_DECRYPT_CRL_SIGNATURE,
    #[error("UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY")]
    UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY,
    #[error("CERT_SIGNATURE_FAILURE")]
    CERT_SIGNATURE_FAILURE,
    #[error("CRL_SIGNATURE_FAILURE")]
    CRL_SIGNATURE_FAILURE,
    #[error("CERT_NOT_YET_VALID")]
    CERT_NOT_YET_VALID,
    #[error("CERT_HAS_EXPIRED")]
    CERT_HAS_EXPIRED,
    #[error("CRL_NOT_YET_VALID")]

View on GitHub (pinned to 8c5296ac45)

Solutions

  1. Fix the server to serve the full chain (e.g. nginx: ssl_certificate fullchain.pem; certbot deployments already produce fullchain.pem)
  2. Pass the missing intermediate(s) to the client: fetch(url, { tls: { ca: [intermediatePem] } }) or set NODE_EXTRA_CA_CERTS=/path/to/intermediates.pem
  3. If the issuer is a private root, add that root via tls.ca or NODE_EXTRA_CA_CERTS
  4. As a last resort for internal services, fetch(url, { tls: { rejectUnauthorized: false } }) — never for public traffic

Example fix

# before (server serves only the leaf):
ssl_certificate /etc/letsencrypt/live/example.com/cert.pem;

# after (serve the full chain):
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
Defensive patterns

Strategy: try-catch

Type guard

function isCertErrorCode(e: unknown, code = "UNABLE_TO_GET_ISSUER_CERT"): e is Error & { code: string } {
  return e instanceof Error && (e as any).code === code;
}

Try / catch

try {
  const res = await fetch("https://api.example.com");
} catch (e) {
  if (isCertErrorCode(e, "UNABLE_TO_GET_ISSUER_CERT")) {
    // chain incomplete: alert the service owner, or retry with known intermediates pinned
    const res = await fetch("https://api.example.com", { tls: { ca: intermediatePems } });
  } else throw e;
}

Prevention

When it happens

Trigger: fetch("https://host") or Bun.connect with TLS (or an HTTPS request tunneled through an HTTP proxy, src/http/ProxyTunnel.rs:374) where reject_unauthorized is true (default) and the server presents a chain missing an intermediate CA certificate, or the intermediate is not in Bun's CA root store.

Common situations: Server admins who serve only the leaf cert (nginx misconfiguration using ssl_certificate with only the server.pem instead of fullchain.pem), freshly rotated certificates, private/internal PKIs whose intermediates were never distributed, or Docker images that trimmed the CA store.

Related errors


AI-assisted analysis of oven-sh/bun@8c5296ac45 (2026-08-16). Data as JSON: /api/errors/031a7606581427ed. Report an issue: GitHub.