use bytes::Bytes;
use lambda_http::aws_lambda_events::apigw::ApiGatewayRequestAuthorizer;
use lambda_http::aws_lambda_events::query_map::QueryMap;
use lambda_http::{Request, RequestExt};
use std::collections::HashMap;
pub struct LambdaRequest {
pub method: http::Method,
pub path: String,
pub query_string: Option<String>,
pub headers: HashMap<String, String>,
pub body: Bytes,
pub path_parameters: HashMap<String, String>,
pub stage_variables: HashMap<String, String>,
pub request_context: RequestContext,
}
#[derive(Debug, Clone, Default)]
pub struct RequestContext {
pub request_id: Option<String>,
pub stage: Option<String>,
pub domain_name: Option<String>,
pub http_method: Option<String>,
pub source_ip: Option<String>,
pub user_agent: Option<String>,
pub authorizer_claims: HashMap<String, String>,
}
impl LambdaRequest {
pub fn from_lambda_request(request: Request) -> Self {
let path_parameters = query_map_to_hashmap(&request.path_parameters());
let stage_variables = query_map_to_hashmap(&request.stage_variables());
let (parts, body) = request.into_parts();
let mut headers = HashMap::new();
for (name, value) in parts.headers.iter() {
if let Ok(v) = value.to_str() {
headers.insert(name.to_string(), v.to_string());
}
}
let query_string = parts.uri.query().map(String::from);
let request_context = parts
.extensions
.get::<lambda_http::request::RequestContext>()
.map(|ctx| match ctx {
lambda_http::request::RequestContext::ApiGatewayV2(v2) => RequestContext {
request_id: v2.request_id.clone(),
stage: v2.stage.clone(),
domain_name: v2.domain_name.clone(),
http_method: Some(v2.http.method.to_string()),
source_ip: v2.http.source_ip.clone(),
user_agent: v2.http.user_agent.clone(),
authorizer_claims: v2
.authorizer
.as_ref()
.map(extract_claims)
.unwrap_or_default(),
},
lambda_http::request::RequestContext::ApiGatewayV1(v1) => RequestContext {
request_id: v1.request_id.clone(),
stage: v1.stage.clone(),
domain_name: v1.domain_name.clone(),
http_method: Some(v1.http_method.to_string()),
source_ip: v1.identity.source_ip.clone(),
user_agent: v1.identity.user_agent.clone(),
authorizer_claims: extract_claims(&v1.authorizer),
},
lambda_http::request::RequestContext::Alb(_) => RequestContext::default(),
_ => RequestContext::default(),
})
.unwrap_or_default();
let body_bytes = match body {
lambda_http::Body::Empty => Bytes::new(),
lambda_http::Body::Text(s) => Bytes::from(s),
lambda_http::Body::Binary(b) => Bytes::from(b),
_ => Bytes::new(),
};
Self {
method: parts.method,
path: parts.uri.path().to_string(),
query_string,
headers,
body: body_bytes,
path_parameters,
stage_variables,
request_context,
}
}
pub fn header(&self, name: &str) -> Option<&str> {
self.headers
.get(&name.to_lowercase())
.or_else(|| self.headers.get(name))
.map(|s| s.as_str())
}
pub fn content_type(&self) -> Option<&str> {
self.header("content-type")
}
pub fn is_json(&self) -> bool {
self.content_type()
.map(|ct| ct.contains("application/json"))
.unwrap_or(false)
}
pub fn source_ip(&self) -> Option<&str> {
self.request_context.source_ip.as_deref()
}
pub fn path_parameter(&self, name: &str) -> Option<&str> {
self.path_parameters.get(name).map(|s| s.as_str())
}
pub fn stage_variable(&self, name: &str) -> Option<&str> {
self.stage_variables.get(name).map(|s| s.as_str())
}
pub fn claims(&self) -> &HashMap<String, String> {
&self.request_context.authorizer_claims
}
pub fn claim(&self, key: &str) -> Option<&str> {
self.request_context
.authorizer_claims
.get(key)
.map(|s| s.as_str())
}
}
fn query_map_to_hashmap(map: &QueryMap) -> HashMap<String, String> {
let mut out = HashMap::new();
for (key, value) in map.iter() {
out.entry(key.to_string())
.or_insert_with(|| value.to_string());
}
out
}
fn extract_claims(authorizer: &ApiGatewayRequestAuthorizer) -> HashMap<String, String> {
if let Some(jwt) = &authorizer.jwt
&& !jwt.claims.is_empty()
{
return jwt.claims.clone();
}
if let Some(serde_json::Value::Object(claims)) = authorizer.fields.get("claims") {
return claims
.iter()
.filter_map(|(key, value)| match value {
serde_json::Value::String(s) => Some((key.clone(), s.clone())),
serde_json::Value::Null => None,
other => Some((key.clone(), other.to_string())),
})
.collect();
}
HashMap::new()
}
#[cfg(test)]
mod tests {
use super::*;
use lambda_http::Body;
use lambda_http::request::RequestContext as HttpRequestContext;
fn v2_context_json() -> &'static str {
r#"{
"routeKey": "POST /users/{id}",
"accountId": "123456789012",
"stage": "$default",
"requestId": "req-v2-1",
"authorizer": {
"jwt": {
"claims": { "sub": "user-123", "email": "user@example.com" },
"scopes": ["read"]
}
},
"apiId": "abcd1234",
"domainName": "api.example.com",
"http": {
"method": "POST",
"path": "/users/42",
"protocol": "HTTP/1.1",
"sourceIp": "203.0.113.7",
"userAgent": "test-agent/1.0"
},
"timeEpoch": 0
}"#
}
fn v1_context_json() -> &'static str {
r#"{
"accountId": "123456789012",
"resourceId": "abc123",
"stage": "prod",
"requestId": "req-v1-1",
"domainName": "api.example.com",
"identity": {
"sourceIp": "198.51.100.9",
"userAgent": "rest-agent/2.0"
},
"authorizer": {
"claims": {
"sub": "cognito-user-9",
"cognito:username": "alice"
}
},
"resourcePath": "/users/{id}",
"httpMethod": "POST",
"apiId": "restapi1"
}"#
}
fn make_v2_request() -> Request {
let ctx: lambda_http::aws_lambda_events::apigw::ApiGatewayV2httpRequestContext =
serde_json::from_str(v2_context_json()).expect("v2 context deserializes");
let mut path_params = HashMap::new();
path_params.insert("id".to_string(), "42".to_string());
let mut stage_vars = HashMap::new();
stage_vars.insert("env".to_string(), "staging".to_string());
http::Request::builder()
.method("POST")
.uri("https://api.example.com/users/42?page=2")
.header("content-type", "application/json")
.header("x-custom", "hello")
.body(Body::Text("{\"name\":\"a\"}".to_string()))
.unwrap()
.with_path_parameters(path_params)
.with_stage_variables(stage_vars)
.with_request_context(HttpRequestContext::ApiGatewayV2(ctx))
}
fn make_v1_request() -> Request {
let ctx: lambda_http::aws_lambda_events::apigw::ApiGatewayProxyRequestContext =
serde_json::from_str(v1_context_json()).expect("v1 context deserializes");
let mut path_params = HashMap::new();
path_params.insert("id".to_string(), "42".to_string());
let mut stage_vars = HashMap::new();
stage_vars.insert("region".to_string(), "us-east-1".to_string());
http::Request::builder()
.method("POST")
.uri("https://api.example.com/users/42")
.header("content-type", "application/json")
.body(Body::Text("body-1".to_string()))
.unwrap()
.with_path_parameters(path_params)
.with_stage_variables(stage_vars)
.with_request_context(HttpRequestContext::ApiGatewayV1(ctx))
}
#[test]
fn v2_claims_are_populated_from_jwt() {
let req = LambdaRequest::from_lambda_request(make_v2_request());
assert_eq!(req.claim("sub"), Some("user-123"));
assert_eq!(req.claim("email"), Some("user@example.com"));
assert_eq!(req.claims().len(), 2);
}
#[test]
fn v1_claims_are_populated_from_authorizer_map() {
let req = LambdaRequest::from_lambda_request(make_v1_request());
assert_eq!(req.claim("sub"), Some("cognito-user-9"));
assert_eq!(req.claim("cognito:username"), Some("alice"));
assert_eq!(req.claims().len(), 2);
}
#[test]
fn v2_path_parameters_and_stage_variables_are_extracted() {
let req = LambdaRequest::from_lambda_request(make_v2_request());
assert_eq!(req.path_parameter("id"), Some("42"));
assert_eq!(req.stage_variable("env"), Some("staging"));
}
#[test]
fn v1_path_parameters_and_stage_variables_are_extracted() {
let req = LambdaRequest::from_lambda_request(make_v1_request());
assert_eq!(req.path_parameter("id"), Some("42"));
assert_eq!(req.stage_variable("region"), Some("us-east-1"));
}
#[test]
fn context_and_headers_and_body_are_mapped() {
let req = LambdaRequest::from_lambda_request(make_v2_request());
assert_eq!(req.method, http::Method::POST);
assert_eq!(req.path, "/users/42");
assert_eq!(req.query_string.as_deref(), Some("page=2"));
assert_eq!(req.header("content-type"), Some("application/json"));
assert_eq!(req.header("x-custom"), Some("hello"));
assert!(req.is_json());
assert_eq!(&req.body[..], b"{\"name\":\"a\"}");
assert_eq!(req.request_context.request_id.as_deref(), Some("req-v2-1"));
assert_eq!(req.request_context.stage.as_deref(), Some("$default"));
assert_eq!(req.source_ip(), Some("203.0.113.7"));
assert_eq!(
req.request_context.user_agent.as_deref(),
Some("test-agent/1.0")
);
}
#[test]
fn missing_context_yields_empty_defaults() {
let req = LambdaRequest::from_lambda_request(
http::Request::builder()
.method("GET")
.uri("https://api.example.com/health")
.body(Body::Empty)
.unwrap(),
);
assert!(req.claims().is_empty());
assert!(req.path_parameters.is_empty());
assert!(req.stage_variables.is_empty());
assert_eq!(req.body.len(), 0);
}
}