quarkusio/quarkus · error · IllegalArgumentException
The certificate chain cannot be null or empty
Error message
The certificate chain cannot be null or empty
What it means
writePrivateKeyAndCertificateChainsAsPem also validates the X509Certificate[] argument and throws IllegalArgumentException when it is null or zero-length. A certificate chain is mandatory to write the chain PEM; an empty result means the ACME order returned no certificates, so there is nothing usable to persist.
Source
Thrown at extensions/tls-registry/cli/src/main/java/io/quarkus/tls/cli/letsencrypt/LetsEncryptHelpers.java:46
import io.smallrye.certs.CertificateUtils;
import io.vertx.core.json.JsonObject;
public class LetsEncryptHelpers {
public static final String DEFAULT_ACME_URL = "https://acme-v02.api.letsencrypt.org/directory";
public static final String DEFAULT_ACME_STAGING_URL = "https://acme-staging-v02.api.letsencrypt.org/directory";
public static final String TLS_AUDIT_LOG = "io.quarkus.tls.audit";
static Logger LOGGER = Logger.getLogger(LetsEncryptHelpers.class);
public static Logger AUDIT = Logger.getLogger(LetsEncryptHelpers.TLS_AUDIT_LOG);
public static void writePrivateKeyAndCertificateChainsAsPem(PrivateKey pk, X509Certificate[] chain, File privateKeyFile,
File certificateChainFile) throws Exception {
if (pk == null) {
throw new IllegalArgumentException("The private key cannot be null");
}
if (chain == null || chain.length == 0) {
throw new IllegalArgumentException("The certificate chain cannot be null or empty");
}
AUDIT.debug("Writing private key to file: " + privateKeyFile.getAbsolutePath());
CertificateUtils.writePrivateKeyToPem(pk, null, privateKeyFile);
AUDIT.debug("Writing certificate chain to file: " + certificateChainFile.getAbsolutePath());
if (chain.length == 1) {
CertificateUtils.writeCertificateToPEM(chain[0], certificateChainFile);
return;
}
// For some reason the method from CertificateUtils distinguishes the first certificate and the rest of the chain
X509Certificate[] restOfTheChain = new X509Certificate[chain.length - 1];
System.arraycopy(chain, 1, restOfTheChain, 0, chain.length - 1);
CertificateUtils.writeCertificateToPEM(chain[0], certificateChainFile, restOfTheChain);
}
View on GitHub (pinned to e1c734241f)
Solutions
- Null/empty-check getCertificateChain() before writing; if empty, treat the order as failed and re-run lets-encrypt issue/renew.
- Inspect the ACME order state (pending/invalid/valid) — wait for pending orders or fix authorization failures (DNS/HTTP-01 reachability) before retrying.
- Test with the Let's Encrypt staging URL to rule out a custom-server incompatibility.
- Ensure the domain in the CLI command resolves and is reachable over HTTP 80 so the HTTP-01 challenge can succeed and a chain is actually issued.
Example fix
// before
X509Certificate[] chain = certChainAndPrivateKey.getCertificateChain();
LetsEncryptHelpers.writePrivateKeyAndCertificateChainsAsPem(key, chain, keyPem, chainPem);
// after
X509Certificate[] chain = certChainAndPrivateKey.getCertificateChain();
if (chain == null || chain.length == 0) {
throw new IllegalStateException("No certificate chain returned by ACME; order likely failed");
}
LetsEncryptHelpers.writePrivateKeyAndCertificateChainsAsPem(key, chain, keyPem, chainPem); Defensive patterns
Strategy: validation
Validate before calling
X509Certificate[] chain = certChainAndPrivateKey.getCertificateChain();
if (chain == null || chain.length == 0) {
throw new IllegalStateException("ACME returned no certificate chain; order failed or pending");
}
LetsEncryptHelpers.writePrivateKeyAndCertificateChainsAsPem(pk, chain, keyPem, chainPem); Type guard
static boolean hasCertificateChain(X509CertificateChainAndSigningKey r) {
return r != null && r.getCertificateChain() != null && r.getCertificateChain().length > 0;
} Try / catch
try {
LetsEncryptHelpers.writePrivateKeyAndCertificateChainsAsPem(pk, chain, keyPem, chainPem);
} catch (IllegalArgumentException e) {
if (e.getMessage() != null && e.getMessage().contains("certificate chain cannot be null")) {
// no chain issued: check ACME order state / authorization, then retry issuance
} else {
throw e;
}
} Prevention
- Confirm the HTTP-01 challenge succeeded (domain reachable on port 80) before expecting a chain.
- Check ACME order status; retry only after it is valid, and wait out pending orders.
- Null/length-check getCertificateChain() at every call site of the helper.
- Validate issuance end-to-end against the staging ACME URL before production.
When it happens
Trigger: Calling writePrivateKeyAndCertificateChainsAsPem(pk, null, ...) or with new X509Certificate[0], or via issueCertificate when obtainCertificateChain's X509CertificateChainAndSigningKey.getCertificateChain() is null/empty (failed ACME authorization, order still pending/invalid, custom server bug).
Common situations: Renewing while the previous order is still pending at the CA; domain authorization failed so the CA returned no chain; tests/tools calling the helper with placeholder data; a custom ACME directory that returns a malformed finalize response.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- The private key cannot be null
- Failed to clear challenge content in the Quarkus management
- Failed to notify the Quarkus management endpoint
- Extension passed an invalid shutdown handler
- Extension passed an invalid last shutdown handler
AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05).
Data as JSON: /api/errors/b1182604c144ad77.
Report an issue: GitHub.