Skip to content

Commit

Permalink
Release 2.6.0. Support for multiValueHeaders in the RouterResponse.…
Browse files Browse the repository at this point in the history
… Fixed setting multiple cookies at once in the response.
  • Loading branch information
jeff committed Apr 16, 2020
1 parent 1c6b1e1 commit 9104b91
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 26 deletions.
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cassava",
"version": "2.5.2",
"version": "2.6.0",
"description": "AWS API Gateway Router",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
10 changes: 10 additions & 0 deletions src/ProxyResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ export interface ProxyResponse {
*/
headers: { [key: string]: string };

/**
* Headers to set on the response. Can contain multi-value headers
* as well as single-value headers.
*
* If you specify values for both headers and multiValueHeaders, API Gateway
* merges them into a single list. If the same key-value pair is specified in
* both, only the values from multiValueHeaders will appear in the merged list.
*/
multiValueHeaders: { [header: string]: string[] };

/**
* The string representation of the JSON to respond with.
*/
Expand Down
80 changes: 80 additions & 0 deletions src/Router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,86 @@ describe("Router", () => {
});
});

describe("response header handling", () => {
it("turns a cookie string into a Set-Cookie header", async () => {
const router = new cassava.Router();
router.route("/foo")
.handler(async evt => {
return {
body: {},
cookies: {
"foo": "bar"
}
};
});

const resp = await testRouter(router, createTestProxyEvent("/foo", "GET"));

chai.assert.deepEqual(resp.headers, {
"Content-Type": "application/json",
"Set-Cookie": "foo=bar"
});
});

it("turns a cookie object with options into a Set-Cookie header", async () => {
const router = new cassava.Router();
router.route("/foo")
.handler(async evt => {
return {
body: {},
cookies: {
"foo": {
value: "bar",
options: {
httpOnly: true,
maxAge: 600
}
}
}
};
});

const resp = await testRouter(router, createTestProxyEvent("/foo", "GET"));

chai.assert.deepEqual(resp.headers, {
"Content-Type": "application/json",
"Set-Cookie": "foo=bar; Max-Age=600; HttpOnly"
});
});

it("supports multiple cookies", async () => {
const router = new cassava.Router();
router.route("/foo")
.handler(async evt => {
return {
body: {},
cookies: {
"foo": {
value: "bar",
options: {
httpOnly: true,
maxAge: 600
}
},
"baz": "qux"
}
};
});

const resp = await testRouter(router, createTestProxyEvent("/foo", "GET"));

chai.assert.deepEqual(resp.headers, {
"Content-Type": "application/json"
});
chai.assert.deepEqual(resp.multiValueHeaders, {
"Set-Cookie": [
"foo=bar; Max-Age=600; HttpOnly",
"baz=qux"
]
})
});
});

describe("error handling", () => {
describe("RestError handling", () => {
it("RestErrors thrown from handle() are returned", async () => {
Expand Down
12 changes: 3 additions & 9 deletions src/Router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,17 +244,10 @@ export class Router {

private routerResponseToProxyResponse(resp: RouterResponse): ProxyResponse {
if (resp.cookies) {
const cookieKeys = Object.keys(resp.cookies);
for (let i = 0, length = cookieKeys.length; i < length; i++) {
const key = cookieKeys[i];
for (const key of Object.keys(resp.cookies)) {
const value = resp.cookies[key];
const cookieString = typeof value === "string" ? cookieLib.serialize(key, value) : cookieLib.serialize(key, value.value, value.options);
const setCookie = RouterResponse.getHeader(resp, "Set-Cookie");
if (setCookie) {
RouterResponse.setHeader(resp, "Set-Cookie", `${setCookie}; ${cookieString}`);
} else {
RouterResponse.setHeader(resp, "Set-Cookie", cookieString);
}
RouterResponse.setHeader(resp, "Set-Cookie", cookieString);
}
}

Expand All @@ -275,6 +268,7 @@ export class Router {
return {
statusCode: resp.statusCode || httpStatusCode.success.OK,
headers: resp.headers || {},
multiValueHeaders: resp.multiValueHeaders || {},
body,
isBase64Encoded
};
Expand Down
86 changes: 86 additions & 0 deletions src/RouterResponse.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import * as chai from "chai";
import {RouterResponse} from "./RouterResponse";

describe("RouterResponse", () => {
describe("getHeader()", () => {
it("gets the header from the response object, case insensitive", () => {
const resp: RouterResponse = {
statusCode: 200,
body: {},
headers: {
"UpperCase": "correct",
"lowercase": "lazy",
"ALL-CAPS": "why are you screaming?"
}
};

chai.assert.equal(RouterResponse.getHeader(resp, "UpperCase"), "correct");
chai.assert.equal(RouterResponse.getHeader(resp, "uppercase"), "correct");

chai.assert.equal(RouterResponse.getHeader(resp, "LowerCase"), "lazy");
chai.assert.equal(RouterResponse.getHeader(resp, "lowercase"), "lazy");

chai.assert.equal(RouterResponse.getHeader(resp, "ALL-CAPS"), "why are you screaming?");
chai.assert.equal(RouterResponse.getHeader(resp, "All-Caps"), "why are you screaming?");
chai.assert.equal(RouterResponse.getHeader(resp, "all-caps"), "why are you screaming?");
});

it("supports multiValueHeaders", () => {
const resp: RouterResponse = {
statusCode: 200,
body: {},
headers: {
"Single-Only": "in headers",
"In-Both": "don't use this one"
},
multiValueHeaders: {
"In-Both": ["use this one"],
"Multiple-Values": ["alpha", "bravo"],
"Array-Of-One": ["one"]
}
};

chai.assert.equal(RouterResponse.getHeader(resp, "Single-Only"), "in headers");

chai.assert.equal(RouterResponse.getHeader(resp, "In-Both"), "use this one");
chai.assert.equal(RouterResponse.getHeader(resp, "in-both"), "use this one");

chai.assert.deepEqual(RouterResponse.getHeader(resp, "Multiple-Values"), ["alpha", "bravo"]);
chai.assert.deepEqual(RouterResponse.getHeader(resp, "multiple-values"), ["alpha", "bravo"]);

chai.assert.equal(RouterResponse.getHeader(resp, "Array-Of-One"), "one");
chai.assert.equal(RouterResponse.getHeader(resp, "array-of-one"), "one");
});
});

describe("setHeader()", () => {
it("sets the header if headers is empty", () => {
const resp: RouterResponse = {
statusCode: 200,
body: {}
};
RouterResponse.setHeader(resp, "Foo", "bar");
RouterResponse.setHeader(resp, "Baz", "qux");

chai.assert.deepEqual(resp.headers, {"Foo": "bar", "Baz": "qux"})
});

it("can set multi-value headers", () => {
const resp: RouterResponse = {
statusCode: 200,
body: {}
};

RouterResponse.setHeader(resp, "Foo", "bar");
chai.assert.deepEqual(resp.headers, {"Foo": "bar"});

RouterResponse.setHeader(resp, "Foo", "baz");
chai.assert.deepEqual(resp.headers, {});
chai.assert.deepEqual(resp.multiValueHeaders, {"Foo": ["bar", "baz"]});

RouterResponse.setHeader(resp, "Foo", "qux");
chai.assert.deepEqual(resp.headers, {});
chai.assert.deepEqual(resp.multiValueHeaders, {"Foo": ["bar", "baz", "qux"]});
});
});
});
55 changes: 47 additions & 8 deletions src/RouterResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ export interface RouterResponse {
*/
headers?: { [key: string]: string };

/**
* Optional headers to set on the response but allowing multiple
* values.
*/
multiValueHeaders?: { [header: string]: string[] };

/**
* Optional cookies to set on the response.
*/
Expand All @@ -34,34 +40,67 @@ export interface RouterResponseCookie {
}

export namespace RouterResponse {
export function getHeader(resp: RouterResponse, field: string): string | null {
if (!resp.headers) {
return null;
export function getHeader(resp: RouterResponse, field: string): string | string[] | null {
const fieldLower = field.toLowerCase();

if (resp.multiValueHeaders) {
for (const k of Object.keys(resp.multiValueHeaders)) {
if (k.toLowerCase() === fieldLower) {
if (resp.multiValueHeaders[k].length === 1) {
return resp.multiValueHeaders[k][0];
} else {
return resp.multiValueHeaders[k];
}
}
}
}

const fieldLower = field.toLowerCase();
for (const k of Object.keys(resp.headers)) {
if (k.toLowerCase() === fieldLower) {
return resp.headers[k];
if (resp.headers) {
for (const k of Object.keys(resp.headers)) {
if (k.toLowerCase() === fieldLower) {
return resp.headers[k];
}
}
}

return null;
}

export function setHeader(resp: RouterResponse, field: string, value: string): void {
if (resp.multiValueHeaders?.[field]) {
setMultiValueHeader(resp, field, [value]);
return;
}

if (!resp.headers) {
resp.headers = {};
}

const fieldLower = field.toLowerCase();
for (const k of Object.keys(resp.headers)) {
if (k.toLowerCase() === fieldLower) {
resp.headers[k] = value;
setMultiValueHeader(resp, field, [resp.headers[k], value]);
delete resp.headers[k];
return;
}
}

resp.headers[field] = value;
}

function setMultiValueHeader(resp: RouterResponse, field: string, value: string[]): void {
if (!resp.multiValueHeaders) {
resp.multiValueHeaders = {};
}

const fieldLower = field.toLowerCase();
for (const k of Object.keys(resp.multiValueHeaders)) {
if (k.toLowerCase() === fieldLower) {
resp.multiValueHeaders[k].push(...value);
return;
}
}

resp.multiValueHeaders[field] = value;
}
}

0 comments on commit 9104b91

Please sign in to comment.