Skip to content

Commit 80a421f

Browse files
committed
fix: multi value header handling
Close #1861
1 parent 6210a3b commit 80a421f

File tree

6 files changed

+111
-17
lines changed

6 files changed

+111
-17
lines changed

function-aws-api-proxy-test/src/main/java/io/micronaut/function/aws/proxy/test/DefaultServletToAwsProxyResponseAdapter.java

+4-11
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import io.micronaut.core.annotation.NonNull;
2020
import io.micronaut.core.annotation.Nullable;
2121
import io.micronaut.core.convert.ConversionService;
22-
import io.micronaut.function.aws.proxy.MutableMapListOfStringAndMapStringConvertibleMultiValue;
2322
import io.micronaut.http.HttpMethod;
2423

2524
import jakarta.inject.Singleton;
@@ -32,7 +31,6 @@
3231
import java.nio.charset.Charset;
3332
import java.nio.charset.StandardCharsets;
3433
import java.util.Base64;
35-
import java.util.List;
3634
import java.util.Map;
3735

3836
/**
@@ -47,7 +45,7 @@ public void handle(@NonNull ConversionService conversionService,
4745
@NonNull HttpServletRequest request,
4846
@NonNull APIGatewayV2HTTPResponse awsProxyResponse,
4947
@NonNull HttpServletResponse response) throws IOException {
50-
populateHeaders(conversionService, awsProxyResponse, response);
48+
populateHeaders(awsProxyResponse, response);
5149
response.setStatus(awsProxyResponse.getStatusCode());
5250
HttpMethod httpMethod = HttpMethod.parse(request.getMethod());
5351
if (httpMethod != HttpMethod.HEAD && httpMethod != HttpMethod.OPTIONS) {
@@ -65,15 +63,10 @@ public void handle(@NonNull ConversionService conversionService,
6563
}
6664
}
6765

68-
private void populateHeaders(@NonNull ConversionService conversionService,
69-
@NonNull APIGatewayV2HTTPResponse apiGatewayV2HTTPResponse,
66+
private void populateHeaders(@NonNull APIGatewayV2HTTPResponse apiGatewayV2HTTPResponse,
7067
@NonNull HttpServletResponse response) {
71-
Map<String, String> singleHeaders = apiGatewayV2HTTPResponse.getHeaders();
72-
Map<String, List<String>> multiValueHeaders = apiGatewayV2HTTPResponse.getMultiValueHeaders();
73-
MutableMapListOfStringAndMapStringConvertibleMultiValue entries = new MutableMapListOfStringAndMapStringConvertibleMultiValue(conversionService, multiValueHeaders, singleHeaders);
74-
75-
for (String name: entries.names()) {
76-
response.addHeader(name, String.join(",", entries.getAll(name)));
68+
for (Map.Entry<String, String> entry : apiGatewayV2HTTPResponse.getHeaders().entrySet()) {
69+
response.addHeader(entry.getKey(), entry.getValue());
7770
}
7871
}
7972

function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/MapCollapseUtils.java

+28-3
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616
package io.micronaut.function.aws.proxy;
1717

1818
import io.micronaut.core.annotation.Internal;
19+
import io.micronaut.core.annotation.NonNull;
20+
import io.micronaut.core.annotation.Nullable;
1921
import io.micronaut.core.util.CollectionUtils;
22+
import io.micronaut.http.HttpHeaders;
2023
import io.micronaut.http.MutableHttpHeaders;
2124

25+
import java.util.Arrays;
2226
import java.util.ArrayList;
2327
import java.util.Collections;
2428
import java.util.HashMap;
@@ -31,6 +35,8 @@
3135
@Internal
3236
public final class MapCollapseUtils {
3337

38+
private static final List<String> HEADERS_ALLOWING_COMMAS = Arrays.asList(HttpHeaders.DATE, HttpHeaders.USER_AGENT);
39+
3440
private MapCollapseUtils() {
3541
}
3642

@@ -81,9 +87,20 @@ public static Map<String, List<String>> collapse(Map<String, List<String>> multi
8187
}
8288
if (CollectionUtils.isNotEmpty(single)) {
8389
for (var entry: single.entrySet()) {
84-
List<String> strings = values.computeIfAbsent(entry.getKey(), s -> new ArrayList<>());
85-
if (!strings.contains(entry.getValue())) {
86-
strings.add(entry.getValue());
90+
String headerName = entry.getKey();
91+
List<String> strings = values.computeIfAbsent(headerName, s -> new ArrayList<>());
92+
String value = entry.getValue();
93+
if (HEADERS_ALLOWING_COMMAS.contains(headerName)) {
94+
if (!strings.contains(value)) {
95+
strings.add(value);
96+
}
97+
} else {
98+
for (String v : splitCommaSeparatedValue(value)) {
99+
v = v.trim();
100+
if (!strings.contains(v)) {
101+
strings.add(v);
102+
}
103+
}
87104
}
88105
}
89106
}
@@ -103,4 +120,12 @@ public static Map<String, String> collapse(Map<String, List<String>> input) {
103120
}
104121
return result;
105122
}
123+
124+
@NonNull
125+
private static List<String> splitCommaSeparatedValue(@Nullable String value) {
126+
if (value == null) {
127+
return Collections.emptyList();
128+
}
129+
return Arrays.asList(value.split(","));
130+
}
106131
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package io.micronaut.function.aws.proxy
2+
3+
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
4+
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent
5+
import io.micronaut.core.convert.ConversionService
6+
import io.micronaut.http.CaseInsensitiveMutableHttpHeaders
7+
import io.micronaut.http.HttpHeaders
8+
import io.micronaut.http.MutableHttpHeaders
9+
import io.micronaut.test.extensions.spock.annotation.MicronautTest
10+
import jakarta.inject.Inject
11+
import spock.lang.Specification
12+
13+
@MicronautTest
14+
class MapCollapseUtilsSpec extends Specification {
15+
16+
@Inject
17+
ConversionService conversionService
18+
19+
/**
20+
*
21+
* GET /prod/ HTTP/1.1
22+
* Key: value1,value2,value3
23+
* Foo: Bar
24+
*/
25+
void "payload v2 example"() {
26+
given:
27+
APIGatewayV2HTTPEvent event = new APIGatewayV2HTTPEvent()
28+
Map<String, String> headers = new HashMap<>();
29+
headers.put(HttpHeaders.DATE, "Wed, 21 Oct 2015 07:28:00 GMT")
30+
headers.put("foo", "Bar")
31+
headers.put("key", "value1,value2,value3")
32+
headers.put(HttpHeaders.USER_AGENT, "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)")
33+
event.setHeaders(headers)
34+
35+
when:
36+
MutableHttpHeaders httpHeaders = new CaseInsensitiveMutableHttpHeaders(MapCollapseUtils.collapse(Collections.emptyMap(), event.getHeaders()), conversionService);
37+
38+
then:
39+
noExceptionThrown()
40+
41+
and:
42+
"Bar" == httpHeaders.get("Foo")
43+
["value1", "value2", "value3"] == httpHeaders.getAll("Key")
44+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)" == httpHeaders.get(HttpHeaders.USER_AGENT)
45+
"Wed, 21 Oct 2015 07:28:00 GMT" == httpHeaders.get(HttpHeaders.DATE)
46+
}
47+
48+
/**
49+
*
50+
* GET /prod/ HTTP/1.1
51+
* Key: value1,value2,value3
52+
* Foo: Bar
53+
*/
54+
void "payload v1 example"() {
55+
given:
56+
APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent()
57+
Map<String, String> headers = new HashMap<>()
58+
headers.put("Foo", "Bar")
59+
headers.put("Key", "value1,value2,value3")
60+
request.setHeaders(headers)
61+
Map<String, List<String>> multiValueHeaders = new HashMap<>()
62+
multiValueHeaders.put("Key", Arrays.asList("value1", "value2", "value3"))
63+
multiValueHeaders.put("Foo", Collections.singletonList("Bar"))
64+
request.setMultiValueHeaders(multiValueHeaders)
65+
66+
when:
67+
MutableHttpHeaders httpHeaders = new CaseInsensitiveMutableHttpHeaders(MapCollapseUtils.collapse(request.getMultiValueHeaders(), request.getHeaders()), conversionService);
68+
69+
then:
70+
noExceptionThrown()
71+
72+
and:
73+
"Bar" == httpHeaders.get("Foo")
74+
["value1", "value2", "value3"] == httpHeaders.getAll("Key")
75+
76+
}
77+
}

test-suite-http-server-tck-function-aws-api-gateway-proxy-payloadv2/src/test/java/io/micronaut/http/server/tck/lambda/APIGatewayV2HTTPEventFactory.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
*/
3434
@Internal
3535
public final class APIGatewayV2HTTPEventFactory {
36+
private final static String COMMA = ",";
3637

3738
private APIGatewayV2HTTPEventFactory() {
3839
}
@@ -44,7 +45,7 @@ public static APIGatewayV2HTTPEvent create(HttpRequest<?> request, JsonMapper js
4445
public Map<String, String> getHeaders() {
4546
Map<String, String> result = new HashMap<>();
4647
for (String headerName : request.getHeaders().names()) {
47-
result.put(headerName, request.getHeaders().get(headerName));
48+
result.put(headerName, String.join(COMMA, request.getHeaders().getAll(headerName)));
4849
}
4950
return result;
5051
}

test-suite-http-server-tck-function-aws-api-gateway-proxy-payloadv2/src/test/java/io/micronaut/http/server/tck/lambda/tests/FunctionAwsApiGatewayProxyV2HttpServerTestSuite.java

-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
"io.micronaut.http.server.tck.lambda.tests"
1212
})
1313
@ExcludeClassNamePatterns({
14-
"io.micronaut.http.server.tck.tests.HeadersTest", // https://github.com/micronaut-projects/micronaut-aws/issues/1861
1514
"io.micronaut.http.server.tck.tests.FilterProxyTest" // Immmutable request
1615
})
1716
@SuiteDisplayName("HTTP Server TCK for Function AWS API Gateway Proxy v2 Event model")

test-suite-http-server-tck-function-aws-api-proxy-test/src/test/java/io/micronaut/http/server/tck/lambda/tests/MicronautLambdaHandlerHttpServerTestSuite.java

-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
"io.micronaut.http.server.tck.lambda.tests"
1212
})
1313
@ExcludeClassNamePatterns({
14-
"io.micronaut.http.server.tck.tests.HeadersTest", // https://github.com/micronaut-projects/micronaut-aws/issues/1861
1514
"io.micronaut.http.server.tck.tests.LocalErrorReadingBodyTest", // Binding body different type (e.g. a String in error handler)
1615
"io.micronaut.http.server.tck.tests.FilterProxyTest" // Immmutable request
1716

0 commit comments

Comments
 (0)