oven-sh/bun · error · TypeError

INVALID_EXTENSION

INVALID_EXTENSION

Error message

INVALID_EXTENSION

What it means

CertError::INVALID_EXTENSION is Bun's mapping of BoringSSL X509_V_ERR_INVALID_EXTENSION (verify code 41). It fires during TLS chain validation when a certificate presented by the peer contains an X.509 extension that is malformed, duplicated, or internally inconsistent. Bun converts SSL_get_verify_result() into Error::Cert(CertError::INVALID_EXTENSION) (src/http/lib.rs get_cert_error_from_no) and fetch() rejects with a SystemError whose code is "INVALID_EXTENSION" and message "invalid or inconsistent certificate extension".

Source

Thrown at src/http/error.rs:203

    #[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")]
    UNHANDLED_CRITICAL_CRL_EXTENSION,
    #[error("INVALID_NON_CA")]
    INVALID_NON_CA,
    #[error("PROXY_PATH_LENGTH_EXCEEDED")]
    PROXY_PATH_LENGTH_EXCEEDED,
    #[error("KEYUSAGE_NO_DIGITAL_SIGNATURE")]
    KEYUSAGE_NO_DIGITAL_SIGNATURE,
    #[error("PROXY_CERTIFICATES_NOT_ALLOWED")]
    PROXY_CERTIFICATES_NOT_ALLOWED,
    #[error("INVALID_EXTENSION")]
    INVALID_EXTENSION,
    #[error("INVALID_POLICY_EXTENSION")]
    INVALID_POLICY_EXTENSION,
    #[error("NO_EXPLICIT_POLICY")]
    NO_EXPLICIT_POLICY,
    #[error("DIFFERENT_CRL_SCOPE")]
    DIFFERENT_CRL_SCOPE,
    #[error("UNSUPPORTED_EXTENSION_FEATURE")]
    UNSUPPORTED_EXTENSION_FEATURE,
    #[error("UNNESTED_RESOURCE")]
    UNNESTED_RESOURCE,
    #[error("PERMITTED_VIOLATION")]
    PERMITTED_VIOLATION,
    #[error("EXCLUDED_VIOLATION")]
    EXCLUDED_VIOLATION,
    #[error("SUBTREE_MINMAX")]
    SUBTREE_MINMAX,
    #[error("APPLICATION_VERIFICATION")]

View on GitHub (pinned to 8c5296ac45)

Solutions

  1. Dump the chain and find the broken extension: openssl s_client -connect host:443 -showcerts | openssl x509 -noout -text and look for repeated OIDs or malformed values
  2. Regenerate the offending cert (usually the leaf) with a clean extension config; verify with openssl verify -CAfile ca.pem cert.pem before deploying
  3. If a middlebox (proxy/LB) mangles the cert, fix or trust that middlebox properly instead of bypassing validation
  4. As a throwaway diagnostic only, retry with tls: { rejectUnauthorized: false } to confirm the cert is the cause; never ship this

Example fix

# before (extfile duplicates basicConstraints -> INVALID_EXTENSION)
[req]
distinguished_name = dn
x509_extensions = v3
[v3]
basicConstraints = critical, CA:FALSE
basicConstraints = CA:TRUE
subjectAltName = DNS:example.com

# after (each extension appears once)
[v3]
basicConstraints = critical, CA:FALSE
subjectAltName = DNS:example.com
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-flight: fetch and lint the peer chain before relying on it
import tls from "node:tls";
import { execFileSync } from "node:child_process";
export function lintPeerCert(host, port = 443) {
  const pem = execFileSync("openssl", ["s_client", "-connect", `${host}:${port}`, "-showcerts", "-servername", host], { input: "" }).toString();
  const out = execFileSync("openssl", ["x509", "-noout", "-text"], { input: pem }).toString();
  const seen = new Set();
  for (const m of out.matchAll(/X509v3 ([^:\n]+):/g)) {
    if (seen.has(m[1])) throw new Error(`duplicate extension: ${m[1]}`);
    seen.add(m[1]);
  }
}

Type guard

export function isInvalidExtension(e): e is Error & { code: "INVALID_EXTENSION" } {
  return e instanceof Error && (e as any).code === "INVALID_EXTENSION";
}

Try / catch

try {
  const res = await fetch("https://api.example.com/v1");
} catch (e) {
  if (e instanceof Error && e.code === "INVALID_EXTENSION") {
    // server cert has a malformed/duplicated extension - server-side fix required
    await alertCertOwner("api.example.com", e);
  } else throw e;
}

Prevention

When it happens

Trigger: fetch("https://host") or any TLS connect where the server's leaf/intermediate cert has: the same extension OID present twice (X.509 forbids duplicates), a critical extension with an empty/undecodable payload, or an extension whose DER does not parse. The handshake is aborted before any HTTP bytes flow.

Common situations: Certificates produced by hand-rolled ASN.1 scripts or misconfigured CFSSL/openssl -extfile templates; TLS-intercepting corporate proxies or load balancers that re-sign certs and botch extensions; stale test certs accepted by older OpenSSL but rejected by BoringSSL's stricter path builder.

Related errors


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