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
56impl<T, S> axum::extract::FromRequest<S> for Bcs<T>
57where
58    T: serde::de::DeserializeOwned,
59    S: Send + Sync,
60{
61    type Rejection = BcsRejection;
62
63    async fn from_request(
64        req: axum::http::Request<axum::body::Body>,
65        state: &S,
66    ) -> Result<Self, Self::Rejection> {
67        if bcs_content_type(req.headers()) {
68            let bytes = axum::body::Bytes::from_request(req, state)
69                .await
70                .map_err(BcsRejection::BytesRejection)?;
71            bcs::from_bytes(&bytes)
72                .map(Self)
73                .map_err(BcsRejection::DeserializationError)
74        } else {
75            Err(BcsRejection::MissingBcsContentType)
76        }
77    }
78}
79
80fn bcs_content_type(headers: &HeaderMap) -> bool {
81    let Some(ContentType(mime)) = ContentType::from_headers(headers) else {
82        return false;
83    };
84
85    let is_bcs_content_type = mime.type_() == "application"
86        && (mime.subtype() == "bcs" || mime.suffix().is_some_and(|name| name == "bcs"));
87
88    is_bcs_content_type
89}
90
91pub enum BcsRejection {
92    MissingBcsContentType,
93    DeserializationError(bcs::Error),
94    BytesRejection(axum::extract::rejection::BytesRejection),
95}
96
97impl axum::response::IntoResponse for BcsRejection {
98    fn into_response(self) -> axum::response::Response {
99        match self {
100            BcsRejection::MissingBcsContentType => (
101                StatusCode::UNSUPPORTED_MEDIA_TYPE,
102                "Expected request with `Content-Type: application/bcs`",
103            )
104                .into_response(),
105            BcsRejection::DeserializationError(e) => (
106                StatusCode::UNPROCESSABLE_ENTITY,
107                format!("Failed to deserialize the BCS body into the target type: {e}"),
108            )
109                .into_response(),
110            BcsRejection::BytesRejection(bytes_rejection) => bytes_rejection.into_response(),
111        }
112    }
113}
114
115impl<T, J> axum::response::IntoResponse for ResponseContent<T, J>
116where
117    T: serde::Serialize,
118    J: serde::Serialize,
119{
120    fn into_response(self) -> axum::response::Response {
121        match self {
122            ResponseContent::Bcs(inner) => Bcs(inner).into_response(),
123            ResponseContent::Json(inner) => axum::Json(inner).into_response(),
124        }
125    }
126}
127
128pub async fn append_info_headers(
129    State(state): State<RestService>,
130    response: Response,
131) -> impl IntoResponse {
132    let mut headers = HeaderMap::new();
133
134    if let Ok(chain_id) = state.chain_id().to_string().try_into() {
135        headers.insert(X_IOTA_CHAIN_ID, chain_id);
136    }
137
138    if let Ok(chain) = state.chain_id().chain().as_str().try_into() {
139        headers.insert(X_IOTA_CHAIN, chain);
140    }
141
142    if let Ok(latest_checkpoint) = state.reader.inner().get_latest_checkpoint() {
143        headers.insert(X_IOTA_EPOCH, latest_checkpoint.epoch().into());
144        headers.insert(
145            X_IOTA_CHECKPOINT_HEIGHT,
146            latest_checkpoint.sequence_number.into(),
147        );
148        headers.insert(X_IOTA_TIMESTAMP_MS, latest_checkpoint.timestamp_ms.into());
149    }
150
151    if let Ok(lowest_available_checkpoint) = state.reader.inner().get_lowest_available_checkpoint()
152    {
153        headers.insert(
154            X_IOTA_LOWEST_AVAILABLE_CHECKPOINT,
155            lowest_available_checkpoint.into(),
156        );
157    }
158
159    if let Ok(lowest_available_checkpoint_objects) = state
160        .reader
161        .inner()
162        .get_lowest_available_checkpoint_objects()
163    {
164        headers.insert(
165            X_IOTA_LOWEST_AVAILABLE_CHECKPOINT_OBJECTS,
166            lowest_available_checkpoint_objects.into(),
167        );
168    }
169
170    (headers, response)
171}