Skip to content

Commit ee24617

Browse files
committed
Additional unit tests. Also added a configuration parameter to specify whether multiple Set-Cookie headers should be consolidated into a single header - RFC2109 is not support in all clients but API Gateway can only return a single header per key. These changes try to address concerns in issue #51
1 parent 8ee206e commit ee24617

File tree

6 files changed

+258
-8
lines changed

6 files changed

+258
-8
lines changed

aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/model/ContainerConfig.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33

44
/**
5-
* Configuration paramters used by the <code>RequestReader</code> and <code>ResponseWriter</code> objects.
5+
* Configuration parameters for the framework
66
*/
77
public class ContainerConfig {
88
public static final String DEFAULT_URI_ENCODING = "UTF-8";
@@ -11,6 +11,7 @@ public static ContainerConfig defaultConfig() {
1111
ContainerConfig configuration = new ContainerConfig();
1212
configuration.setStripBasePath(false);
1313
configuration.setUriEncoding(DEFAULT_URI_ENCODING);
14+
configuration.setConsolidateSetCookieHeaders(true);
1415

1516
return configuration;
1617
}
@@ -22,6 +23,7 @@ public static ContainerConfig defaultConfig() {
2223
private String serviceBasePath;
2324
private boolean stripBasePath;
2425
private String uriEncoding;
26+
private boolean consolidateSetCookieHeaders;
2527

2628

2729
//-------------------------------------------------------------
@@ -33,6 +35,11 @@ public String getServiceBasePath() {
3335
}
3436

3537

38+
/**
39+
* Configures a base path that can be strippped from the request path before passing it to the frameowkr-specific implementation. This can be used to
40+
* remove API Gateway's base path mappings from the request.
41+
* @param serviceBasePath The base path mapping to be removed.
42+
*/
3643
public void setServiceBasePath(String serviceBasePath) {
3744
// clean up base path before setting it, we want a "/" at the beginning but not at the end.
3845
String finalBasePath = serviceBasePath;
@@ -51,6 +58,11 @@ public boolean isStripBasePath() {
5158
}
5259

5360

61+
/**
62+
* Whether this framework should strip the base path mapping specified with the {@link #setServiceBasePath(String)} method from a request before
63+
* passing it to the framework-specific implementations
64+
* @param stripBasePath
65+
*/
5466
public void setStripBasePath(boolean stripBasePath) {
5567
this.stripBasePath = stripBasePath;
5668
}
@@ -61,7 +73,30 @@ public String getUriEncoding() {
6173
}
6274

6375

76+
/**
77+
* Sets the charset used to URLEncode and Decode request paths.
78+
* @param uriEncoding The charset. By default this is set to UTF-8
79+
*/
6480
public void setUriEncoding(String uriEncoding) {
6581
this.uriEncoding = uriEncoding;
6682
}
83+
84+
85+
public boolean isConsolidateSetCookieHeaders() {
86+
return consolidateSetCookieHeaders;
87+
}
88+
89+
90+
/**
91+
* Tells the library to consolidate multiple Set-Cookie headers into a single Set-Cookie header with multiple, comma-separated values. This is allowed
92+
* by the RFC 2109 (https://tools.ietf.org/html/rfc2109). However, since not all clients support this, we consider it optional. When this value is set
93+
* to true the framework will consolidate all Set-Cookie headers into a single header, when it's set to false, the framework will only return the first
94+
* Set-Cookie header specified in a response.
95+
*
96+
* Because API Gateway needs header keys to be unique, we give an option to configure this.
97+
* @param consolidateSetCookieHeaders Whether to consolidate the cookie headers or not.
98+
*/
99+
public void setConsolidateSetCookieHeaders(boolean consolidateSetCookieHeaders) {
100+
this.consolidateSetCookieHeaders = consolidateSetCookieHeaders;
101+
}
67102
}

aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletResponse.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
*/
1313
package com.amazonaws.serverless.proxy.internal.servlet;
1414

15+
import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler;
16+
1517
import org.slf4j.Logger;
1618
import org.slf4j.LoggerFactory;
1719

@@ -410,7 +412,22 @@ byte[] getAwsResponseBodyBytes() {
410412
Map<String, String> getAwsResponseHeaders() {
411413
Map<String, String> responseHeaders = new HashMap<>();
412414
for (String header : getHeaderNames()) {
413-
responseHeaders.put(header, headers.getFirst(header));
415+
// special behavior for set cookie
416+
// RFC 2109 allows for a comma separated list of cookies in one Set-Cookie header: https://tools.ietf.org/html/rfc2109
417+
if (header.equals(HttpHeaders.SET_COOKIE) && LambdaContainerHandler.getContainerConfig().isConsolidateSetCookieHeaders()) {
418+
StringBuilder cookieHeader = new StringBuilder();
419+
for (String cookieValue : headers.get(header)) {
420+
if (cookieHeader.length() > 0) {
421+
cookieHeader.append(",");
422+
}
423+
424+
cookieHeader.append(" ").append(cookieValue);
425+
}
426+
427+
responseHeaders.put(header, cookieHeader.toString());
428+
} else {
429+
responseHeaders.put(header, headers.getFirst(header));
430+
}
414431
}
415432

416433
return responseHeaders;

aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyResponseWriter.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,14 @@
1313
package com.amazonaws.serverless.proxy.jersey;
1414

1515

16+
import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler;
17+
1618
import org.glassfish.jersey.server.ContainerException;
1719
import org.glassfish.jersey.server.ContainerResponse;
1820
import org.glassfish.jersey.server.spi.ContainerResponseWriter;
1921

22+
import javax.ws.rs.core.HttpHeaders;
23+
2024
import java.io.ByteArrayOutputStream;
2125
import java.io.OutputStream;
2226
import java.util.HashMap;
@@ -79,7 +83,18 @@ public OutputStream writeResponseStatusAndHeaders(long contentLength, ContainerR
7983

8084
for (final Map.Entry<String, List<String>> e : containerResponse.getStringHeaders().entrySet()) {
8185
for (final String value : e.getValue()) {
82-
headers.put(e.getKey(), value);
86+
// special case for set cookies
87+
// RFC 2109 allows for a comma separated list of cookies in one Set-Cookie header: https://tools.ietf.org/html/rfc2109
88+
if (e.getKey().equals(HttpHeaders.SET_COOKIE)) {
89+
if (headers.containsKey(e.getKey()) && LambdaContainerHandler.getContainerConfig().isConsolidateSetCookieHeaders()) {
90+
headers.put(e.getKey(), headers.get(e.getKey()) + ", " + value);
91+
} else {
92+
headers.put(e.getKey(), containerResponse.getStringHeaders().getFirst(e.getKey()));
93+
break;
94+
}
95+
} else {
96+
headers.put(e.getKey(), value);
97+
}
8398
}
8499
}
85100

aws-serverless-java-container-spark/src/test/java/com/amazonaws/serverless/proxy/spark/HelloWorldSparkTest.java

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder;
88
import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext;
99

10+
import org.junit.BeforeClass;
1011
import org.junit.Test;
1112

13+
import javax.servlet.http.Cookie;
14+
import javax.ws.rs.core.HttpHeaders;
15+
1216
import static org.junit.Assert.*;
1317
import static spark.Spark.get;
1418

@@ -18,10 +22,15 @@ public class HelloWorldSparkTest {
1822
private static final String CUSTOM_HEADER_VALUE = "My Header Value";
1923
private static final String BODY_TEXT_RESPONSE = "Hello World";
2024

25+
public static final String COOKIE_NAME = "MyCookie";
26+
public static final String COOKIE_VALUE = "CookieValue";
27+
public static final String COOKIE_DOMAIN = "mydomain.com";
28+
public static final String COOKIE_PATH = "/";
29+
2130
private static SparkLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;
2231

23-
@Test
24-
public void basicServer_initialize() {
32+
@BeforeClass
33+
public static void initializeServer() {
2534
try {
2635
handler = SparkLambdaContainerHandler.getAwsProxyHandler();
2736

@@ -44,11 +53,60 @@ public void basicServer_handleRequest_emptyFilters() {
4453
assertEquals(BODY_TEXT_RESPONSE, response.getBody());
4554
}
4655

47-
private void configureRoutes() {
56+
@Test
57+
public void addCookie_setCookieOnResponse_validCustomCookie() {
58+
AwsProxyRequest req = new AwsProxyRequestBuilder().method("GET").path("/cookie").build();
59+
AwsProxyResponse response = handler.proxy(req, new MockLambdaContext());
60+
61+
assertEquals(200, response.getStatusCode());
62+
assertTrue(response.getHeaders().containsKey(HttpHeaders.SET_COOKIE));
63+
assertTrue(response.getHeaders().get(HttpHeaders.SET_COOKIE).contains(COOKIE_NAME + "=" + COOKIE_VALUE));
64+
assertTrue(response.getHeaders().get(HttpHeaders.SET_COOKIE).contains(COOKIE_DOMAIN));
65+
assertTrue(response.getHeaders().get(HttpHeaders.SET_COOKIE).contains(COOKIE_PATH));
66+
}
67+
68+
@Test
69+
public void multiCookie_setCookieOnResponse_singleHeaderWithMultipleValues() {
70+
AwsProxyRequest req = new AwsProxyRequestBuilder().method("GET").path("/multi-cookie").build();
71+
AwsProxyResponse response = handler.proxy(req, new MockLambdaContext());
72+
73+
System.out.println("Cookie: " + response.getHeaders().get(HttpHeaders.SET_COOKIE));
74+
75+
assertEquals(200, response.getStatusCode());
76+
assertTrue(response.getHeaders().containsKey(HttpHeaders.SET_COOKIE));
77+
// we compare against 4 because the expiration date of the cookies will also contain a string
78+
assertEquals(4, response.getHeaders().get(HttpHeaders.SET_COOKIE).split(",").length);
79+
assertTrue(response.getHeaders().get(HttpHeaders.SET_COOKIE).contains(COOKIE_NAME + "=" + COOKIE_VALUE));
80+
assertTrue(response.getHeaders().get(HttpHeaders.SET_COOKIE).contains(COOKIE_NAME + "2=" + COOKIE_VALUE + "2"));
81+
assertTrue(response.getHeaders().get(HttpHeaders.SET_COOKIE).contains(COOKIE_DOMAIN));
82+
assertTrue(response.getHeaders().get(HttpHeaders.SET_COOKIE).contains(COOKIE_PATH));
83+
}
84+
85+
private static void configureRoutes() {
4886
get("/hello", (req, res) -> {
4987
res.status(200);
5088
res.header(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE);
5189
return BODY_TEXT_RESPONSE;
5290
});
91+
92+
get("/cookie", (req, res) -> {
93+
Cookie testCookie = new Cookie(COOKIE_NAME, COOKIE_VALUE);
94+
testCookie.setDomain(COOKIE_DOMAIN);
95+
testCookie.setPath(COOKIE_PATH);
96+
res.raw().addCookie(testCookie);
97+
return BODY_TEXT_RESPONSE;
98+
});
99+
100+
get("/multi-cookie", (req, res) -> {
101+
Cookie testCookie = new Cookie(COOKIE_NAME, COOKIE_VALUE);
102+
testCookie.setDomain(COOKIE_DOMAIN);
103+
testCookie.setPath(COOKIE_PATH);
104+
Cookie testCookie2 = new Cookie(COOKIE_NAME + "2", COOKIE_VALUE + "2");
105+
testCookie2.setDomain(COOKIE_DOMAIN);
106+
testCookie2.setPath(COOKIE_PATH);
107+
res.raw().addCookie(testCookie);
108+
res.raw().addCookie(testCookie2);
109+
return BODY_TEXT_RESPONSE;
110+
});
53111
}
54112
}

aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringServletContextTest.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77
import com.amazonaws.serverless.proxy.internal.servlet.AwsServletContext;
88
import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder;
99
import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext;
10+
import com.amazonaws.serverless.proxy.spring.echoapp.ContextResource;
1011
import com.amazonaws.serverless.proxy.spring.echoapp.CustomHeaderFilter;
1112
import com.amazonaws.serverless.proxy.spring.echoapp.EchoSpringAppConfig;
1213
import com.amazonaws.serverless.proxy.spring.echoapp.model.ValidatedUserModel;
1314
import org.junit.BeforeClass;
1415
import org.junit.Test;
16+
import org.springframework.http.HttpHeaders;
1517
import org.springframework.http.HttpStatus;
18+
import org.springframework.http.MediaType;
1619

1720
import javax.servlet.DispatcherType;
1821
import javax.servlet.FilterRegistration;
@@ -97,5 +100,34 @@ public void filter_validationFilter_emptyName() {
97100
AwsProxyResponse output = handler.proxy(request, lambdaContext);
98101
assertEquals(HttpStatus.BAD_REQUEST.value(), output.getStatusCode());
99102
}
103+
104+
@Test
105+
public void exception_populatedException_annotationValuesMappedCorrectly() {
106+
AwsProxyRequest request = new AwsProxyRequestBuilder("/context/exception", "GET")
107+
.stage(STAGE)
108+
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
109+
.build();
110+
111+
AwsProxyResponse output = handler.proxy(request, lambdaContext);
112+
113+
assertEquals(409, output.getStatusCode());
114+
assertTrue(output.getBody().contains(ContextResource.EXCEPTION_REASON));
115+
}
116+
117+
@Test
118+
public void cookie_injectInResponse_expectCustomSetCookie() {
119+
AwsProxyRequest request = new AwsProxyRequestBuilder("/context/cookie", "GET")
120+
.stage(STAGE)
121+
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
122+
.build();
123+
124+
AwsProxyResponse output = handler.proxy(request, lambdaContext);
125+
126+
127+
assertEquals(200, output.getStatusCode());
128+
assertTrue(output.getHeaders().containsKey(HttpHeaders.SET_COOKIE));
129+
assertTrue(output.getHeaders().get(HttpHeaders.SET_COOKIE).contains(ContextResource.COOKIE_NAME + "=" + ContextResource.COOKIE_VALUE));
130+
assertTrue(output.getHeaders().get(HttpHeaders.SET_COOKIE).contains(ContextResource.COOKIE_DOMAIN));
131+
}
100132
}
101133

0 commit comments

Comments
 (0)