oven-sh/bun · error · CertError
CERT_HAS_EXPIRED
Error message
CERT_HAS_EXPIRED
What it means
X509 verify result 10 (X509_V_ERR_CERT_HAS_EXPIRED): the certificate's notAfter is in the past, so it is no longer valid. Mapped via get_cert_error_from_no (src/http/lib.rs:1529) to CertError::CERT_HAS_EXPIRED, message "certificate has expired" (FetchTasklet.rs:1403). One of the most common TLS failures.
Source
Thrown at src/http/error.rs:141
#[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")]
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")]View on GitHub (pinned to 8c5296ac45)
Solutions
- Renew the certificate on the server and reload the web server (certbot renew && systemctl reload nginx)
- If already renewed, confirm the server actually serves the new chain: openssl s_client -connect host:443 | openssl x509 -noout -enddate
- Set up expiry monitoring (certbot auto-renew timer, Prometheus blackbox_exporter) so it never recurs
- For internal throwaway environments only: fetch(url, { tls: { rejectUnauthorized: false } }) as a stopgap
Example fix
# check echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -enddate # renew certbot renew --force-renewal && systemctl reload nginx
Defensive patterns
Strategy: try-catch
Validate before calling
// pre-check your OWN endpoint's expiry before it becomes an outage
import { connect as tlsConnect } from "node:tls";
function daysUntilCertExpiry(host: string, port = 443): Promise<number> {
return new Promise((resolve, reject) => {
const s = tlsConnect({ host, port, servername: host, rejectUnauthorized: false }, () => {
const { valid_to } = s.getPeerCertificate();
s.end();
resolve((new Date(valid_to).getTime() - Date.now()) / 86_400_000);
});
s.once("error", reject);
});
}
if (await daysUntilCertExpiry("api.example.com") < 14) await alertOps("cert expiring"); Type guard
function isCertErrorCode(e: unknown, code = "CERT_HAS_EXPIRED"): e is Error & { code: string } {
return e instanceof Error && (e as any).code === code;
} Try / catch
try {
await fetch(url);
} catch (e) {
if (isCertErrorCode(e, "CERT_HAS_EXPIRED")) {
// server-side problem — page the owner; retrying won't help until renewal
throw new Error("Peer TLS certificate has expired");
}
throw e;
} Prevention
- Automate renewal (certbot timers / ACME clients) and alert at T-14 and T-3 days
- Cert-expiry monitoring in blackbox checks for every public hostname
- Reload the web server after renewal — a renewed file on disk doesn't update listening sockets
When it happens
Trigger: Any TLS handshake (fetch, Bun.connect, proxied CONNECT tunnel) where the leaf or an intermediate in the served chain is past its expiry and reject_unauthorized is true (default).
Common situations: Forgotten Let's Encrypt renewals (90-day certs), expired certs on internal/staging servers nobody monitors, legacy appliances with 1-year certs, or browsers caching an old chain while the server already serves the renewed one.
Related errors
- UNABLE_TO_GET_ISSUER_CERT
- UNABLE_TO_DECRYPT_CERT_SIGNATURE
- UNABLE_TO_GET_CRL
- UNABLE_TO_DECRYPT_CRL_SIGNATURE
- UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY
AI-assisted analysis of oven-sh/bun@8c5296ac45 (2026-08-16).
Data as JSON: /api/errors/533fab3423cfb973.
Report an issue: GitHub.