Skip to main content

rust_web_server/error/
problem.rs

1//! RFC 9457 problem details for the errors rws itself produces (18.17.0):
2//! `application/problem+json` bodies such as
3//! `{"type":"about:blank","title":"Bad Gateway","status":502,"detail":"…"}`.
4//! `RWS_CONFIG_ERROR_FORMAT=text` keeps the plain-text bodies of earlier
5//! releases (`502 Bad Gateway`, `413 Payload Too Large: …`).
6
7use crate::response::{Response, StatusCodeReasonPhrase};
8
9/// `Content-Type` of a problem details body.
10pub const PROBLEM_JSON: &str = "application/problem+json";
11
12/// Whether rws-made errors use plain text (`error_format = "text"`).
13pub fn text_errors() -> bool {
14    crate::server_config::ServerConfig::current().error_format_text
15}
16
17/// An rws-made error: problem details, or with `error_format = "text"` the
18/// text `"<code> <reason>[: <detail>]"`.
19pub fn problem(status: &StatusCodeReasonPhrase, detail: &str) -> Response {
20    let text = if detail.is_empty() {
21        format!("{} {}", status.status_code, status.reason_phrase)
22    } else {
23        format!("{} {}: {detail}", status.status_code, status.reason_phrase)
24    };
25    problem_with_text(status, detail, &text)
26}
27
28/// [`problem`] whose text form is `text` rather than the status line.
29pub fn problem_with_text(status: &StatusCodeReasonPhrase, detail: &str, text: &str) -> Response {
30    with_headers(status, detail, text, None)
31}
32
33pub(crate) fn with_headers(status: &StatusCodeReasonPhrase, detail: &str, text: &str, headers: Option<Vec<crate::header::Header>>) -> Response {
34    let as_text = text_errors();
35    if as_text && text.is_empty() {
36        return Response::get_response(status, headers, None);
37    }
38    let (body, content_type) = body(status, detail, text, as_text);
39    let content_range = crate::range::Range::get_content_range(body, content_type.to_string());
40    Response::get_response(status, headers, Some(vec![content_range]))
41}
42
43/// The body and its content type, for writers that build the response
44/// head themselves.
45pub(crate) fn body(status: &StatusCodeReasonPhrase, detail: &str, text: &str, as_text: bool) -> (Vec<u8>, &'static str) {
46    if as_text {
47        return (text.as_bytes().to_vec(), crate::mime_type::MimeType::TEXT_PLAIN);
48    }
49    let mut json = format!(r#"{{"type":"about:blank","title":{},"status":{}"#, quote(status.reason_phrase), status.status_code);
50    if !detail.is_empty() {
51        json.push_str(r#","detail":"#);
52        json.push_str(&quote(detail));
53    }
54    json.push('}');
55    (json.into_bytes(), PROBLEM_JSON)
56}
57
58/// The body for an error whose text form is `text` (`"413 Payload Too
59/// Large: …"`): the text, or problem details with the part after the status
60/// line as `detail`.
61pub(crate) fn from_text(status: &StatusCodeReasonPhrase, text: &str) -> (Vec<u8>, &'static str) {
62    let line = format!("{} {}", status.status_code, status.reason_phrase);
63    let detail = text.strip_prefix(&line).map(|rest| rest.trim_start_matches(':').trim()).unwrap_or(text);
64    body(status, detail, text, text_errors())
65}
66
67fn quote(s: &str) -> String {
68    let mut out = String::with_capacity(s.len() + 2);
69    out.push('"');
70    for c in s.chars() {
71        match c {
72            '"' => out.push_str("\\\""),
73            '\\' => out.push_str("\\\\"),
74            '\n' => out.push_str("\\n"),
75            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
76            c => out.push(c),
77        }
78    }
79    out.push('"');
80    out
81}