Skip to main content

acme_proxy/
error.rs

1use std::borrow::Cow;
2
3use axum::{
4    Json,
5    http::{StatusCode, header},
6    response::{IntoResponse, Response},
7};
8use serde_json::{Value, json};
9
10/// An ACME error, rendered as an RFC 8555 §6.7 `application/problem+json`
11/// document:
12///
13/// ```json
14/// { "type": "urn:ietf:params:acme:error:malformed", "detail": "…", "status": 400 }
15/// ```
16///
17/// This struct represents ACME protocol errors that are returned to clients
18/// in the standardized RFC 8555 problem+json format. It implements the `IntoResponse`
19/// trait to convert errors into proper HTTP responses.
20///
21/// ## ACME Protocol Compliance
22///
23/// Following RFC 8555, each error has:
24/// - A `type` field with URN format identifying the error type
25/// - A `detail` field with human-readable description
26/// - A `status` field with HTTP status code
27///
28/// ## Error Types
29///
30/// - `malformed`: Request format issues (HTTP 400)
31/// - `bad_nonce`: Invalid or expired nonce (HTTP 400)
32/// - `unauthorized`: Signature verification failures (HTTP 401)
33/// - `server_internal`: Internal server errors (HTTP 500)
34/// - `account_does_not_exist`: Referenced account not found (HTTP 400)
35/// - `order_not_ready`: Finalize attempted on a non-`ready` order (HTTP 403)
36/// - `bad_csr`: Unacceptable finalize CSR (HTTP 400)
37/// - `unsupported_identifier`: Unsupported newOrder identifier type (HTTP 400)
38/// - `rejected_identifier`: Identifier refused by server policy (HTTP 403)
39/// - `access_denied`: Request blocked by a filter (HTTP 403)
40/// - `external_account_required`: newAccount missing a required EAB (HTTP 400)
41/// - `already_revoked`: Certificate already revoked (HTTP 400)
42/// - `bad_revocation_reason`: Unsupported `CRLReason` code (HTTP 400)
43/// - `key_change_conflict`: keyChange new key belongs to another account (HTTP 409)
44/// - `unsupported_media_type`: body was not `application/jose+json` (HTTP 415)
45/// - `method_not_allowed`: resource read with the wrong method, e.g. a bare GET (HTTP 405)
46/// - `not_found`: nothing routed at this path (HTTP 404)
47///
48/// ## Usage
49///
50/// Used both as the `AcmeRequest` extractor's rejection and as the error arm of
51/// handler results, so every failure reaches the client in the shape ACME
52/// clients expect.
53///
54/// ## Owned details
55///
56/// `detail` is a [`Cow<'static, str>`] rather than a `&'static str`: most
57/// call sites pass a literal (borrowed, no allocation), but the `filter`
58/// subsystem needs to name the offending value — "identifier evil.example.com
59/// is denied by policy" — which can only be built at runtime.
60#[derive(Debug)]
61pub struct Problem {
62    status: StatusCode,
63    /// The `urn:ietf:params:acme:error:*` problem type.
64    typ: &'static str,
65    detail: Cow<'static, str>,
66    /// The optional members, allocated only when one is actually used.
67    ///
68    /// Boxed because `Problem` is the `Err` of nearly every function in this
69    /// codebase, so its size is paid on every call — and all three of these are
70    /// empty for the great majority of problems, which are about the request
71    /// rather than about a list of identifiers. Inline they push the struct past
72    /// clippy's `result_large_err` threshold; behind an `Option<Box<_>>` the
73    /// common path costs one pointer and no allocation.
74    ext: Option<Box<ProblemExtensions>>,
75}
76
77/// The parts of an RFC 8555 problem document beyond RFC 7807's three fields.
78#[derive(Debug, Default)]
79struct ProblemExtensions {
80    /// The identifier this problem is about (RFC 8555 §9.7.7), set only on a
81    /// *subproblem*: §6.7.1 says the field "MUST NOT be present at the top
82    /// level in ACME problem documents. It can only be present in subproblems."
83    /// [`Problem::to_value`] enforces that by rendering it nowhere else.
84    identifier: Option<Value>,
85    /// Per-identifier failures (RFC 8555 §6.7.1).
86    subproblems: Vec<Problem>,
87    /// Type-specific members some error types carry — today only
88    /// `badSignatureAlgorithm`'s `algorithms` (RFC 8555 §6.2).
89    extra: serde_json::Map<String, Value>,
90}
91
92/// Declares the problem constructors that are nothing but a status, a type URN
93/// and the caller's detail — which is 27 of the 29.
94///
95/// They were written out longhand, six lines each, and the repetition was the
96/// point of failure: a wrong `StatusCode` or a typo'd URN in one of twenty-odd
97/// near-identical bodies reads as correct code. Here each is one line against
98/// its own doc comment, and the shared body exists once.
99///
100/// The two that are absent stay hand-written because they are not this shape:
101/// [`Problem::compound`] takes its status from the caller, and
102/// [`Problem::bad_signature_algorithm`] carries the `algorithms` member RFC 8555
103/// §6.2 requires.
104macro_rules! problems {
105    ($(
106        $(#[$doc:meta])*
107        $name:ident => ($status:ident, $urn:literal);
108    )*) => {
109        impl Problem {
110            $(
111                $(#[$doc])*
112                pub fn $name(detail: impl Into<Cow<'static, str>>) -> Self {
113                    Self::build(StatusCode::$status, $urn, detail)
114                }
115            )*
116        }
117    };
118}
119
120problems! {
121    /// The request was unacceptable for some reason (bad JSON, base64, JWS
122    /// shape, unexpected payload, wrong algorithm…). HTTP 400.
123    malformed => (BAD_REQUEST, "urn:ietf:params:acme:error:malformed");
124
125    /// The client sent an unacceptable anti-replay nonce (unknown or expired).
126    /// The client should retry with a fresh nonce. HTTP 400.
127    bad_nonce => (BAD_REQUEST, "urn:ietf:params:acme:error:badNonce");
128
129    /// The client lacks authorization to perform the request — here, a JWS
130    /// signature that fails verification. HTTP 401.
131    unauthorized => (UNAUTHORIZED, "urn:ietf:params:acme:error:unauthorized");
132
133    /// The server experienced an internal failure (e.g. the database was
134    /// unreachable). HTTP 500.
135    server_internal => (INTERNAL_SERVER_ERROR, "urn:ietf:params:acme:error:serverInternal");
136
137    /// The request referenced an account that does not exist. HTTP 400.
138    account_does_not_exist => (BAD_REQUEST, "urn:ietf:params:acme:error:accountDoesNotExist");
139
140    /// A finalize was attempted on an order that is not in the `ready` state
141    /// (RFC 8555 §7.4). HTTP 403.
142    order_not_ready => (FORBIDDEN, "urn:ietf:params:acme:error:orderNotReady");
143
144    /// The CSR in a finalize request was unacceptable (unparsable, its
145    /// identifiers do not match the order, or a filter refused one of the
146    /// names it requests). HTTP 400.
147    bad_csr => (BAD_REQUEST, "urn:ietf:params:acme:error:badCSR");
148
149    /// A newOrder identifier used a type the server does not support (only
150    /// `dns` is supported here). HTTP 400.
151    unsupported_identifier => (BAD_REQUEST, "urn:ietf:params:acme:error:unsupportedIdentifier");
152
153    /// The server will not issue for an identifier it otherwise supports,
154    /// because policy refuses it (RFC 8555 §6.7). Raised by the `filter`
155    /// subsystem at newOrder. HTTP 403.
156    rejected_identifier => (FORBIDDEN, "urn:ietf:params:acme:error:rejectedIdentifier");
157
158    /// The request was blocked by a connection-level filter (IP allowlist,
159    /// reverse DNS…), or a challenge responder answered with something that is
160    /// not the key authorization. HTTP 403.
161    ///
162    /// RFC 8555 has no dedicated "blocked by policy" code, so this reuses the
163    /// `unauthorized` type — but with 403 rather than the 401 that
164    /// [`Problem::unauthorized`] returns for a failed signature check, since
165    /// no credential could make this request succeed. RFC 8555 §8.3's own
166    /// example uses the same type for an `http-01` body mismatch.
167    access_denied => (FORBIDDEN, "urn:ietf:params:acme:error:unauthorized");
168
169    /// The server could not reach the client's validation target — TCP refused,
170    /// no route, a redirect chain that never terminated. HTTP 400.
171    ///
172    /// One of the four challenge-validation error types of RFC 8555 §6.7. The
173    /// client can fix the situation and retry with a new order.
174    connection => (BAD_REQUEST, "urn:ietf:params:acme:error:connection");
175
176    /// A DNS query the server needed failed or returned nothing (RFC 8555 §6.7).
177    /// HTTP 400.
178    ///
179    /// Distinct from [`Problem::incorrect_response`]: this says the lookup did
180    /// not produce an answer, not that the answer was wrong.
181    dns => (BAD_REQUEST, "urn:ietf:params:acme:error:dns");
182
183    /// The validation target answered, but not with what the challenge requires
184    /// — a TXT record that does not match, a certificate without the expected
185    /// `acmeIdentifier` extension (RFC 8555 §6.7). HTTP 403.
186    incorrect_response => (FORBIDDEN, "urn:ietf:params:acme:error:incorrectResponse");
187
188    /// A TLS-level failure while validating a `tls-alpn-01` challenge — no ALPN
189    /// negotiated, a handshake alert, no certificate presented (RFC 8555 §6.7).
190    /// HTTP 400.
191    tls => (BAD_REQUEST, "urn:ietf:params:acme:error:tls");
192
193    /// The server requires External Account Binding (RFC 8555 §6.7 / §7.3.4)
194    /// but the request did not include one. HTTP 400.
195    external_account_required => (BAD_REQUEST, "urn:ietf:params:acme:error:externalAccountRequired");
196
197    /// The certificate identified by a `POST /revokeCert` request has already
198    /// been revoked (RFC 8555 §7.6). HTTP 400.
199    already_revoked => (BAD_REQUEST, "urn:ietf:params:acme:error:alreadyRevoked");
200
201    /// The `reason` a `POST /revokeCert` request gave is not one of the
202    /// `CRLReason` codes RFC 8555 §7.6 permits (RFC 5280 §5.3.1, excluding the
203    /// reserved code `7`). HTTP 400.
204    bad_revocation_reason => (BAD_REQUEST, "urn:ietf:params:acme:error:badRevocationReason");
205
206    /// The new key in a `keyChange` (RFC 8555 §7.3.5) request is already
207    /// associated with a different account. HTTP 409.
208    ///
209    /// RFC 8555 defines no dedicated error type for this case, so this
210    /// reuses the `malformed` urn — mirroring how [`Problem::access_denied`]
211    /// already reuses `unauthorized`'s urn under a different status. Unlike
212    /// every other `Problem`, the caller also attaches a `Location` header
213    /// naming the conflicting account (RFC 8555 §7.3.5), which requires
214    /// building the `Response` by hand rather than through `IntoResponse`
215    /// (see `to_value`).
216    key_change_conflict => (CONFLICT, "urn:ietf:params:acme:error:malformed");
217
218    /// The request body did not carry `Content-Type: application/jose+json`.
219    /// HTTP 415.
220    ///
221    /// RFC 8555 §6.2 makes the media type mandatory and names the status
222    /// itself: "If a request does not meet this requirement, then the server
223    /// MUST return a response with status code 415 (Unsupported Media Type)".
224    /// It defines no error *type* for the case, so this reuses `malformed`'s
225    /// urn under a different status — the same pattern as
226    /// [`Problem::access_denied`] and [`Problem::key_change_conflict`].
227    unsupported_media_type => (UNSUPPORTED_MEDIA_TYPE, "urn:ietf:params:acme:error:malformed");
228
229    /// The request body was larger than `server.max_body_bytes`. HTTP 413.
230    ///
231    /// Distinct from `malformed` on purpose: the body was never read, so
232    /// nothing is known about whether it was a well-formed JWS, and telling a
233    /// client its JWS is malformed would send it rebuilding the one thing that
234    /// is not the problem. RFC 8555 defines no type for this, so it reuses
235    /// `malformed`'s urn under its own status — the same pattern as
236    /// [`Problem::unsupported_media_type`].
237    payload_too_large => (PAYLOAD_TOO_LARGE, "urn:ietf:params:acme:error:malformed");
238
239    /// The server is at capacity and refused the request without doing any of
240    /// the work. HTTP 503.
241    ///
242    /// Deliberately *not* `rateLimited`/429: that type says "you asked too
243    /// often", which is a statement about the client, and here the client may
244    /// have made its first request of the day. RFC 8555 defines no type for
245    /// server-side saturation, so this reuses `serverInternal`'s urn under a
246    /// different status — the same pattern as [`Problem::access_denied`],
247    /// [`Problem::key_change_conflict`] and [`Problem::unsupported_media_type`].
248    ///
249    /// The caller attaches `Retry-After`; every ACME client already understands
250    /// it from the rate-limit case.
251    service_unavailable => (SERVICE_UNAVAILABLE, "urn:ietf:params:acme:error:serverInternal");
252
253    /// The resource exists but not for this HTTP method — in practice, a bare
254    /// `GET` of a resource that RFC 8555 §6.3 requires be read with
255    /// POST-as-GET. HTTP 405.
256    ///
257    /// §6.3 pins both halves: "if the server receives a GET request, it MUST
258    /// return an error with status code 405 (Method Not Allowed) and type
259    /// `malformed`". axum's own method-not-allowed response carries the right
260    /// status but an empty body, so this supplies the problem document.
261    method_not_allowed => (METHOD_NOT_ALLOWED, "urn:ietf:params:acme:error:malformed");
262
263    /// A newOrder named a predecessor certificate that another order already
264    /// claims to replace (RFC 9773 §7.4). HTTP 409.
265    ///
266    /// The one status RFC 9773 pins by name: §5 says the server "MUST return an
267    /// HTTP 409 (Conflict) with a problem document of type `alreadyReplaced`" —
268    /// unlike the other §5 checks, which only say "SHOULD reject".
269    already_replaced => (CONFLICT, "urn:ietf:params:acme:error:alreadyReplaced");
270
271    /// A `contact` URL used a scheme this server does not support
272    /// (RFC 8555 §7.3). HTTP 400.
273    unsupported_contact => (BAD_REQUEST, "urn:ietf:params:acme:error:unsupportedContact");
274
275    /// A `contact` URL was of a supported scheme but not usable — a `mailto:`
276    /// carrying `hfields` or more than one address (RFC 8555 §7.3). HTTP 400.
277    invalid_contact => (BAD_REQUEST, "urn:ietf:params:acme:error:invalidContact");
278
279    /// The client must take an out-of-band action before the request can
280    /// succeed — here, agreeing to the terms of service (RFC 8555 §7.3.3).
281    /// HTTP 403.
282    ///
283    /// The caller attaches a `Link: <tos-url>;rel="terms-of-service"` header,
284    /// as §6.7 requires for this type.
285    user_action_required => (FORBIDDEN, "urn:ietf:params:acme:error:userActionRequired");
286
287    /// No resource is routed at the requested path. HTTP 404.
288    ///
289    /// RFC 8555 defines no type for this either; `malformed` keeps an unknown
290    /// path answering in the `application/problem+json` shape every other
291    /// failure uses, rather than axum's empty-bodied default.
292    not_found => (NOT_FOUND, "urn:ietf:params:acme:error:malformed");
293}
294
295impl Problem {
296    /// The shared constructor every named one funnels through, so a new field
297    /// on the struct does not have to be threaded through twenty-odd literals.
298    fn build(status: StatusCode, typ: &'static str, detail: impl Into<Cow<'static, str>>) -> Self {
299        Self {
300            status,
301            typ,
302            detail: detail.into(),
303            ext: None,
304        }
305    }
306
307    /// The extensions block, created on first use.
308    fn ext_mut(&mut self) -> &mut ProblemExtensions {
309        self.ext.get_or_insert_with(Box::default)
310    }
311    /// Several errors at once, each attributed to its own identifier
312    /// (RFC 8555 §6.7.1). Pair with [`Problem::with_subproblems`].
313    ///
314    /// The status is the caller's to choose, since §6.7.1 puts no constraint on
315    /// it and a compound of rejections (403) reads differently from a compound
316    /// of malformed names (400).
317    pub fn compound(status: StatusCode, detail: impl Into<Cow<'static, str>>) -> Self {
318        Self::build(status, "urn:ietf:params:acme:error:compound", detail)
319    }
320
321    /// The JWS was signed with an algorithm this server does not support
322    /// (RFC 8555 §6.2). HTTP 400.
323    ///
324    /// §6.2 requires the response to carry the supported list: "an
325    /// `algorithms` field […] listing the JWS algorithms the server supports",
326    /// so the client can retry with one instead of guessing. Attached here
327    /// rather than left to the caller, since the list is a property of this
328    /// server's verifier, not of the call site.
329    pub fn bad_signature_algorithm(detail: impl Into<Cow<'static, str>>) -> Self {
330        Self::build(
331            StatusCode::BAD_REQUEST,
332            "urn:ietf:params:acme:error:badSignatureAlgorithm",
333            detail,
334        )
335        .with_extra("algorithms", json!(["ES256", "RS256"]))
336    }
337}
338
339impl Problem {
340    /// The HTTP status this problem renders as.
341    ///
342    /// Exposed so a caller assembling a `compound` (RFC 8555 §6.7.1) can pick a
343    /// status for the wrapper from the parts it is wrapping.
344    #[must_use]
345    pub fn status(&self) -> StatusCode {
346        self.status
347    }
348
349    /// Attaches the identifier this problem is about (RFC 8555 §9.7.7).
350    ///
351    /// Only meaningful on a problem destined to become a *subproblem*: §6.7.1
352    /// forbids the field at the top level, and [`Problem::to_value`] drops it
353    /// there, so calling this on a problem that is then returned directly is a
354    /// no-op rather than a violation.
355    #[must_use]
356    pub fn with_identifier(mut self, identifier: &crate::sqlite::order::Identifier) -> Self {
357        self.ext_mut().identifier = serde_json::to_value(identifier).ok();
358        self
359    }
360
361    /// Attaches per-identifier failures (RFC 8555 §6.7.1).
362    ///
363    /// §6.7.1: "Subproblems need not all have the same type, and they do not
364    /// need to match the top level type."
365    #[must_use]
366    pub fn with_subproblems(mut self, subproblems: Vec<Problem>) -> Self {
367        self.ext_mut().subproblems = subproblems;
368        self
369    }
370
371    /// Attaches a type-specific member, e.g. `badSignatureAlgorithm`'s
372    /// `algorithms` list (RFC 8555 §6.2).
373    #[must_use]
374    pub fn with_extra(mut self, key: &str, value: Value) -> Self {
375        self.ext_mut().extra.insert(key.to_string(), value);
376        self
377    }
378
379    /// The RFC 8555 problem document as a JSON value (`{type, detail, status}`,
380    /// plus `subproblems` and any type-specific members when present).
381    ///
382    /// Shared by [`IntoResponse`] (the response body) and callers that need to
383    /// *persist* the same document — e.g. an order's `error` field on a failed
384    /// finalize renders exactly what the client is told.
385    ///
386    /// `identifier` is deliberately absent: §6.7.1 makes it a subproblem-only
387    /// field, and [`Problem::to_subproblem_value`] is where it appears.
388    #[must_use]
389    pub fn to_value(&self) -> Value {
390        let mut object = serde_json::Map::new();
391        object.insert("type".to_string(), Value::String(self.typ.to_string()));
392        object.insert("detail".to_string(), json!(self.detail));
393        object.insert("status".to_string(), json!(self.status.as_u16()));
394
395        if let Some(ext) = &self.ext {
396            for (key, value) in &ext.extra {
397                object.insert(key.clone(), value.clone());
398            }
399
400            if !ext.subproblems.is_empty() {
401                object.insert(
402                    "subproblems".to_string(),
403                    Value::Array(
404                        ext.subproblems
405                            .iter()
406                            .map(Problem::to_subproblem_value)
407                            .collect(),
408                    ),
409                );
410            }
411        }
412
413        Value::Object(object)
414    }
415
416    /// This problem as it appears *inside* another's `subproblems` array
417    /// (RFC 8555 §6.7.1): `type` and `detail`, plus the `identifier` it is
418    /// about. No `status` — the HTTP status belongs to the response, and the
419    /// RFC's own example omits it here.
420    #[must_use]
421    fn to_subproblem_value(&self) -> Value {
422        let mut object = serde_json::Map::new();
423        object.insert("type".to_string(), Value::String(self.typ.to_string()));
424        object.insert("detail".to_string(), json!(self.detail));
425        if let Some(identifier) = self.ext.as_ref().and_then(|ext| ext.identifier.as_ref()) {
426            object.insert("identifier".to_string(), identifier.clone());
427        }
428        Value::Object(object)
429    }
430}
431
432impl IntoResponse for Problem {
433    fn into_response(self) -> Response {
434        let body = Json(self.to_value());
435
436        (
437            self.status,
438            [(header::CONTENT_TYPE, "application/problem+json")],
439            body,
440        )
441            .into_response()
442    }
443}
444
445#[cfg(test)]
446mod tests {
447    use super::*;
448    use http_body_util::BodyExt;
449
450    /// Renders a `Problem` and asserts its status, `content-type`, and the three
451    /// JSON fields of the RFC 8555 problem document.
452    async fn assert_problem(problem: Problem, expected_status: u16, expected_type: &str) {
453        let response = problem.into_response();
454
455        assert_eq!(response.status().as_u16(), expected_status);
456        assert_eq!(
457            response
458                .headers()
459                .get(header::CONTENT_TYPE)
460                .and_then(|v| v.to_str().ok()),
461            Some("application/problem+json"),
462        );
463
464        let bytes = response.into_body().collect().await.unwrap().to_bytes();
465        let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
466        assert_eq!(json["type"], expected_type);
467        assert_eq!(json["status"], expected_status);
468        assert_eq!(json["detail"], "boom");
469    }
470
471    #[tokio::test]
472    async fn malformed_renders_400_problem_json() {
473        assert_problem(
474            Problem::malformed("boom"),
475            400,
476            "urn:ietf:params:acme:error:malformed",
477        )
478        .await;
479    }
480
481    #[tokio::test]
482    async fn bad_nonce_renders_400_problem_json() {
483        assert_problem(
484            Problem::bad_nonce("boom"),
485            400,
486            "urn:ietf:params:acme:error:badNonce",
487        )
488        .await;
489    }
490
491    #[tokio::test]
492    async fn unauthorized_renders_401_problem_json() {
493        assert_problem(
494            Problem::unauthorized("boom"),
495            401,
496            "urn:ietf:params:acme:error:unauthorized",
497        )
498        .await;
499    }
500
501    #[tokio::test]
502    async fn server_internal_renders_500_problem_json() {
503        assert_problem(
504            Problem::server_internal("boom"),
505            500,
506            "urn:ietf:params:acme:error:serverInternal",
507        )
508        .await;
509    }
510
511    #[tokio::test]
512    async fn account_does_not_exist_renders_400_problem_json() {
513        assert_problem(
514            Problem::account_does_not_exist("boom"),
515            400,
516            "urn:ietf:params:acme:error:accountDoesNotExist",
517        )
518        .await;
519    }
520
521    #[tokio::test]
522    async fn order_not_ready_renders_403_problem_json() {
523        assert_problem(
524            Problem::order_not_ready("boom"),
525            403,
526            "urn:ietf:params:acme:error:orderNotReady",
527        )
528        .await;
529    }
530
531    #[tokio::test]
532    async fn bad_csr_renders_400_problem_json() {
533        assert_problem(
534            Problem::bad_csr("boom"),
535            400,
536            "urn:ietf:params:acme:error:badCSR",
537        )
538        .await;
539    }
540
541    #[tokio::test]
542    async fn unsupported_identifier_renders_400_problem_json() {
543        assert_problem(
544            Problem::unsupported_identifier("boom"),
545            400,
546            "urn:ietf:params:acme:error:unsupportedIdentifier",
547        )
548        .await;
549    }
550
551    #[tokio::test]
552    async fn rejected_identifier_renders_403_problem_json() {
553        assert_problem(
554            Problem::rejected_identifier("boom"),
555            403,
556            "urn:ietf:params:acme:error:rejectedIdentifier",
557        )
558        .await;
559    }
560
561    #[tokio::test]
562    async fn access_denied_renders_403_problem_json() {
563        assert_problem(
564            Problem::access_denied("boom"),
565            403,
566            "urn:ietf:params:acme:error:unauthorized",
567        )
568        .await;
569    }
570
571    #[tokio::test]
572    async fn connection_renders_400_problem_json() {
573        assert_problem(
574            Problem::connection("boom"),
575            400,
576            "urn:ietf:params:acme:error:connection",
577        )
578        .await;
579    }
580
581    #[tokio::test]
582    async fn dns_renders_400_problem_json() {
583        assert_problem(Problem::dns("boom"), 400, "urn:ietf:params:acme:error:dns").await;
584    }
585
586    #[tokio::test]
587    async fn incorrect_response_renders_403_problem_json() {
588        assert_problem(
589            Problem::incorrect_response("boom"),
590            403,
591            "urn:ietf:params:acme:error:incorrectResponse",
592        )
593        .await;
594    }
595
596    #[tokio::test]
597    async fn tls_renders_400_problem_json() {
598        assert_problem(Problem::tls("boom"), 400, "urn:ietf:params:acme:error:tls").await;
599    }
600
601    #[tokio::test]
602    async fn external_account_required_renders_400_problem_json() {
603        assert_problem(
604            Problem::external_account_required("boom"),
605            400,
606            "urn:ietf:params:acme:error:externalAccountRequired",
607        )
608        .await;
609    }
610
611    #[tokio::test]
612    async fn already_revoked_renders_400_problem_json() {
613        assert_problem(
614            Problem::already_revoked("boom"),
615            400,
616            "urn:ietf:params:acme:error:alreadyRevoked",
617        )
618        .await;
619    }
620
621    #[tokio::test]
622    async fn bad_revocation_reason_renders_400_problem_json() {
623        assert_problem(
624            Problem::bad_revocation_reason("boom"),
625            400,
626            "urn:ietf:params:acme:error:badRevocationReason",
627        )
628        .await;
629    }
630
631    #[tokio::test]
632    async fn key_change_conflict_renders_409_problem_json() {
633        assert_problem(
634            Problem::key_change_conflict("boom"),
635            409,
636            "urn:ietf:params:acme:error:malformed",
637        )
638        .await;
639    }
640
641    #[test]
642    fn to_value_matches_rendered_body() {
643        let value = Problem::server_internal("boom").to_value();
644        assert_eq!(value["type"], "urn:ietf:params:acme:error:serverInternal");
645        assert_eq!(value["detail"], "boom");
646        assert_eq!(value["status"], 500);
647    }
648
649    /// A `String` detail (what the filters build) renders like a literal one.
650    #[test]
651    fn owned_detail_is_accepted_and_rendered() {
652        let name = "evil.example.com";
653        let value = Problem::rejected_identifier(format!("identifier {name} is denied")).to_value();
654        assert_eq!(value["detail"], "identifier evil.example.com is denied");
655    }
656}