1use std::{net::SocketAddr, sync::Arc, time::Duration};
7
8use anyhow::Result;
9use axum::{
10 Router,
11 extract::{MatchedPath, Request},
12 http::{StatusCode, header::HeaderName},
13 response::{IntoResponse, Response},
14 routing::{get, post},
15};
16use iota_storage::http_key_value_store::ItemType;
17use tokio_util::sync::CancellationToken;
18use tower::ServiceBuilder;
19use tower_http::{
20 classify::ServerErrorsFailureClass,
21 request_id::{MakeRequestUuid, PropagateRequestIdLayer, SetRequestIdLayer},
22 trace::TraceLayer,
23};
24use tracing::{Level, Span, field};
25
26const REQUEST_ID_HEADER: HeaderName = HeaderName::from_static("x-request-id");
27
28use crate::{
29 RestApiConfig,
30 bigtable::KvStoreClient,
31 errors::ApiError,
32 routes::{health, kv_store},
33 types::{RestServerAppState, SharedRestServerAppState},
34};
35
36pub struct Server {
39 router: Router,
40 server_address: SocketAddr,
41 token: CancellationToken,
42}
43
44impl Server {
45 pub async fn new(config: RestApiConfig, token: CancellationToken) -> Result<Self> {
50 let kv_store_client = KvStoreClient::new(config.kv_store_config).await?;
51
52 let shared_state = Arc::new(RestServerAppState {
53 kv_store_client: Arc::new(kv_store_client),
54 multiget_max_items: config.multiget_max_items,
55 });
56
57 Ok(Self {
58 router: build_router(shared_state),
59 token,
60 server_address: config.server_address,
61 })
62 }
63
64 pub async fn serve(self) -> Result<()> {
66 let listener = tokio::net::TcpListener::bind(self.server_address)
67 .await
68 .expect("failed to bind to socket");
69
70 tracing::info!("listening on: {}", self.server_address);
71
72 axum::serve(listener, self.router)
73 .with_graceful_shutdown(async move {
74 self.token.cancelled().await;
75 tracing::info!("shutdown signal received.");
76 })
77 .await
78 .inspect_err(|e| tracing::error!("server encountered an error: {e}"))
79 .map_err(Into::into)
80 }
81}
82
83fn build_router(state: SharedRestServerAppState) -> Router {
85 Router::new()
86 .route("/health", get(health::health))
87 .route("/{item_type}", post(kv_store::multi_get_data))
88 .route("/{item_type}/{key}", get(kv_store::data_as_bytes))
89 .route(
92 &format!("/{}/{{address}}", ItemType::TransactionDigestsByAddress),
93 get(kv_store::transaction_digests_by_address),
94 )
95 .fallback(fallback)
98 .layer(
99 ServiceBuilder::new()
100 .layer(SetRequestIdLayer::new(REQUEST_ID_HEADER, MakeRequestUuid))
101 .layer(
102 TraceLayer::new_for_http()
103 .make_span_with(make_request_span)
104 .on_response(log_response)
105 .on_failure(
106 |class: ServerErrorsFailureClass, latency: Duration, _: &Span| {
107 tracing::error!(
108 %class,
109 latency_ms = latency.as_millis() as u64,
110 "request failed"
111 );
112 },
113 ),
114 )
115 .layer(PropagateRequestIdLayer::new(REQUEST_ID_HEADER)),
116 )
117 .with_state(state)
118}
119
120async fn fallback() -> impl IntoResponse {
129 ApiError::NotFound
130}
131
132fn make_request_span(request: &Request) -> Span {
143 if tracing::span_enabled!(Level::DEBUG) {
144 let request_id = request
145 .headers()
146 .get(REQUEST_ID_HEADER)
147 .and_then(|value| value.to_str().ok())
148 .unwrap_or_default();
149 let route = request
150 .extensions()
151 .get::<MatchedPath>()
152 .map_or_else(|| request.uri().path(), MatchedPath::as_str);
153
154 tracing::debug_span!(
155 "request",
156 request_id,
157 method = %request.method(),
158 route,
159 uri = %request.uri(),
160 error = field::Empty,
161 )
162 } else {
163 tracing::info_span!(
164 "request",
165 uri = %request.uri(),
166 error = field::Empty,
167 )
168 }
169}
170
171fn log_response(response: &Response, latency: Duration, _: &Span) {
177 let status = response.status();
178 let latency_ms = latency.as_millis() as u64;
179
180 if status.is_server_error() {
181 return;
182 }
183
184 if status.is_client_error() && status != StatusCode::NOT_FOUND {
185 tracing::warn!(%status, latency_ms, "request failed with client error");
186 } else {
187 tracing::info!(%status, latency_ms, "request completed");
188 }
189}
190
191#[cfg(test)]
198mod tests {
199 use std::num::NonZeroUsize;
200
201 use axum::{
202 body::Body,
203 http::{Method, Request, StatusCode, header},
204 };
205 use http_body_util::BodyExt;
206 use iota_sdk_types::{Address, TransactionDigest};
207 use iota_storage::http_key_value_store::{
208 TaggedKey, encode_digest, encode_object_key, encoded_tagged_key,
209 };
210 use iota_types::storage::ObjectKey;
211 use tower::ServiceExt;
212
213 use super::*;
214 use crate::{errors::ErrorResponse, routes::health::HealthResponse};
215
216 const MULTIGET_MAX_ITEMS: usize = 5;
217
218 fn test_router() -> Router {
220 build_router(Arc::new(RestServerAppState {
221 kv_store_client: Arc::new(KvStoreClient::new_for_tests()),
222 multiget_max_items: NonZeroUsize::new(MULTIGET_MAX_ITEMS).unwrap(),
223 }))
224 }
225
226 async fn send(router: Router, request: Request<Body>) -> (StatusCode, String) {
228 let response = router.oneshot(request).await.unwrap();
229 let status = response.status();
230 let body = response.into_body().collect().await.unwrap().to_bytes();
231 (status, String::from_utf8_lossy(&body).into_owned())
232 }
233
234 async fn get(uri: &str) -> (StatusCode, String) {
236 send(
237 test_router(),
238 Request::builder().uri(uri).body(Body::empty()).unwrap(),
239 )
240 .await
241 }
242
243 async fn post_json(uri: &str, body: serde_json::Value) -> (StatusCode, String) {
246 send(
247 test_router(),
248 Request::builder()
249 .method(Method::POST)
250 .uri(uri)
251 .header(header::CONTENT_TYPE, "application/json")
252 .body(Body::from(body.to_string()))
253 .unwrap(),
254 )
255 .await
256 }
257
258 fn item_type_cases() -> [(ItemType, String, &'static str); 7] {
264 let digest_key = encode_digest(&TransactionDigest::random());
265 let tagged_key = encoded_tagged_key(&TaggedKey::CheckpointSequenceNumber(1));
266 let object_key = encode_object_key(&ObjectKey::ZERO);
267
268 [
269 (
270 ItemType::Transaction,
271 digest_key.clone(),
272 "invalid digest byte length",
273 ),
274 (
275 ItemType::TransactionEffects,
276 digest_key.clone(),
277 "invalid digest byte length",
278 ),
279 (
280 ItemType::TransactionToCheckpoint,
281 digest_key.clone(),
282 "invalid digest byte length",
283 ),
284 (
285 ItemType::EventTransactionDigest,
286 digest_key.clone(),
287 "invalid digest byte length",
288 ),
289 (
290 ItemType::Object,
291 object_key,
292 "failed to deserialize object key",
293 ),
294 (
295 ItemType::CheckpointContents,
296 tagged_key,
297 "failed to deserialize checkpoint sequence number",
298 ),
299 (
300 ItemType::CheckpointSummary,
301 digest_key,
302 "failed to deserialize checkpoint sequence number",
303 ),
304 ]
305 }
306
307 async fn assert_item_type_route_rejections(
315 item_type: ItemType,
316 valid_key: &str,
317 key_decode_error: &str,
318 ) {
319 let (status, body) = get(&format!("/{item_type}/!!")).await;
321 assert_eq!(
322 status,
323 StatusCode::BAD_REQUEST,
324 "{item_type}: malformed base64 key"
325 );
326 assert!(
327 body.contains("invalid base64 url string"),
328 "{item_type}: unexpected body: {body}"
329 );
330
331 let (status, body) = get(&format!("/{item_type}/AAAA")).await;
335 assert_eq!(
336 status,
337 StatusCode::BAD_REQUEST,
338 "{item_type}: key decode failure"
339 );
340 assert!(
341 body.contains(key_decode_error),
342 "{item_type}: unexpected body: {body}"
343 );
344
345 if item_type != ItemType::Object {
347 let (status, body) =
348 get(&format!("/{item_type}/{valid_key}?before_version=true")).await;
349 assert_eq!(
350 status,
351 StatusCode::BAD_REQUEST,
352 "{item_type}: before_version"
353 );
354 assert!(
355 body.contains("`before_version` query parameter is only valid for `ob` item types"),
356 "{item_type}: unexpected body: {body}"
357 );
358 }
359
360 let (status, _) = get(&format!("/{item_type}/{valid_key}?before_version=notabool")).await;
362 assert_eq!(
363 status,
364 StatusCode::BAD_REQUEST,
365 "{item_type}: invalid query parameter value"
366 );
367 }
368
369 async fn assert_multiget_route_rejections(
379 item_type: ItemType,
380 valid_key: &str,
381 key_decode_error: &str,
382 ) {
383 let (status, body) =
385 post_json(&format!("/{item_type}"), serde_json::json!({ "keys": [] })).await;
386 assert_eq!(status, StatusCode::BAD_REQUEST, "{item_type}: empty keys");
387 assert!(
388 body.contains("no keys provided"),
389 "{item_type}: unexpected body: {body}"
390 );
391
392 let keys = vec![valid_key; MULTIGET_MAX_ITEMS + 1];
394 let (status, body) = post_json(
395 &format!("/{item_type}"),
396 serde_json::json!({ "keys": keys }),
397 )
398 .await;
399 assert_eq!(
400 status,
401 StatusCode::BAD_REQUEST,
402 "{item_type}: too many keys"
403 );
404 assert!(
405 body.contains("too many keys"),
406 "{item_type}: unexpected body: {body}"
407 );
408
409 let (status, body) = post_json(
411 &format!("/{item_type}"),
412 serde_json::json!({ "keys": [valid_key, "!!"] }),
413 )
414 .await;
415 assert_eq!(
416 status,
417 StatusCode::BAD_REQUEST,
418 "{item_type}: malformed base64 key"
419 );
420 assert!(
421 body.contains("invalid key '!!'"),
422 "{item_type}: unexpected body: {body}"
423 );
424
425 let (status, body) = post_json(
428 &format!("/{item_type}"),
429 serde_json::json!({ "keys": ["AAAA"] }),
430 )
431 .await;
432 assert_eq!(
433 status,
434 StatusCode::BAD_REQUEST,
435 "{item_type}: key decode failure"
436 );
437 assert!(
438 body.contains("invalid key 'AAAA'") && body.contains(key_decode_error),
439 "{item_type}: unexpected body: {body}"
440 );
441
442 if item_type != ItemType::Object {
444 let (status, body) = post_json(
445 &format!("/{item_type}?before_version=true"),
446 serde_json::json!({ "keys": [valid_key] }),
447 )
448 .await;
449 assert_eq!(
450 status,
451 StatusCode::BAD_REQUEST,
452 "{item_type}: before_version"
453 );
454 assert!(
455 body.contains("`before_version` query parameter is only valid for `ob` item types"),
456 "{item_type}: unexpected body: {body}"
457 );
458 }
459 }
460
461 #[tokio::test]
462 async fn health_endpoint_reports_ok() {
463 let (status, body) = get("/health").await;
464 assert_eq!(status, StatusCode::OK);
465 let health: HealthResponse = serde_json::from_str(&body).unwrap();
466 assert_eq!(health.status, "OK");
467 }
468
469 #[tokio::test]
470 async fn unknown_route_returns_not_found() {
471 let (status, _) = get("/unknown/route/segments").await;
472 assert_eq!(status, StatusCode::NOT_FOUND);
473 }
474
475 #[tokio::test]
476 async fn unknown_item_type_is_rejected() {
477 let (status, body) = get(&format!(
478 "/zz/{}",
479 encode_digest(&TransactionDigest::random())
480 ))
481 .await;
482 assert_eq!(status, StatusCode::BAD_REQUEST);
483 assert!(
484 body.contains("invalid path parameter"),
485 "unexpected body: {body}"
486 );
487 }
488
489 #[tokio::test]
490 async fn item_type_routes_reject_invalid_requests() {
491 for (item_type, valid_key, key_decode_error) in item_type_cases() {
492 assert_item_type_route_rejections(item_type, &valid_key, key_decode_error).await;
493 }
494 }
495
496 #[tokio::test]
497 async fn before_version_for_min_version_returns_not_found() {
498 let uri = format!(
501 "/ob/{}?before_version=true",
502 encode_object_key(&ObjectKey::ZERO)
503 );
504 let (status, body) = get(&uri).await;
505
506 assert_eq!(status, StatusCode::NOT_FOUND);
507 assert!(body.is_empty(), "expected empty body, got: {body}");
508 }
509
510 #[tokio::test]
511 async fn multiget_routes_reject_invalid_requests() {
512 for (item_type, valid_key, key_decode_error) in item_type_cases() {
513 assert_multiget_route_rejections(item_type, &valid_key, key_decode_error).await;
514 }
515 }
516
517 #[tokio::test]
518 async fn transactions_by_address_rejects_invalid_requests() {
519 let address_key = encode_digest(&Address::random());
520
521 let (status, body) = get("/txa/!!").await;
523 assert_eq!(status, StatusCode::BAD_REQUEST);
524 assert!(
525 body.contains("address is not valid base64-url"),
526 "unexpected body: {body}"
527 );
528
529 let (status, body) = get("/txa/AAAA").await;
531 assert_eq!(status, StatusCode::BAD_REQUEST);
532 assert!(body.contains("invalid address"), "unexpected body: {body}");
533
534 let uri = format!("/txa/{address_key}?limit={}", MULTIGET_MAX_ITEMS + 1);
536 let (status, body) = get(&uri).await;
537 assert_eq!(status, StatusCode::BAD_REQUEST);
538 assert!(body.contains("limit too large"), "unexpected body: {body}");
539
540 for query in ["limit=0", "cursor=notanumber", "oldest_first=notabool"] {
542 let uri = format!("/txa/{address_key}?{query}");
543 let (status, _) = get(&uri).await;
544 assert_eq!(status, StatusCode::BAD_REQUEST, "GET {uri}");
545 }
546 }
547
548 #[tokio::test]
549 async fn multiget_rejects_transaction_digests_by_address_item_type() {
550 let key = encode_digest(&Address::random());
551 let (status, body) = post_json("/txa", serde_json::json!({ "keys": [key] })).await;
552
553 assert_eq!(status, StatusCode::BAD_REQUEST);
554 let res = serde_json::from_str::<ErrorResponse>(&body).unwrap();
555 assert_eq!(res.error_code, "400");
556 assert!(
557 res.error_message.contains("unsupported key"),
558 "unexpected body: {body}"
559 );
560 }
561
562 #[tokio::test]
563 async fn store_error_maps_to_internal_server_error() {
564 let uri = format!("/tx/{}", encode_digest(&TransactionDigest::random()));
567 let (status, body) = get(&uri).await;
568
569 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
570 let res = serde_json::from_str::<ErrorResponse>(&body).unwrap();
571 assert_eq!(res.error_code, "500");
572 assert_eq!(res.error_message, "internal server error");
573 }
574
575 #[tokio::test]
576 async fn wrong_method_returns_method_not_allowed() {
577 let (status, _) = get("/tx").await;
579 assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED);
580 }
581}