oven-sh/bun · error · CertError

UNABLE_TO_GET_ISSUER_CERT_LOCALLY

UNABLE_TO_GET_ISSUER_CERT_LOCALLY

Error message

UNABLE_TO_GET_ISSUER_CERT_LOCALLY

What it means

X509 verify result 20 (X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY): the issuer of a certificate was found in the chain but the lookup against the local trust store failed — the chain's anchor (root) is not among the CAs the client trusts. Mapped via get_cert_error_from_no (src/http/lib.rs:1539) to CertError::UNABLE_TO_GET_ISSUER_CERT_LOCALLY. Distinct from UNABLE_TO_GET_ISSUER_CERT (2): here the chain is complete but untrusted locally.

Source

Thrown at src/http/error.rs:161

    #[error("CRL_NOT_YET_VALID")]
    CRL_NOT_YET_VALID,
    #[error("CRL_HAS_EXPIRED")]
    CRL_HAS_EXPIRED,
    #[error("ERROR_IN_CERT_NOT_BEFORE_FIELD")]
    ERROR_IN_CERT_NOT_BEFORE_FIELD,
    #[error("ERROR_IN_CERT_NOT_AFTER_FIELD")]
    ERROR_IN_CERT_NOT_AFTER_FIELD,
    #[error("ERROR_IN_CRL_LAST_UPDATE_FIELD")]
    ERROR_IN_CRL_LAST_UPDATE_FIELD,
    #[error("ERROR_IN_CRL_NEXT_UPDATE_FIELD")]
    ERROR_IN_CRL_NEXT_UPDATE_FIELD,
    #[error("OUT_OF_MEM")]
    OUT_OF_MEM,
    #[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")]

View on GitHub (pinned to 8c5296ac45)

Solutions

  1. Trust the private CA in Bun: set NODE_EXTRA_CA_CERTS=/path/to/org-ca.pem or pass it per-request via fetch(url, { tls: { ca: orgCaPem } })
  2. In containers, install ca-certificates and append the org CA to the system store (update-ca-certificates)
  3. Or get the service a publicly-trusted cert (Let's Encrypt) so no custom trust is needed
  4. Avoid rejectUnauthorized: false here — trusting the real CA is cheap and keeps verification on

Example fix

// before
await fetch("https://internal.corp.svc/api"); // UNABLE_TO_GET_ISSUER_CERT_LOCALLY

// after
import { readFileSync } from "node:fs";
await fetch("https://internal.corp.svc/api", {
  tls: { ca: readFileSync("./org-ca.pem", "utf8") },
});
Defensive patterns

Strategy: try-catch

Validate before calling

// startup check: private CA present before any internal fetch
import { readFileSync } from "node:fs";
const CA_PATH = process.env.ORG_CA_PATH ?? "./org-ca.pem";
export const orgCa = readFileSync(CA_PATH, "utf8"); // throws at boot if missing — fail fast
// then always: fetch(url, { tls: { ca: orgCa } })

Type guard

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

Try / catch

try {
  await fetch("https://internal.corp.svc/api");
} catch (e) {
  if (isCertErrorCode(e, "UNABLE_TO_GET_ISSUER_CERT_LOCALLY")) {
    // chain is complete but root not trusted locally — supply the org CA and retry once
    const res = await fetch("https://internal.corp.svc/api", { tls: { ca: orgCa } });
  } else throw e;
}

Prevention

When it happens

Trigger: fetch/https/Bun.connect to a server whose certificate chains to a private CA that is not in Bun's default root store — corporate/internal PKIs, self-hosted service meshes, test CAs — with reject_unauthorized left at its default true.

Common situations: Corporate environments with a private root CA not installed system-wide, minimal Docker images (alpine/distroless) missing ca-certificates, custom Kubernetes cluster CAs, freshly built machines without the org's CA package.

Understand the failure class

Related errors


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