Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Error Handling

All fallible operations in Akāmu return Result<T, AcmeError>. The AcmeError type is defined in src/error.rs and implements both std::error::Error (via thiserror) and axum’s IntoResponse.

AcmeError taxonomy

ACME-specific errors

These map to ACME problem type URNs (urn:ietf:params:acme:error:*):

VariantACME typeHTTP status
BadNoncebadNonce400
BadSignatureAlgorithm(String)badSignatureAlgorithm400
Unauthorized(String)unauthorized401
AccountDoesNotExistaccountDoesNotExist400
AccountAlreadyExistsaccountAlreadyExists409
InvalidContact(String)invalidContact400
UnsupportedContactunsupportedContact400
UserActionRequired(String)userActionRequired403
RejectedIdentifier(String)rejectedIdentifier400
UnsupportedIdentifier(String)unsupportedIdentifier400
OrderNotReadyorderNotReady403
CertAlreadyReplacedalreadyReplaced409
BadCsr(String)badCSR400
BadRevocationReasonbadRevocationReason400
AlreadyRevokedalreadyRevoked400
Caa(String)caa403
ExternalAccountRequiredexternalAccountRequired403
Connection(String)connection400
Dns(String)dns400
IncorrectResponse(String)incorrectResponse400
Tls(String)tls400
AutoRenewalCanceledautoRenewalCanceled403
AutoRenewalCancellationInvalidautoRenewalCancellationInvalid400
AutoRenewalRevocationNotSupportedautoRenewalRevocationNotSupported403
InvalidProfile(String)invalidProfile400

Generic HTTP-mapped errors

These do not have dedicated ACME error types and carry the appropriate HTTP status. NotFound maps to the malformed ACME type; all others fall through to serverInternal.

VariantACME typeHTTP status
NotFoundmalformed404
MethodNotAllowedserverInternal405
Conflict(String)serverInternal409
UnsupportedMediaTypeserverInternal415
PayloadTooLargeserverInternal413
BadRequest(String)serverInternal400
ServiceUnavailable(String)serverInternal503

Internal errors

These indicate server-side failures. They map to serverInternal in the ACME error type and HTTP 500:

VariantMeaning
Database(String)SQLite error
Crypto(String)Cryptographic operation failure
Builder(String)Certificate or CRL builder error
Mtc(String)MTC log operation failure
Config(String)Configuration or startup error
Internal(String)General internal error

Response format

AcmeError::into_response builds a response with:

  • HTTP status from http_status().
  • Content-Type: application/problem+json (RFC 7807).
  • JSON body:
{
  "type": "urn:ietf:params:acme:error:badNonce",
  "status": 400,
  "detail": "bad nonce"
}

For responses with an HTTP 4xx status, the detail field is the Display string of the variant, which for parameterised variants includes the inner string.

For responses with an HTTP 5xx status (server errors), the detail field is always the fixed string "internal server error", regardless of the underlying cause. The actual error is logged server-side at ERROR level but is never included in the response body. This applies to ServiceUnavailable (503) as well as the 500-class internal errors.

From implementations

Two From implementations allow the ? operator to be used with library errors:

  • From<sqlx::Error> for AcmeError → wraps in AcmeError::Database.
  • From<akamu_jose::JoseError> for AcmeError → maps to AcmeError::BadRequest, AcmeError::Crypto, or AcmeError::BadSignatureAlgorithm depending on the JOSE error kind.

This means any sqlx::query!(...).fetch_one(&db).await? will automatically convert database errors to AcmeError::Database(msg), and any JOSE verification failure will convert to the appropriate ACME error without an explicit match.

Error propagation in handlers

Handlers return Result<Response, AcmeError>. axum automatically calls IntoResponse::into_response on the error variant when building the HTTP response.

Example handler error propagation chain:

db::accounts::get_by_id(&db, &id).await?
  ↓ sqlx::Error
  ↓ From<sqlx::Error> for AcmeError
  ↓ AcmeError::Database("...")
  ↓ returned from handler as Err(AcmeError::Database(...))
  ↓ axum calls AcmeError::into_response()
  ↓ HTTP 500 with application/problem+json body
  ↓ detail field = "internal server error" (not the database message)

Error handling in background tasks

Background validation tasks (tokio::spawn) must not panic and must not propagate errors. validation::validate_challenge is declared infallible: it calls on_valid or on_invalid internally and logs any database errors via tracing::warn!. Panics inside validation tasks are caught by the observer task pattern described in the Validation chapter.

Design principles

  • No unwrap() in production paths. All fallible operations use ? or explicit error handling.
  • Internal errors do not leak details to clients. AcmeError::Internal, AcmeError::Database, AcmeError::Crypto, AcmeError::Builder, AcmeError::Mtc, AcmeError::Config, and AcmeError::ServiceUnavailable all produce 5xx responses whose detail field is the fixed string "internal server error". The actual error message is written to the server log only.
  • Challenge errors are ACME errors. The validation layer converts hyper, hickory-resolver, and rustls errors into specific AcmeError variants (Connection, Dns, Tls, IncorrectResponse) so the client receives a meaningful ACME error type.