Skip to main content

rskit_http/
status.rs

1use http::StatusCode;
2use rskit_errors::{AppError, ErrorCode};
3
4/// Framework-neutral HTTP request type.
5pub type HttpRequest<B = Vec<u8>> = http::Request<B>;
6
7/// Framework-neutral HTTP response type.
8pub type HttpResponse<B = Vec<u8>> = http::Response<B>;
9
10/// Framework-neutral HTTP header collection.
11pub type HttpHeaders = http::HeaderMap;
12
13/// Framework-neutral HTTP status code.
14pub type HttpStatusCode = http::StatusCode;
15
16/// Convert an HTTP status code into the canonical rskit error code.
17#[must_use]
18pub fn status_to_error_code(status: StatusCode) -> ErrorCode {
19    match status.as_u16() {
20        400 => ErrorCode::InvalidInput,
21        401 => ErrorCode::Unauthorized,
22        403 => ErrorCode::Forbidden,
23        404 => ErrorCode::NotFound,
24        409 => ErrorCode::Conflict,
25        429 => ErrorCode::RateLimited,
26        408 => ErrorCode::Cancelled,
27        500 => ErrorCode::Internal,
28        502 => ErrorCode::ExternalService,
29        503 => ErrorCode::ServiceUnavailable,
30        504 => ErrorCode::Timeout,
31        _ if status.is_client_error() || status.is_server_error() => ErrorCode::ExternalService,
32        _ => ErrorCode::Internal,
33    }
34}
35
36/// Return `true` when a status is in the successful 2xx range.
37#[must_use]
38pub fn is_success_status(status: StatusCode) -> bool {
39    status.is_success()
40}
41
42/// Return the HTTP status code carried by an [`AppError`].
43#[must_use]
44pub fn app_error_status(error: &AppError) -> StatusCode {
45    error.http_status()
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn maps_http_status_to_error_code() {
54        assert_eq!(
55            status_to_error_code(StatusCode::BAD_REQUEST),
56            ErrorCode::InvalidInput
57        );
58        assert_eq!(
59            status_to_error_code(StatusCode::NOT_FOUND),
60            ErrorCode::NotFound
61        );
62        assert_eq!(
63            status_to_error_code(StatusCode::TOO_MANY_REQUESTS),
64            ErrorCode::RateLimited
65        );
66        assert_eq!(
67            status_to_error_code(StatusCode::BAD_GATEWAY),
68            ErrorCode::ExternalService
69        );
70        assert_eq!(
71            status_to_error_code(StatusCode::SERVICE_UNAVAILABLE),
72            ErrorCode::ServiceUnavailable
73        );
74        assert_eq!(
75            status_to_error_code(StatusCode::GATEWAY_TIMEOUT),
76            ErrorCode::Timeout
77        );
78        assert_eq!(
79            status_to_error_code(StatusCode::INTERNAL_SERVER_ERROR),
80            ErrorCode::Internal
81        );
82    }
83
84    #[test]
85    fn canonical_server_statuses_round_trip_through_error_code() {
86        for code in [
87            ErrorCode::ServiceUnavailable,
88            ErrorCode::Timeout,
89            ErrorCode::ExternalService,
90            ErrorCode::Internal,
91        ] {
92            assert_eq!(status_to_error_code(code.http_status()), code);
93        }
94    }
95
96    #[test]
97    fn cancelled_status_round_trips_through_error_code() {
98        let status = ErrorCode::Cancelled.http_status();
99        assert_eq!(status_to_error_code(status), ErrorCode::Cancelled);
100    }
101
102    #[test]
103    fn exposes_protocol_types_without_framework_coupling() {
104        let request: HttpRequest = http::Request::builder()
105            .uri("/healthz")
106            .body(Vec::new())
107            .unwrap();
108        let response: HttpResponse = http::Response::builder()
109            .status(StatusCode::OK)
110            .body(Vec::new())
111            .unwrap();
112
113        assert_eq!(request.uri(), "/healthz");
114        assert!(is_success_status(response.status()));
115    }
116}