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}