static_web_server/
error_page.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// This file is part of Static Web Server.
3// See https://static-web-server.net/ for more information
4// Copyright (C) 2019-present Jose Quintana <joseluisq.net>
5
6//! Error page module to compose an HTML page response.
7//!
8
9use headers::{AcceptRanges, ContentLength, ContentType, HeaderMapExt};
10use hyper::{Body, Method, Response, StatusCode, Uri};
11use maud::{DOCTYPE, html};
12use mime_guess::mime;
13use std::path::Path;
14
15use crate::{Result, helpers, http_ext::MethodExt};
16
17/// It returns a HTTP error response which also handles available `404` or `50x` HTML content.
18pub fn error_response(
19    uri: &Uri,
20    method: &Method,
21    status_code: &StatusCode,
22    page404: &Path,
23    page50x: &Path,
24) -> Result<Response<Body>> {
25    tracing::warn!(
26        method = ?method, uri = ?uri, status = status_code.as_u16(),
27        error = status_code.canonical_reason().unwrap_or_default()
28    );
29
30    // Check for 4xx/50x status codes and handle their corresponding HTML content
31    let mut page_content = String::new();
32    let status_code = match status_code {
33        // 4xx
34        &StatusCode::BAD_REQUEST
35        | &StatusCode::UNAUTHORIZED
36        | &StatusCode::PAYMENT_REQUIRED
37        | &StatusCode::FORBIDDEN
38        | &StatusCode::NOT_FOUND
39        | &StatusCode::METHOD_NOT_ALLOWED
40        | &StatusCode::NOT_ACCEPTABLE
41        | &StatusCode::PROXY_AUTHENTICATION_REQUIRED
42        | &StatusCode::REQUEST_TIMEOUT
43        | &StatusCode::CONFLICT
44        | &StatusCode::GONE
45        | &StatusCode::LENGTH_REQUIRED
46        | &StatusCode::PRECONDITION_FAILED
47        | &StatusCode::PAYLOAD_TOO_LARGE
48        | &StatusCode::URI_TOO_LONG
49        | &StatusCode::UNSUPPORTED_MEDIA_TYPE
50        | &StatusCode::RANGE_NOT_SATISFIABLE
51        | &StatusCode::EXPECTATION_FAILED => {
52            // Extra check for 404 status code and its HTML content
53            if status_code == &StatusCode::NOT_FOUND {
54                if page404.is_file() {
55                    String::from_utf8_lossy(&helpers::read_bytes_default(page404))
56                        .trim()
57                        .clone_into(&mut page_content);
58                } else {
59                    tracing::debug!(
60                        "page404 file path not found or not a regular file: {}",
61                        page404.display()
62                    );
63                }
64            }
65            status_code
66        }
67        // 50x
68        &StatusCode::INTERNAL_SERVER_ERROR
69        | &StatusCode::NOT_IMPLEMENTED
70        | &StatusCode::BAD_GATEWAY
71        | &StatusCode::SERVICE_UNAVAILABLE
72        | &StatusCode::GATEWAY_TIMEOUT
73        | &StatusCode::HTTP_VERSION_NOT_SUPPORTED
74        | &StatusCode::VARIANT_ALSO_NEGOTIATES
75        | &StatusCode::INSUFFICIENT_STORAGE
76        | &StatusCode::LOOP_DETECTED => {
77            // HTML content check for status codes 50x
78            if page50x.is_file() {
79                String::from_utf8_lossy(&helpers::read_bytes_default(page50x))
80                    .trim()
81                    .clone_into(&mut page_content);
82            } else {
83                tracing::debug!(
84                    "page50x file path not found or not a regular file: {}",
85                    page50x.display()
86                );
87            }
88            status_code
89        }
90        // other status codes
91        _ => status_code,
92    };
93
94    if page_content.is_empty() {
95        let reason = status_code.canonical_reason().unwrap_or_default();
96        let title = [status_code.as_str(), " ", reason].concat();
97
98        page_content = html! {
99            (DOCTYPE)
100            html {
101                head {
102                    meta charset="utf-8";
103                    meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1";
104                    title {
105                        (title)
106                    }
107                    style {
108                        "html { color-scheme: light dark; } body { font-family: sans-serif; text-align: center; }"
109                    }
110                }
111                body {
112                    h1 {
113                        (title)
114                    }
115                }
116            }
117        }.into();
118    }
119
120    let mut body = Body::empty();
121    let len = page_content.len() as u64;
122
123    if !method.is_head() {
124        body = Body::from(page_content)
125    }
126
127    let mut resp = Response::new(body);
128    *resp.status_mut() = *status_code;
129    resp.headers_mut()
130        .typed_insert(ContentType::from(mime::TEXT_HTML_UTF_8));
131    resp.headers_mut().typed_insert(ContentLength(len));
132    resp.headers_mut().typed_insert(AcceptRanges::bytes());
133
134    Ok(resp)
135}