oven-sh/bun · error · CertError

DEPTH_ZERO_SELF_SIGNED_CERT

DEPTH_ZERO_SELF_SIGNED_CERT

Error message

DEPTH_ZERO_SELF_SIGNED_CERT

What it means

X509 verify result 18 (X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT): the leaf certificate itself is self-signed (issuer == subject) and is not present in the trust store, so it cannot be trusted. Mapped via get_cert_error_from_no (src/http/lib.rs:1537) to CertError::DEPTH_ZERO_SELF_SIGNED_CERT (message per FetchTasklet.rs table).

Source

Thrown at src/http/error.rs:157

    #[error("CERT_NOT_YET_VALID")]
    CERT_NOT_YET_VALID,
    #[error("CERT_HAS_EXPIRED")]
    CERT_HAS_EXPIRED,
    #[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")]

View on GitHub (pinned to 8c5296ac45)

Solutions

  1. For local dev, use mkcert to create a locally-trusted cert: mkcert -install && mkcert localhost
  2. Or trust the exact self-signed cert on the client: fetch(url, { tls: { ca: selfSignedPem } }) / NODE_EXTRA_CA_CERTS
  3. Get a real certificate (Let's Encrypt / internal CA-issued) for anything non-local
  4. Temporary stopgap only for localhost: fetch(url, { tls: { rejectUnauthorized: false } }) — scope it to localhost so it can't be used against public hosts

Example fix

# before
await fetch("https://localhost:3000/api"); // DEPTH_ZERO_SELF_SIGNED_CERT

# after (dev): mkcert -install && mkcert localhost
# serve localhost.pem, and no client change is needed
Defensive patterns

Strategy: try-catch

Validate before calling

// for local dev: detect a self-signed endpoint up front and scope any bypass to localhost
const isLocal = (u: string) => /^(https?:\/\/)?(localhost|127\.0\.0\.1|\[::1\])(:|\/|$)/.test(u);
if (!isLocal(target) && process.env.ALLOW_INSECURE_TLS === "1") {
  throw new Error("Insecure TLS bypass is only permitted for localhost targets");
}

Type guard

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

Try / catch

try {
  await fetch("https://localhost:3000/api");
} catch (e) {
  if (isCertErrorCode(e, "DEPTH_ZERO_SELF_SIGNED_CERT")) {
    // dev-only escape hatch, scoped to localhost
    const res = await fetch("https://localhost:3000/api", { tls: { rejectUnauthorized: false } });
  } else throw e;
}

Prevention

When it happens

Trigger: fetch("https://localhost:...") or Bun.connect against a local dev server using `openssl req -x509` self-signed certs, an IoT/appliance web UI, or any endpoint presenting a self-signed leaf — with default verification on.

Common situations: Local development (next.js/vite dev servers with self-signed TLS), internal tools with never-provisioned certs, hardware devices, k8s services with self-generated certs.

Understand the failure class

Related errors


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