27

I'm using API Gateway's Proxy integration to call a Lambda. The output format specification is this follow JSON format:

{
  "statusCode": httpStatusCode,
  "headers": { "headerName": "headerValue", ... },
  "body": "..."
}

In one response I wish to set two cookies (two different auth cookies) but JSON doesn't allow having two identical keys in the headers object (OK, technically the spec does but most libraries do not).

RFC 7230 notes that Set-Cookie should be handled specially but I can't see how I can send multiple Set-Cookie values through API gateway.

Does anyone know whether this is possible?

Community
  • 1
  • 1
sihil
  • 2,563
  • 1
  • 17
  • 24

6 Answers6

26

Note: API gateway now has a version 2 payload that is fundamentally different from that described here (and the default for new APIs). Documentation on the differences here. See other accepted answer from Samuel for some more details.

As of November 2018 this is possible using the multiValueHeaders field in the response instead of headers (see announcement).

As an example instead of:

{
  "statusCode": 200,
  "body": "testing multiple set-cookie headers",
  "headers": {
    "X-Test-Header": "baking experiment",
    "Set-Cookie": "cookie1=chocolate-chip",
    "Set-Cookie": "cookie2=oatmeal",
    "Content-Type": "text/plain"
  }
}

You can respond with:

{
  "statusCode": 200,
  "body": "testing multiple set-cookie headers",
  "multiValueHeaders": {
    "X-Test-Header": ["baking experiment"],
    "Set-Cookie": ["cookie1=chocolate-chip", "cookie2=oatmeal"],
    "Content-Type": ["text/plain"]
  }
}

Note that you can use a mix of headers and multiValueHeaders:

{
  "statusCode": 200,
  "body": "testing multiple set-cookie headers",
  "headers": {
    "X-Test-Header": "baking experiment",
    "Content-Type": "text/plain"
  },
  "multiValueHeaders": {
    "Set-Cookie": ["cookie1=chocolate-chip", "cookie2=oatmeal"]
  }
}

However using the same header in both will mean that the value under headers is dropped.

See the documentation for more details.

When using only the header field (as available prior to Nov 2018) I tried sending the following manually curated JSON as a response:

{
  "statusCode": 200,
  "body": "testing multiple set-cookie headers",
  "headers": {
    "X-Test-Header": "baking experiment",
    "Set-Cookie": "cookie1=chocolate-chip",
    "Set-Cookie": "cookie2=oatmeal",
    "Content-Type": "text/plain"
  }
}

The cookies that API gateway returns in response to a CURL request are:

< Content-Type: text/plain
< Content-Length: 35
< Connection: keep-alive
< Date: Thu, 29 Sep 2016 11:22:09 GMT
< Set-Cookie: cookie2=oatmeal
< X-Test-Header: baking experiment
< X-Cache: Miss from cloudfront

As you can see the first Set-Cookie is dropped on the floor.

sihil
  • 2,563
  • 1
  • 17
  • 24
  • @MarkB that is not true, [see rfc6265](https://tools.ietf.org/html/rfc6265#page-7). You need to be able to respond with multiple `Set-Cookie` headers because each cookie could have different levels of cookie security, expirations, etc. – idbehold Sep 29 '16 at 17:37
  • I think ideally AWS would instead utilize the [the `Headers` interface of the Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Headers). A `Headers` instance allows you to `headers.append(key, value)` meaning that you can set the `Set-Cookie` header multiple times. Then you could invoke the Lambda callback with the `headers` property equal to either a plain object *or* a `Headers` instance. – idbehold Sep 29 '16 at 17:53
10

Please note that in 09/2022 AWS Api Gateway by default uses a new payload format (version 2.0) that no longer recognizes multiValueHeaders as proposed by all the top answers, including the accepted answer. I just spent 40 minutes trying to figure out why it doesn't return my cookies :).

From AWS Documentation:

Format 2.0 doesn't have multiValueHeaders or multiValueQueryStringParameters fields. Duplicate headers are combined with commas and included in the headers field. Duplicate query strings are combined with commas and included in the queryStringParameters field.

Format 2.0 includes a new cookies field. All cookie headers in the request are combined with commas and added to the cookies field. In the response to the client, each cookie becomes a set-cookie header.

https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html

So the actual solution should now be:

    return {
      statusCode: 302,
      headers: {
        Location: location,
      },
      cookies: [
         "cookie1=value1;",
         "cookie2=value2;"
      ]
    };
Samuel
  • 2,430
  • 5
  • 31
  • 41
  • Thanks @Samuel - I've accepted this and made a tweak to the top answer signposting this. – sihil Sep 03 '22 at 13:53
  • Best answer for current SDK version – Levidps Dec 14 '22 at 12:26
  • 1
    Hello, I tried this out and it works for HTTP API, but I'm having trouble getting it to work for REST API. Do I have to add a Mapping Template specifically for "cookies"? – rickyc May 02 '23 at 02:11
7

As answered, to date, API Gateway will drop identical keys, only setting one of the cookies.

However, a workaround exists. You can change the casing of the string 'Set-Cookie' so the keys are not unique. For example, you could use the keys set-cookie, Set-cookie, sEt-cookie, and the headers will be preserved and 3 different cookies would be set.

Because the RFC standard makes headers case-insensitive this should work with all RFC-compliant clients.

So, you could rewrite your set-cookie headers, permuting all the possible casings of "Set-Cookie" to get around this.

This technique (hack) is employed by Zappa, a popular serverless framework written in Python.

sytech
  • 29,298
  • 3
  • 45
  • 86
  • 2
    Only just seen this and obviously now fixed properly, but whoever thought this hack up deserves a unicorn. Brilliant. Thanks for bringing it to my attention @sytech – sihil Sep 29 '20 at 12:53
4

Use multiValueHeaders:

response.multiValueHeaders = {
  "Set-Cookie": [
    'cookie1=value1',
    'cookie1=value1'
  ]
}

or:

{
    "statusCode": httpStatusCode,
    "headers": { "headerName": "headerValue", ... },
    "multiValueHeaders": { "headerName": ["headerValue", "headerValue2",...], ... },
    "body": "..."
}

https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format

Hugodby
  • 1,124
  • 9
  • 24
3

As Mark B pointed out, you can/should achieve this by setting multiple cookie name/value pairs in a single Set-Cookie header. The browser should interpret this correctly.

Cookie: a=1; b=2

Edit: as pointed out by OP, there are use cases that require multiple instances of the header. We've added it our backlog along with supporting multiple header names on incoming requests.

jackko
  • 6,998
  • 26
  • 38
  • 2
    [See rfc6265](https://tools.ietf.org/html/rfc6265#page-7). You need to be able to respond with multiple `Set-Cookie` headers because each cookie could have different levels of cookie security, expirations, etc. – idbehold Sep 29 '16 at 17:47
  • I see, good point. I'll have to add this to our backlog; it may take a while if we have to change the shape coming back from Lambda and accept both formats. – jackko Sep 29 '16 at 18:01
  • The suggested `Set-Cookie` isn't a valid use of the header. You may only set one cookie per header (unlike `Cookie` sent from the browser, which works exactly as you say above). Everything after the first `;` is interpreted as options such as expiry, max-age etc. See the syntax in [the RFC](https://tools.ietf.org/html/rfc6265#section-4.1.1). – sihil Sep 29 '16 at 21:00
  • I've just tried this with Chrome. When Chrome receives a headers of `Set-Cookie": "cookie1=chocolate-chip; cookie2=oatmeal`, Chrome sets `cookie1=chocolate-chip` and discards the invalid attribute - which follows the RFC as far as I can tell. Your update won't work at all - `Cookie` headers are sent form the browser to the server rather than from the server to the browser (sending all accumulated cookies back to the server on each method call). – sihil Sep 30 '16 at 09:34
  • @JackKohn-AWS, wouldn't it be as simple as allowing either a string (which is the current syntax) or an array of strings. Sounds backward compatible to me. – Arlen Beiler Aug 29 '17 at 02:21
  • 3
    hi, what is the status of this now? – supersan Nov 04 '17 at 10:44
0

Couple of years late, but I just required to implement something like this and this is how I was able to make it work:

...

//15 minutes
var expirationTime = new Date(new Date().getTime() + 15 * 60 * 1000);
//30 minutes
var expirationTime2 = new Date(new Date().getTime() + 30 * 60 * 1000);

var response = {};

var cookies = [];
cookies.push("testCookie={'keyX':'valx', 'keyy':'valy'}; Expires=" + expirationTime + ";");
cookies.push("testCookie2={'key1':'val1', 'key2':'val2'}; Expires=" + expirationTime2 + ";");

response.headers["Set-Cookie"] =  cookies;

...

Each array item will be processed independently, so you can add as many cookies to the array with different settings.

i.e.

cookies.push("testCookie3={'key1':'val1', 'key2':'val2'}; Expires=" + expirationTime2 + "; Max-Age=...");

cookies.push("testCookie4={'key1':'val1', 'key2':'val2'}; Expires=" + expirationTime2 + "; Domain=<domain-value>; Path=<path-value>");
Rolo
  • 3,208
  • 1
  • 24
  • 25