oven-sh/bun · error · CertError

CERT_UNTRUSTED

CERT_UNTRUSTED

Error message

CERT_UNTRUSTED

What it means

CertError::CERT_UNTRUSTED maps BoringSSL X509 verify code 27 (X509_V_ERR_CERT_UNTRUSTED) via get_cert_error_from_no() in src/http/lib.rs:1546. The chain built completely to a root, but that root (or an intermediate treated as anchor) is not present in the trust store used by the connection. JS error.code is CERT_UNTRUSTED with message 'certificate not trusted' (FetchTasklet.rs:1460).

Source

Thrown at src/http/error.rs:175

    #[error("DEPTH_ZERO_SELF_SIGNED_CERT")]
    DEPTH_ZERO_SELF_SIGNED_CERT,
    #[error("SELF_SIGNED_CERT_IN_CHAIN")]
    SELF_SIGNED_CERT_IN_CHAIN,
    #[error("UNABLE_TO_GET_ISSUER_CERT_LOCALLY")]
    UNABLE_TO_GET_ISSUER_CERT_LOCALLY,
    #[error("UNABLE_TO_VERIFY_LEAF_SIGNATURE")]
    UNABLE_TO_VERIFY_LEAF_SIGNATURE,
    #[error("CERT_CHAIN_TOO_LONG")]
    CERT_CHAIN_TOO_LONG,
    #[error("CERT_REVOKED")]
    CERT_REVOKED,
    #[error("INVALID_CA")]
    INVALID_CA,
    #[error("PATH_LENGTH_EXCEEDED")]
    PATH_LENGTH_EXCEEDED,
    #[error("INVALID_PURPOSE")]
    INVALID_PURPOSE,
    #[error("CERT_UNTRUSTED")]
    CERT_UNTRUSTED,
    #[error("CERT_REJECTED")]
    CERT_REJECTED,
    #[error("SUBJECT_ISSUER_MISMATCH")]
    SUBJECT_ISSUER_MISMATCH,
    #[error("AKID_SKID_MISMATCH")]
    AKID_SKID_MISMATCH,
    #[error("AKID_ISSUER_SERIAL_MISMATCH")]
    AKID_ISSUER_SERIAL_MISMATCH,
    #[error("KEYUSAGE_NO_CERTSIGN")]
    KEYUSAGE_NO_CERTSIGN,
    #[error("UNABLE_TO_GET_CRL_ISSUER")]
    UNABLE_TO_GET_CRL_ISSUER,
    #[error("UNHANDLED_CRITICAL_EXTENSION")]
    UNHANDLED_CRITICAL_EXTENSION,
    #[error("KEYUSAGE_NO_CRL_SIGN")]
    KEYUSAGE_NO_CRL_SIGN,
    #[error("UNHANDLED_CRITICAL_CRL_EXTENSION")]

View on GitHub (pinned to 8c5296ac45)

Solutions

  1. Export the CA root that actually terminates the chain (openssl s_client -showcerts, last cert) and pass it: fetch(url, { tls: { ca: await Bun.file('corp-root.pem').text() })
  2. Or set NODE_EXTRA_CA_CERTS=/path/to/corp-root.pem for the process
  3. If behind a TLS-inspection proxy, install the proxy's root CA bundle system-wide (update-ca-certificates)
  4. Confirm the anchor: openssl verify -CAfile corp-root.pem leaf.pem

Example fix

// before
await fetch("https://svc.corp.internal/api");
// after
const ca = await Bun.file("/certs/corp-root.pem").text();
await fetch("https://svc.corp.internal/api", { tls: { ca } });
Defensive patterns

Strategy: try-catch

Validate before calling

import tls from "node:tls";
function endpointTrusted(host: string, ca: string, port = 443): Promise<boolean> {
  return new Promise((resolve) => {
    const s = tls.connect({ host, port, servername: host, ca }, () => { resolve(s.authorized); s.destroy(); });
    s.on("error", () => resolve(false));
  });
}
const ca = await Bun.file("corp-root.pem").text();
if (!(await endpointTrusted("svc.corp.internal", ca))) throw new Error("corp root does not trust endpoint");
await fetch("https://svc.corp.internal/api", { tls: { ca } });

Type guard

function isCertUntrusted(e: unknown): e is Error & { code: "CERT_UNTRUSTED" } {
  return e instanceof Error && (e as any).code === "CERT_UNTRUSTED";
}

Try / catch

try { await fetch(url, { tls: { ca } }); } catch (e) { if (isCertUntrusted(e)) { logger.error("missing corporate CA — set NODE_EXTRA_CA_CERTS"); process.exitCode = 1; } throw e; }

Prevention

When it happens

Trigger: fetch()/Bun.connect to a host whose TLS chain terminates in a private/corporate CA that was never added via tls: { ca } or NODE_EXTRA_CA_CERTS; corporate TLS-inspecting proxy (Zscaler, Netskope) re-signing traffic with its own root.

Common situations: Corporate laptops behind SSL-inspection proxies, internal *.corp domains served by a private CA, CI containers missing the company root, docker images that don't COPY the corporate CA bundle.

Understand the failure class

Related errors


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