Skip to main content

aep_tower/
response.rs

1use aep_core::{ErrorCode, new_problem_details};
2use aep_service::ServiceResponse;
3use bytes::Bytes;
4use http::{HeaderValue, Response, StatusCode, header};
5use http_body_util::Full;
6
7use crate::TowerError;
8
9pub type HttpResponse = Response<Full<Bytes>>;
10
11pub(crate) fn service_response(response: ServiceResponse) -> Result<HttpResponse, TowerError> {
12    let status = StatusCode::from_u16(response.status).map_err(|_| {
13        TowerError::InvalidConfiguration("Service returned an invalid HTTP status".to_owned())
14    })?;
15    let body = serde_json::to_vec(&response.to_json()?)?;
16    let mut result = Response::new(Full::new(Bytes::from(body)));
17    *result.status_mut() = status;
18    *result.headers_mut() = response.headers;
19    Ok(result)
20}
21
22pub(crate) fn json_response(
23    status: StatusCode,
24    content_type: &'static str,
25    value: &impl serde::Serialize,
26) -> Result<HttpResponse, TowerError> {
27    let mut response = Response::new(Full::new(Bytes::from(serde_json::to_vec(value)?)));
28    *response.status_mut() = status;
29    response
30        .headers_mut()
31        .insert(header::CONTENT_TYPE, HeaderValue::from_static(content_type));
32    Ok(response)
33}
34
35pub(crate) fn problem_response(code: ErrorCode, status: StatusCode) -> HttpResponse {
36    // The registry spells each code in lower snake case and the published titles are the same
37    // words as one sentence, so only the first letter is capitalised. Title-casing every word
38    // would disagree with the Problem Details the Service itself emits for the same code.
39    let mut characters = code.as_str().replace('_', " ").into_bytes();
40    if let Some(first) = characters.first_mut() {
41        first.make_ascii_uppercase();
42    }
43    let title = String::from_utf8(characters).unwrap_or_else(|_| code.as_str().to_owned());
44    let problem = new_problem_details(code, title, i64::from(status.as_u16()));
45    json_response(status, aep_core::PROBLEM_MEDIA_TYPE, &problem)
46        .unwrap_or_else(|_| empty_response(StatusCode::INTERNAL_SERVER_ERROR))
47}
48
49/// Builds the `405` for a known AEP endpoint reached with the wrong method.
50///
51/// RFC 9110 Section 15.5.6 requires the `Allow` field to name every method the target resource
52/// supports, and the body is AEP Problem Details so that a caller parses this failure the same
53/// way it parses every other one.
54pub(crate) fn method_not_allowed(allow: &'static str) -> HttpResponse {
55    let mut response = problem_response(ErrorCode::InvalidRequest, StatusCode::METHOD_NOT_ALLOWED);
56    response
57        .headers_mut()
58        .insert(header::ALLOW, HeaderValue::from_static(allow));
59    response
60}
61
62/// Reduces a `GET` response to the `HEAD` response for the same target.
63///
64/// RFC 9110 Section 9.3.2 makes `HEAD` identical to `GET` except that no content is sent, and the
65/// representation metadata is unchanged, so `Content-Length` keeps the length the `GET` response
66/// would have carried.
67pub(crate) fn head_response(response: HttpResponse) -> HttpResponse {
68    let (mut parts, body) = response.into_parts();
69    let length = http_body::Body::size_hint(&body).exact().unwrap_or(0);
70    parts
71        .headers
72        .insert(header::CONTENT_LENGTH, HeaderValue::from(length));
73    Response::from_parts(parts, Full::new(Bytes::new()))
74}
75
76pub(crate) fn empty_response(status: StatusCode) -> HttpResponse {
77    let mut response = Response::new(Full::new(Bytes::new()));
78    *response.status_mut() = status;
79    response
80}
81
82pub(crate) fn internal_error() -> HttpResponse {
83    empty_response(StatusCode::INTERNAL_SERVER_ERROR)
84}