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:*):
| Variant | ACME type | HTTP status |
|---|---|---|
BadNonce | badNonce | 400 |
BadSignatureAlgorithm(String) | badSignatureAlgorithm | 400 |
Unauthorized(String) | unauthorized | 401 |
AccountDoesNotExist | accountDoesNotExist | 400 |
AccountAlreadyExists | accountAlreadyExists | 409 |
InvalidContact(String) | invalidContact | 400 |
UnsupportedContact | unsupportedContact | 400 |
UserActionRequired(String) | userActionRequired | 403 |
RejectedIdentifier(String) | rejectedIdentifier | 400 |
UnsupportedIdentifier(String) | unsupportedIdentifier | 400 |
OrderNotReady | orderNotReady | 403 |
CertAlreadyReplaced | alreadyReplaced | 409 |
BadCsr(String) | badCSR | 400 |
BadRevocationReason | badRevocationReason | 400 |
AlreadyRevoked | alreadyRevoked | 400 |
Caa(String) | caa | 403 |
ExternalAccountRequired | externalAccountRequired | 403 |
Connection(String) | connection | 400 |
Dns(String) | dns | 400 |
IncorrectResponse(String) | incorrectResponse | 400 |
Tls(String) | tls | 400 |
AutoRenewalCanceled | autoRenewalCanceled | 403 |
AutoRenewalCancellationInvalid | autoRenewalCancellationInvalid | 400 |
AutoRenewalRevocationNotSupported | autoRenewalRevocationNotSupported | 403 |
InvalidProfile(String) | invalidProfile | 400 |
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.
| Variant | ACME type | HTTP status |
|---|---|---|
NotFound | malformed | 404 |
MethodNotAllowed | serverInternal | 405 |
Conflict(String) | serverInternal | 409 |
UnsupportedMediaType | serverInternal | 415 |
PayloadTooLarge | serverInternal | 413 |
BadRequest(String) | serverInternal | 400 |
ServiceUnavailable(String) | serverInternal | 503 |
Internal errors
These indicate server-side failures. They map to serverInternal in the ACME error type and HTTP 500:
| Variant | Meaning |
|---|---|
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 inAcmeError::Database.From<akamu_jose::JoseError> for AcmeError→ maps toAcmeError::BadRequest,AcmeError::Crypto, orAcmeError::BadSignatureAlgorithmdepending 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, andAcmeError::ServiceUnavailableall produce 5xx responses whosedetailfield 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, andrustlserrors into specificAcmeErrorvariants (Connection,Dns,Tls,IncorrectResponse) so the client receives a meaningful ACME error type.