iota_rest_api/
response.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use axum::{
6    extract::State,
7    http::{HeaderMap, StatusCode},
8    response::{IntoResponse, Response},
9};
10
11use crate::{
12    APPLICATION_BCS, RestService, TEXT_PLAIN_UTF_8,
13    content_type::ContentType,
14    types::{
15        X_IOTA_CHAIN, X_IOTA_CHAIN_ID, X_IOTA_CHECKPOINT_HEIGHT, X_IOTA_EPOCH,
16        X_IOTA_LOWEST_AVAILABLE_CHECKPOINT, X_IOTA_LOWEST_AVAILABLE_CHECKPOINT_OBJECTS,
17        X_IOTA_TIMESTAMP_MS,
18    },
19};
20
21pub struct Bcs<T>(pub T);
22
23#[derive(Debug)]
24pub enum ResponseContent<T, J = T> {
25    Bcs(T),
26    Json(J),
27}
28
29impl<T> axum::response::IntoResponse for Bcs<T>
30where
31    T: serde::Serialize,
32{
33    fn into_response(self) -> axum::response::Response {
34        match bcs::to_bytes(&self.0) {
35            Ok(buf) => (
36                [(
37                    axum::http::header::CONTENT_TYPE,
38                    axum::http::HeaderValue::from_static(APPLICATION_BCS),
39                )],
40                buf,
41            )
42                .into_response(),
43            Err(err) => (
44                StatusCode::INTERNAL_SERVER_ERROR,
45                [(
46                    axum::http::header::CONTENT_TYPE,
47                    axum::http::HeaderValue::from_static(TEXT_PLAIN_UTF_8),
48                )],
49                err.to_string(),
50            )
51                .into_response(),
52        }
53    }
54}
55
56#[axum::async_trait]
57impl<T, S> axum::extract::FromRequest<S> for Bcs<T>
58where
59    T: serde::de::DeserializeOwned,
60    S: Send + Sync,
61{
62    type Rejection = BcsRejection;
63
64    async fn from_request(
65        req: axum::http::Request<axum::body::Body>,
66        state: &S,
67    ) -> Result<Self, Self::Rejection> {
68        if bcs_content_type(req.headers()) {
69            let bytes = axum::body::Bytes::from_request(req, state)
70                .await
71                .map_err(BcsRejection::BytesRejection)?;
72            bcs::from_bytes(&bytes)
73                .map(Self)
74                .map_err(BcsRejection::DeserializationError)
75        } else {
76            Err(BcsRejection::MissingBcsContentType)
77        }
78    }
79}
80
81fn bcs_content_type(headers: &HeaderMap) -> bool {
82    let Some(ContentType(mime)) = ContentType::from_headers(headers) else {
83        return false;
84    };
85
86    let is_bcs_content_type = mime.type_() == "application"
87        && (mime.subtype() == "bcs" || mime.suffix().is_some_and(|name| name == "bcs"));
88
89    is_bcs_content_type
90}
91
92pub enum BcsRejection {
93    MissingBcsContentType,
94    DeserializationError(bcs::Error),
95    BytesRejection(axum::extract::rejection::BytesRejection),
96}
97
98impl axum::response::IntoResponse for BcsRejection {
99    fn into_response(self) -> axum::response::Response {
100        match self {
101            BcsRejection::MissingBcsContentType => (
102                StatusCode::UNSUPPORTED_MEDIA_TYPE,
103                "Expected request with `Content-Type: application/bcs`",
104            )
105                .into_response(),
106            BcsRejection::DeserializationError(e) => (
107                StatusCode::UNPROCESSABLE_ENTITY,
108                format!("Failed to deserialize the BCS body into the target type: {e}"),
109            )
110                .into_response(),
111            BcsRejection::BytesRejection(bytes_rejection) => bytes_rejection.into_response(),
112        }
113    }
114}
115
116impl<T, J> axum::response::IntoResponse for ResponseContent<T, J>
117where
118    T: serde::Serialize,
119    J: serde::Serialize,
120{
121    fn into_response(self) -> axum::response::Response {
122        match self {
123            ResponseContent::Bcs(inner) => Bcs(inner).into_response(),
124            ResponseContent::Json(inner) => axum::Json(inner).into_response(),
125        }
126    }
127}
128
129pub async fn append_info_headers(
130    State(state): State<RestService>,
131    response: Response,
132) -> impl IntoResponse {
133    let mut headers = HeaderMap::new();
134
135    if let Ok(chain_id) = state.chain_id().to_string().try_into() {
136        headers.insert(X_IOTA_CHAIN_ID, chain_id);
137    }
138
139    if let Ok(chain) = state.chain_id().chain().as_str().try_into() {
140        headers.insert(X_IOTA_CHAIN, chain);
141    }
142
143    if let Ok(latest_checkpoint) = state.reader.inner().get_latest_checkpoint() {
144        headers.insert(X_IOTA_EPOCH, latest_checkpoint.epoch().into());
145        headers.insert(
146            X_IOTA_CHECKPOINT_HEIGHT,
147            latest_checkpoint.sequence_number.into(),
148        );
149        headers.insert(X_IOTA_TIMESTAMP_MS, latest_checkpoint.timestamp_ms.into());
150    }
151
152    if let Ok(lowest_available_checkpoint) = state.reader.inner().get_lowest_available_checkpoint()
153    {
154        headers.insert(
155            X_IOTA_LOWEST_AVAILABLE_CHECKPOINT,
156            lowest_available_checkpoint.into(),
157        );
158    }
159
160    if let Ok(lowest_available_checkpoint_objects) = state
161        .reader
162        .inner()
163        .get_lowest_available_checkpoint_objects()
164    {
165        headers.insert(
166            X_IOTA_LOWEST_AVAILABLE_CHECKPOINT_OBJECTS,
167            lowest_available_checkpoint_objects.into(),
168        );
169    }
170
171    (headers, response)
172}