Skip to main content

htsget_lambda/
lib.rs

1//! htsget-lambda only has functionality to run the Lambda handler. Please use `htsget-axum`
2//! for similar functionality on routers and logic.
3//!
4
5use futures::TryFuture;
6use htsget_axum::server::ticket::TicketServer;
7use htsget_config::config::Config;
8use htsget_config::package_info;
9use lambda_http::http::Uri;
10use lambda_http::request::{LambdaRequest, RequestContext};
11use lambda_http::{
12  Error, IntoResponse, LambdaEvent, Request, RequestExt, Service, TransformResponse, lambda_runtime,
13};
14use std::marker::PhantomData;
15use std::path::Path;
16use std::task::{Context, Poll};
17use tracing::debug;
18
19/// Wraps the htsget-axum router to forward any Lambda event extensions to the router.
20pub struct Adapter<'a, R, S> {
21  service: S,
22  _phantom_data: PhantomData<&'a R>,
23}
24
25impl<'a, R, S, E> From<S> for Adapter<'a, R, S>
26where
27  S: Service<Request, Response = R, Error = E>,
28  S::Future: Send + 'a,
29  R: IntoResponse,
30{
31  fn from(service: S) -> Self {
32    Adapter {
33      service,
34      _phantom_data: PhantomData,
35    }
36  }
37}
38
39impl<'a, R, S, E> Service<LambdaEvent<serde_json::Value>> for Adapter<'a, R, S>
40where
41  S: Service<Request, Response = R, Error = E>,
42  S::Future: Send + 'a,
43  R: IntoResponse,
44{
45  type Response = <TransformResponse<'a, R, E> as TryFuture>::Ok;
46  type Error = E;
47  type Future = TransformResponse<'a, R, Self::Error>;
48
49  fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
50    self.service.poll_ready(cx)
51  }
52
53  fn call(&mut self, req: LambdaEvent<serde_json::Value>) -> Self::Future {
54    // The original request is consumed when creating a future, so clone it here.
55    let original_request = req.payload.clone();
56
57    let lambda_request: LambdaRequest =
58      serde_json::from_value(req.payload).expect("invalid payload when deserializing to json");
59    let request_origin = lambda_request.request_origin();
60    let mut event: Request = lambda_request.into();
61
62    strip_stage_from_path(&mut event);
63
64    // After creating the request event, add the original request as an extension.
65    debug!(original_request = ?original_request, "original_request");
66    event.extensions_mut().insert(original_request);
67
68    let fut = Box::pin(self.service.call(event.with_lambda_context(req.context)));
69    TransformResponse::Request(request_origin, fut)
70  }
71}
72
73/// Strip the API Gateway stage from the start of the request path so that routing works correctly.
74fn strip_stage_from_path(event: &mut Request) {
75  let stage = match event.request_context_ref() {
76    Some(RequestContext::ApiGatewayV1(context)) => context.stage.as_deref(),
77    Some(RequestContext::ApiGatewayV2(context)) => context.stage.as_deref(),
78    _ => None,
79  };
80
81  let Some(stage) = stage.filter(|stage| *stage != "$default") else {
82    return;
83  };
84
85  let stripped = match event.uri().path().strip_prefix(&format!("/{stage}")) {
86    Some("") => "/",
87    Some(rest) if rest.starts_with('/') => rest,
88    _ => return,
89  };
90
91  let path_and_query = match event.uri().query() {
92    Some(query) => format!("{stripped}?{query}"),
93    None => stripped.to_string(),
94  };
95
96  let mut parts = event.uri().clone().into_parts();
97  let path_and_query = path_and_query
98    .parse()
99    .expect("expected valid path and query from a valid uri");
100  parts.path_and_query = Some(path_and_query);
101  *event.uri_mut() = Uri::from_parts(parts).expect("expected valid parts from a valid uri");
102}
103
104/// Run the Lambda handler using the config file contained at the path.
105pub async fn run_handler(path: &Path) -> Result<(), Error> {
106  let mut config = Config::from_path(path)?;
107  config.set_package_info(package_info!())?;
108  config.setup_tracing()?;
109
110  debug!(config = ?config, "config parsed");
111
112  let service_info = config.service_info().clone();
113  let cors = config.ticket_server().cors().clone();
114  let auth = config.ticket_server().auth().cloned();
115  let package_info = config.package_info().clone();
116  let router = TicketServer::router(
117    config.into_locations(),
118    service_info,
119    cors,
120    auth,
121    Some(package_info),
122  )?;
123
124  lambda_runtime::run(Adapter::from(router)).await
125}
126
127#[cfg(test)]
128mod tests {
129  use super::*;
130  use aws_lambda_events::apigw::{ApiGatewayProxyRequestContext, ApiGatewayV2httpRequestContext};
131  use lambda_http::Body;
132
133  fn v1_request(uri: &str, stage: Option<&str>) -> Request {
134    let mut context = ApiGatewayProxyRequestContext::default();
135    context.stage = stage.map(|stage| stage.to_string());
136    request(uri).with_request_context(RequestContext::ApiGatewayV1(context))
137  }
138
139  fn v2_request(uri: &str, stage: Option<&str>) -> Request {
140    let mut context = ApiGatewayV2httpRequestContext::default();
141    context.stage = stage.map(|stage| stage.to_string());
142    request(uri).with_request_context(RequestContext::ApiGatewayV2(context))
143  }
144
145  fn request(uri: &str) -> Request {
146    lambda_http::http::Request::builder()
147      .uri(uri)
148      .body(Body::Empty)
149      .unwrap()
150  }
151
152  fn strip(mut event: Request) -> String {
153    strip_stage_from_path(&mut event);
154    event.uri().to_string()
155  }
156
157  #[test]
158  fn strip_stage() {
159    assert_eq!(
160      strip(v1_request("/prod/reads/id", Some("prod"))),
161      "/reads/id"
162    );
163    assert_eq!(
164      strip(v2_request("/prod/reads/id", Some("prod"))),
165      "/reads/id"
166    );
167
168    assert_eq!(
169      strip(v1_request("/prod/reads/id?format=BAM", Some("prod"))),
170      "/reads/id?format=BAM"
171    );
172    assert_eq!(strip(v1_request("/prod", Some("prod"))), "/");
173    assert_eq!(strip(request("/prod/reads/id")), "/prod/reads/id");
174    assert_eq!(
175      strip(v1_request("/reads/id", Some("$default"))),
176      "/reads/id"
177    );
178    assert_eq!(strip(v1_request("/reads/id", None)), "/reads/id");
179    assert_eq!(
180      strip(v1_request("/production/reads/id", Some("prod"))),
181      "/production/reads/id"
182    );
183    assert_eq!(strip(v1_request("/reads/id", Some("prod"))), "/reads/id");
184  }
185}