Skip to content

Commit 26d0ece

Browse files
softpropsdavidbarsky
authored andcommitted
impl multi_value_headers (#58)
* impl multi_value_headers * remove redundant closure * impl multi_value_headers in response * fmt * unprefix * missing import in test * rustfmt * improve test assertion msg * rustfmt
1 parent d117272 commit 26d0ece

File tree

4 files changed

+255
-12
lines changed

4 files changed

+255
-12
lines changed

lambda-http/src/request.rs

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::{borrow::Cow, collections::HashMap, fmt, mem};
55

66
use http::{
77
self,
8-
header::{HeaderValue, HOST},
8+
header::{HeaderName, HeaderValue, HOST},
99
HeaderMap, Method, Request as HttpRequest,
1010
};
1111
use serde::{
@@ -31,6 +31,8 @@ pub(crate) struct GatewayRequest<'a> {
3131
pub(crate) http_method: Method,
3232
#[serde(deserialize_with = "deserialize_headers")]
3333
pub(crate) headers: HeaderMap<HeaderValue>,
34+
#[serde(default, deserialize_with = "deserialize_multi_value_headers")]
35+
pub(crate) multi_value_headers: HeaderMap<HeaderValue>,
3436
#[serde(deserialize_with = "nullable_default")]
3537
pub(crate) query_string_parameters: StrMap,
3638
#[serde(deserialize_with = "nullable_default")]
@@ -78,6 +80,7 @@ pub struct Identity {
7880
pub user_arn: Option<String>,
7981
}
8082

83+
/// Deserialize a str into an http::Method
8184
fn deserialize_method<'de, D>(deserializer: D) -> Result<Method, D::Error>
8285
where
8386
D: Deserializer<'de>,
@@ -102,6 +105,49 @@ where
102105
deserializer.deserialize_str(MethodVisitor)
103106
}
104107

108+
/// Deserialize a map of Cow<'_, str> => Vec<Cow<'_, str>> into an http::HeaderMap
109+
fn deserialize_multi_value_headers<'de, D>(deserializer: D) -> Result<HeaderMap<HeaderValue>, D::Error>
110+
where
111+
D: Deserializer<'de>,
112+
{
113+
struct HeaderVisitor;
114+
115+
impl<'de> Visitor<'de> for HeaderVisitor {
116+
type Value = HeaderMap<HeaderValue>;
117+
118+
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
119+
write!(formatter, "a multi valued HeaderMap<HeaderValue>")
120+
}
121+
122+
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
123+
where
124+
A: MapAccess<'de>,
125+
{
126+
let mut headers = map
127+
.size_hint()
128+
.map(HeaderMap::with_capacity)
129+
.unwrap_or_else(HeaderMap::new);
130+
while let Some((key, values)) = map.next_entry::<Cow<'_, str>, Vec<Cow<'_, str>>>()? {
131+
// note the aws docs for multi value headers include an empty key. I'm not sure if this is a doc bug
132+
// or not by the http crate doesn't handle it
133+
// https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
134+
if !key.is_empty() {
135+
for value in values {
136+
let header_name = key.parse::<HeaderName>().map_err(A::Error::custom)?;
137+
let header_value =
138+
HeaderValue::from_shared(value.into_owned().into()).map_err(A::Error::custom)?;
139+
headers.append(header_name, header_value);
140+
}
141+
}
142+
}
143+
Ok(headers)
144+
}
145+
}
146+
147+
deserializer.deserialize_map(HeaderVisitor)
148+
}
149+
150+
/// Deserialize a map of Cow<'_, str> => Cow<'_, str> into an http::HeaderMap
105151
fn deserialize_headers<'de, D>(deserializer: D) -> Result<HeaderMap<HeaderValue>, D::Error>
106152
where
107153
D: Deserializer<'de>,
@@ -119,11 +165,13 @@ where
119165
where
120166
A: MapAccess<'de>,
121167
{
122-
let mut headers = http::HeaderMap::new();
168+
let mut headers = map
169+
.size_hint()
170+
.map(HeaderMap::with_capacity)
171+
.unwrap_or_else(HeaderMap::new);
123172
while let Some((key, value)) = map.next_entry::<Cow<'_, str>, Cow<'_, str>>()? {
124-
let header_name = key.parse::<http::header::HeaderName>().map_err(A::Error::custom)?;
125-
let header_value =
126-
http::header::HeaderValue::from_shared(value.into_owned().into()).map_err(A::Error::custom)?;
173+
let header_name = key.parse::<HeaderName>().map_err(A::Error::custom)?;
174+
let header_value = HeaderValue::from_shared(value.into_owned().into()).map_err(A::Error::custom)?;
127175
headers.append(header_name, header_value);
128176
}
129177
Ok(headers)
@@ -150,6 +198,7 @@ impl<'a> From<GatewayRequest<'a>> for HttpRequest<Body> {
150198
path,
151199
http_method,
152200
headers,
201+
mut multi_value_headers,
153202
query_string_parameters,
154203
path_parameters,
155204
stage_variables,
@@ -191,8 +240,20 @@ impl<'a> From<GatewayRequest<'a>> for HttpRequest<Body> {
191240
})
192241
.expect("failed to build request");
193242

243+
// merge headers into multi_value_headers and make
244+
// multi_value_headers our cannoncial source of request headers
245+
for (key, value) in headers {
246+
// see HeaderMap#into_iter() docs for cases when key element may be None
247+
if let Some(first_key) = key {
248+
// if it contains the key, avoid appending a duplicate value
249+
if !multi_value_headers.contains_key(&first_key) {
250+
multi_value_headers.append(first_key, value);
251+
}
252+
}
253+
}
254+
194255
// no builder method that sets headers in batch
195-
mem::replace(req.headers_mut(), headers);
256+
mem::replace(req.headers_mut(), multi_value_headers);
196257

197258
req
198259
}
@@ -224,8 +285,18 @@ mod tests {
224285
fn deserializes_request_events() {
225286
// from the docs
226287
// https://docs.aws.amazon.com/lambda/latest/dg/eventsources.html#eventsources-api-gateway-request
227-
let input = include_str!("../tests/data/proxy_request.json");
228-
assert!(serde_json::from_str::<GatewayRequest<'_>>(&input).is_ok())
288+
let input = include_str!("../tests/data/apigw_proxy_request.json");
289+
let result = serde_json::from_str::<GatewayRequest<'_>>(&input);
290+
assert!(result.is_ok(), format!("event was not parsed as expected {:?}", result));
291+
}
292+
293+
#[test]
294+
fn deserialize_multi_value_events() {
295+
// from docs
296+
// https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
297+
let input = include_str!("../tests/data/apigw_multi_value_proxy_request.json");
298+
let result = serde_json::from_str::<GatewayRequest<'_>>(&input);
299+
assert!(result.is_ok(), format!("event was not parsed as expected {:?}", result));
229300
}
230301

231302
#[test]

lambda-http/src/response.rs

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
use std::ops::Not;
55

66
use http::{
7-
header::{HeaderMap, HeaderValue},
7+
header::{HeaderMap, HeaderValue, CONTENT_TYPE},
88
Response,
99
};
1010
use serde::{
@@ -22,23 +22,47 @@ pub(crate) struct GatewayResponse {
2222
pub status_code: u16,
2323
#[serde(skip_serializing_if = "HeaderMap::is_empty", serialize_with = "serialize_headers")]
2424
pub headers: HeaderMap<HeaderValue>,
25+
#[serde(
26+
skip_serializing_if = "HeaderMap::is_empty",
27+
serialize_with = "serialize_multi_value_headers"
28+
)]
29+
pub multi_value_headers: HeaderMap<HeaderValue>,
2530
#[serde(skip_serializing_if = "Option::is_none")]
2631
pub body: Option<Body>,
2732
#[serde(skip_serializing_if = "Not::not")]
2833
pub is_base64_encoded: bool,
2934
}
3035

36+
#[cfg(test)]
3137
impl Default for GatewayResponse {
3238
fn default() -> Self {
3339
Self {
3440
status_code: 200,
3541
headers: Default::default(),
42+
multi_value_headers: Default::default(),
3643
body: Default::default(),
3744
is_base64_encoded: Default::default(),
3845
}
3946
}
4047
}
4148

49+
/// Serialize a http::HeaderMap into a serde str => str map
50+
fn serialize_multi_value_headers<S>(headers: &HeaderMap<HeaderValue>, serializer: S) -> Result<S::Ok, S::Error>
51+
where
52+
S: Serializer,
53+
{
54+
let mut map = serializer.serialize_map(Some(headers.keys_len()))?;
55+
for key in headers.keys() {
56+
let mut map_values = Vec::new();
57+
for value in headers.get_all(key) {
58+
map_values.push(value.to_str().map_err(S::Error::custom)?)
59+
}
60+
map.serialize_entry(key.as_str(), &map_values)?;
61+
}
62+
map.end()
63+
}
64+
65+
/// Serialize a http::HeaderMap into a serde str => Vec<str> map
4266
fn serialize_headers<S>(headers: &HeaderMap<HeaderValue>, serializer: S) -> Result<S::Ok, S::Error>
4367
where
4468
S: Serializer,
@@ -65,7 +89,8 @@ where
6589
GatewayResponse {
6690
status_code: parts.status.as_u16(),
6791
body,
68-
headers: parts.headers,
92+
headers: parts.headers.clone(),
93+
multi_value_headers: parts.headers,
6994
is_base64_encoded,
7095
}
7196
}
@@ -114,7 +139,7 @@ where
114139
impl IntoResponse for serde_json::Value {
115140
fn into_response(self) -> Response<Body> {
116141
Response::builder()
117-
.header(http::header::CONTENT_TYPE, "application/json")
142+
.header(CONTENT_TYPE, "application/json")
118143
.body(
119144
serde_json::to_string(&self)
120145
.expect("unable to serialize serde_json::Value")
@@ -127,6 +152,7 @@ impl IntoResponse for serde_json::Value {
127152
#[cfg(test)]
128153
mod tests {
129154
use super::{Body, GatewayResponse, IntoResponse};
155+
use http::{header::CONTENT_TYPE, Response};
130156
use serde_json::{self, json};
131157

132158
#[test]
@@ -139,7 +165,7 @@ mod tests {
139165
assert_eq!(
140166
response
141167
.headers()
142-
.get(http::header::CONTENT_TYPE)
168+
.get(CONTENT_TYPE)
143169
.map(|h| h.to_str().expect("invalid header")),
144170
Some("application/json")
145171
)
@@ -176,4 +202,19 @@ mod tests {
176202
r#"{"statusCode":200,"body":"foo"}"#
177203
);
178204
}
205+
206+
#[test]
207+
fn serialize_multi_value_headers() {
208+
let res: GatewayResponse = Response::builder()
209+
.header("multi", "a")
210+
.header("multi", "b")
211+
.body(Body::from(()))
212+
.expect("failed to create response")
213+
.into();
214+
let json = serde_json::to_string(&res).expect("failed to serialize to json");
215+
assert_eq!(
216+
json,
217+
r#"{"statusCode":200,"headers":{"multi":"a"},"multiValueHeaders":{"multi":["a","b"]}}"#
218+
)
219+
}
179220
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
{
2+
"resource": "/{proxy+}",
3+
"path": "/hello/world",
4+
"httpMethod": "POST",
5+
"headers": {
6+
"Accept": "*/*",
7+
"Accept-Encoding": "gzip, deflate",
8+
"cache-control": "no-cache",
9+
"CloudFront-Forwarded-Proto": "https",
10+
"CloudFront-Is-Desktop-Viewer": "true",
11+
"CloudFront-Is-Mobile-Viewer": "false",
12+
"CloudFront-Is-SmartTV-Viewer": "false",
13+
"CloudFront-Is-Tablet-Viewer": "false",
14+
"CloudFront-Viewer-Country": "US",
15+
"Content-Type": "application/json",
16+
"headerName": "headerValue",
17+
"Host": "gy415nuibc.execute-api.us-east-1.amazonaws.com",
18+
"Postman-Token": "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f",
19+
"User-Agent": "PostmanRuntime/2.4.5",
20+
"Via": "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)",
21+
"X-Amz-Cf-Id": "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==",
22+
"X-Forwarded-For": "54.240.196.186, 54.182.214.83",
23+
"X-Forwarded-Port": "443",
24+
"X-Forwarded-Proto": "https"
25+
},
26+
"multiValueHeaders":{
27+
"Accept":[
28+
"*/*"
29+
],
30+
"Accept-Encoding":[
31+
"gzip, deflate"
32+
],
33+
"cache-control":[
34+
"no-cache"
35+
],
36+
"CloudFront-Forwarded-Proto":[
37+
"https"
38+
],
39+
"CloudFront-Is-Desktop-Viewer":[
40+
"true"
41+
],
42+
"CloudFront-Is-Mobile-Viewer":[
43+
"false"
44+
],
45+
"CloudFront-Is-SmartTV-Viewer":[
46+
"false"
47+
],
48+
"CloudFront-Is-Tablet-Viewer":[
49+
"false"
50+
],
51+
"CloudFront-Viewer-Country":[
52+
"US"
53+
],
54+
"":[
55+
""
56+
],
57+
"Content-Type":[
58+
"application/json"
59+
],
60+
"headerName":[
61+
"headerValue"
62+
],
63+
"Host":[
64+
"gy415nuibc.execute-api.us-east-1.amazonaws.com"
65+
],
66+
"Postman-Token":[
67+
"9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f"
68+
],
69+
"User-Agent":[
70+
"PostmanRuntime/2.4.5"
71+
],
72+
"Via":[
73+
"1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)"
74+
],
75+
"X-Amz-Cf-Id":[
76+
"pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A=="
77+
],
78+
"X-Forwarded-For":[
79+
"54.240.196.186, 54.182.214.83"
80+
],
81+
"X-Forwarded-Port":[
82+
"443"
83+
],
84+
"X-Forwarded-Proto":[
85+
"https"
86+
]
87+
},
88+
"queryStringParameters": {
89+
"name": "me",
90+
"multivalueName": "me"
91+
},
92+
"multiValueQueryStringParameters":{
93+
"name":[
94+
"me"
95+
],
96+
"multivalueName":[
97+
"you",
98+
"me"
99+
]
100+
},
101+
"pathParameters": {
102+
"proxy": "hello/world"
103+
},
104+
"stageVariables": {
105+
"stageVariableName": "stageVariableValue"
106+
},
107+
"requestContext": {
108+
"accountId": "12345678912",
109+
"resourceId": "roq9wj",
110+
"stage": "testStage",
111+
"requestId": "deef4878-7910-11e6-8f14-25afc3e9ae33",
112+
"identity": {
113+
"cognitoIdentityPoolId": null,
114+
"accountId": null,
115+
"cognitoIdentityId": null,
116+
"caller": null,
117+
"apiKey": null,
118+
"sourceIp": "192.168.196.186",
119+
"cognitoAuthenticationType": null,
120+
"cognitoAuthenticationProvider": null,
121+
"userArn": null,
122+
"userAgent": "PostmanRuntime/2.4.5",
123+
"user": null
124+
},
125+
"resourcePath": "/{proxy+}",
126+
"httpMethod": "POST",
127+
"apiId": "gy415nuibc"
128+
},
129+
"body": "{\r\n\t\"a\": 1\r\n}",
130+
"isBase64Encoded": false
131+
}

0 commit comments

Comments
 (0)