Skip to content

Commit

Permalink
close #73 with multi-value header support and test updates
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremydaly committed Dec 24, 2018
1 parent 446044b commit c2cfe6b
Show file tree
Hide file tree
Showing 26 changed files with 796 additions and 695 deletions.
30 changes: 24 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,11 @@ Whatever you decide is best for your use case, **Lambda API** is there to suppor
- [download()](#downloadfile--filename--options--callback)
- [error()](#errorcode-message-detail)
- [etag()](#etagboolean)
- [getHeader()](#getheaderkey)
- [getHeader()](#getheaderkey-value--asarray)
- [getHeaders()](#getheaders)
- [getLink()](#getlinks3path-expires-callback)
- [hasHeader()](#hasheaderkey)
- [header()](#headerkey-value)
- [header()](#headerkey-value--append)
- [html()](#htmlbody)
- [json()](#jsonbody)
- [jsonp()](#jsonpbody)
Expand Down Expand Up @@ -429,19 +430,36 @@ api.get('/users', (req,res) => {
})
```

### header(key, value)
The `header` method allows for you to set additional headers to return to the client. By default, just the `content-type` header is sent with `application/json` as the value. Headers can be added or overwritten by calling the `header()` method with two string arguments. The first is the name of the header and then second is the value.
### header(key, value [,append])
The `header` method allows for you to set additional headers to return to the client. By default, just the `content-type` header is sent with `application/json` as the value. Headers can be added or overwritten by calling the `header()` method with two string arguments. The first is the name of the header and then second is the value. You can utilize multi-value headers by specifying an array with multiple values as the `value`, or you can use an optional third boolean parameter and append multiple headers.

```javascript
api.get('/users', (req,res) => {
res.header('content-type','text/html').send('<div>This is HTML</div>')
})

// Set multiple header values
api.get('/users', (req,res) => {
res.header('someHeader',['foo','bar').send({})
})

// Set multiple header by adding to existing header
api.get('/users', (req,res) => {
res.header('someHeader','foo')
.header('someHeader','bar',true) // append another value
.send({})
})
```

**NOTE:** Header keys are converted and stored as lowercase in compliance with [rfc7540 8.1.2. HTTP Header Fields](https://tools.ietf.org/html/rfc7540) for HTTP/2. Header convenience methods (`getHeader`, `hasHeader`, and `removeHeader`) automatically ignore case.

### getHeader([key])
Retrieve the current header object or pass the optional `key` parameter and retrieve a specific header value. `key` is case insensitive.
### getHeader(key [,asArray])
Retrieve a specific header value. `key` is case insensitive. By default (and for backwards compatibility), header values are returned as a `string`. Multi-value headers will be concatenated using a comma (see [rfc2616 4.2. Message Headers](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2)). An optional second boolean parameter can be passed to return header values as an `array`.

**NOTE:** The ability to retrieve the current header object by calling `getHeader()` is still possible, but the preferred method is to use the `getHeaders()` method. By default, `getHeader()` will return the object with `string` values.

### getHeaders()
Retrieve the current header object. Values are returned as `array`s.

### hasHeader(key)
Returns a boolean indicating the existence of `key` in the response headers. `key` is case insensitive.
Expand Down
42 changes: 28 additions & 14 deletions lib/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class RESPONSE {
// Default the header
this._headers = {
// Set the Content-Type by default
'content-type': 'application/json' //charset=UTF-8
'content-type': ['application/json'] //charset=UTF-8
}

// base64 encoding flag
Expand All @@ -64,17 +64,28 @@ class RESPONSE {
}

// Adds a header field
header(key,value) {
header(key,value,append) {
let _key = key.toLowerCase() // store as lowercase
value = value !== undefined ? value : '' // default value
this._headers[_key] = value // set
let _values = value ? (Array.isArray(value) ? value : [value]) : ['']
this._headers[_key] = append ?
this.hasHeader(_key) ? this._headers[_key].concat(_values) : _values
: _values
return this
}

// Gets a header field
getHeader(key) {
if (!key) return this._headers // return all headers
return this._headers[key.toLowerCase()]
getHeader(key,asArr) {
if (!key) return asArr ? this._headers :
Object.keys(this._headers).reduce((headers,key) =>
Object.assign(headers, { [key]: this._headers[key].toString() })
,{}) // return all headers
return asArr ? this._headers[key.toLowerCase()]
: this._headers[key.toLowerCase()] ?
this._headers[key.toLowerCase()].toString() : undefined
}

getHeaders() {
return this._headers
}

// Removes a header field
Expand Down Expand Up @@ -202,7 +213,7 @@ class RESPONSE {
(opts.sameSite === false ? 'Lax' : opts.sameSite ))
: ''

this.header('Set-Cookie',cookieString)
this.header('Set-Cookie',cookieString,true)
return this
}

Expand Down Expand Up @@ -440,12 +451,15 @@ class RESPONSE {
}

// Create the response
this._response = {
headers: this._headers,
statusCode: this._statusCode,
body: this._request.method === 'HEAD' ? '' : UTILS.encodeBody(body,this._serializer),
isBase64Encoded: this._isBase64
}
this._response = Object.assign({},
this._request._multiValueSupport ? { multiValueHeaders: this._headers }
: { headers: UTILS.stringifyHeaders(this._headers) },
{
statusCode: this._statusCode,
body: this._request.method === 'HEAD' ? '' : UTILS.encodeBody(body,this._serializer),
isBase64Encoded: this._isBase64
}
)

// Trigger the callback function
this.app._callback(null, this._response, this)
Expand Down
10 changes: 10 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,13 @@ exports.deepMerge = (a,b) => {
// Concats values from an array to ',' separated string
exports.fromArray = val =>
val && val instanceof Array ? val.toString() : undefined

// Stringify multi-value headers
exports.stringifyHeaders = headers =>
Object.keys(headers)
.reduce((acc,key) =>
Object.assign(acc,{
// set-cookie cannot be concatenated with a comma
[key]: key === 'set-cookie' ? headers[key].slice(-1)[0] : headers[key].toString()
})
,{})
18 changes: 9 additions & 9 deletions test/attachments.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ let event = {
httpMethod: 'get',
path: '/',
body: {},
headers: {
'Content-Type': 'application/json'
multiValueHeaders: {
'Content-Type': ['application/json']
}
}

Expand Down Expand Up @@ -59,43 +59,43 @@ describe('Attachment Tests:', function() {
it('Simple attachment', async function() {
let _event = Object.assign({},event,{ path: '/attachment' })
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({ headers: { 'content-disposition': 'attachment', 'content-type': 'application/json' }, statusCode: 200, body: '{"status":"ok"}', isBase64Encoded: false })
expect(result).to.deep.equal({ multiValueHeaders: { 'content-disposition': ['attachment'], 'content-type': ['application/json'] }, statusCode: 200, body: '{"status":"ok"}', isBase64Encoded: false })
}) // end it

it('PDF attachment w/ path', async function() {
let _event = Object.assign({},event,{ path: '/attachment/pdf' })
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({ headers: { 'content-disposition': 'attachment; filename=\"foo.pdf\"', 'content-type': 'application/pdf' }, statusCode: 200, body: 'filedata', isBase64Encoded: false })
expect(result).to.deep.equal({ multiValueHeaders: { 'content-disposition': ['attachment; filename=\"foo.pdf\"'], 'content-type': ['application/pdf'] }, statusCode: 200, body: 'filedata', isBase64Encoded: false })
}) // end it

it('PNG attachment w/ path', async function() {
let _event = Object.assign({},event,{ path: '/attachment/png' })
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({ headers: { 'content-disposition': 'attachment; filename=\"foo.png\"', 'content-type': 'image/png' }, statusCode: 200, body: 'filedata', isBase64Encoded: false })
expect(result).to.deep.equal({ multiValueHeaders: { 'content-disposition': ['attachment; filename=\"foo.png\"'], 'content-type': ['image/png'] }, statusCode: 200, body: 'filedata', isBase64Encoded: false })
}) // end it

it('CSV attachment w/ path', async function() {
let _event = Object.assign({},event,{ path: '/attachment/csv' })
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({ headers: { 'content-disposition': 'attachment; filename=\"foo.csv\"', 'content-type': 'text/csv' }, statusCode: 200, body: 'filedata', isBase64Encoded: false })
expect(result).to.deep.equal({ multiValueHeaders: { 'content-disposition': ['attachment; filename=\"foo.csv\"'], 'content-type': ['text/csv'] }, statusCode: 200, body: 'filedata', isBase64Encoded: false })
}) // end it

it('Custom MIME type attachment w/ path', async function() {
let _event = Object.assign({},event,{ path: '/attachment/custom' })
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({ headers: { 'content-disposition': 'attachment; filename=\"foo.test\"', 'content-type': 'text/test' }, statusCode: 200, body: 'filedata', isBase64Encoded: false })
expect(result).to.deep.equal({ multiValueHeaders: { 'content-disposition': ['attachment; filename=\"foo.test\"'], 'content-type': ['text/test'] }, statusCode: 200, body: 'filedata', isBase64Encoded: false })
}) // end it

it('Empty string', async function() {
let _event = Object.assign({},event,{ path: '/attachment/empty-string' })
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({ headers: { 'content-disposition': 'attachment', 'content-type': 'application/json' }, statusCode: 200, body: 'filedata', isBase64Encoded: false })
expect(result).to.deep.equal({ multiValueHeaders: { 'content-disposition': ['attachment'], 'content-type': ['application/json'] }, statusCode: 200, body: 'filedata', isBase64Encoded: false })
}) // end it

it('Null string', async function() {
let _event = Object.assign({},event,{ path: '/attachment/empty-string' })
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({ headers: { 'content-disposition': 'attachment', 'content-type': 'application/json' }, statusCode: 200, body: 'filedata', isBase64Encoded: false })
expect(result).to.deep.equal({ multiValueHeaders: { 'content-disposition': ['attachment'], 'content-type': ['application/json'] }, statusCode: 200, body: 'filedata', isBase64Encoded: false })
}) // end it

}) // end HEADER tests
10 changes: 5 additions & 5 deletions test/basePath.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ let event = {
httpMethod: 'get',
path: '/test',
body: {},
headers: {
'Content-Type': 'application/json'
multiValueHeaders: {
'Content-Type': ['application/json']
}
}

Expand Down Expand Up @@ -43,19 +43,19 @@ describe('Base Path Tests:', function() {
it('Simple path with base: /v1/test', async function() {
let _event = Object.assign({},event,{ path: '/v1/test' })
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({ headers: { 'content-type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok"}', isBase64Encoded: false })
expect(result).to.deep.equal({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","status":"ok"}', isBase64Encoded: false })
}) // end it

it('Path with base and parameter: /v1/test/123', async function() {
let _event = Object.assign({},event,{ path: '/v1/test/123' })
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({ headers: { 'content-type': 'application/json' }, statusCode: 200, body: '{"method":"get","status":"ok","param":"123"}', isBase64Encoded: false })
expect(result).to.deep.equal({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","status":"ok","param":"123"}', isBase64Encoded: false })
}) // end it

it('Nested path with base: /v1/test/test2/test3', async function() {
let _event = Object.assign({},event,{ path: '/v1/test/test2/test3' })
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({ headers: { 'content-type': 'application/json' }, statusCode: 200, body: '{"path":"/v1/test/test2/test3","method":"get","status":"ok"}', isBase64Encoded: false })
expect(result).to.deep.equal({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"path":"/v1/test/test2/test3","method":"get","status":"ok"}', isBase64Encoded: false })
}) // end it

}) // end BASEPATH tests
80 changes: 40 additions & 40 deletions test/cacheControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ let event = {
httpMethod: 'get',
path: '/test',
body: {},
headers: {
'Content-Type': 'application/json'
multiValueHeaders: {
'Content-Type': ['application/json']
}
}

Expand Down Expand Up @@ -75,10 +75,10 @@ describe('cacheControl Tests:', function() {
let _event = Object.assign({},event,{ path: '/cache' })
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({
headers: {
'content-type': 'application/json',
'cache-control': 'max-age=0',
'expires': result.headers.expires
multiValueHeaders: {
'content-type': ['application/json'],
'cache-control': ['max-age=0'],
'expires': result.multiValueHeaders.expires
},
statusCode: 200,
body: 'cache',
Expand All @@ -90,10 +90,10 @@ describe('cacheControl Tests:', function() {
let _event = Object.assign({},event,{ path: '/cacheTrue' })
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({
headers: {
'content-type': 'application/json',
'cache-control': 'max-age=0',
'expires': result.headers.expires
multiValueHeaders: {
'content-type': ['application/json'],
'cache-control': ['max-age=0'],
'expires': result.multiValueHeaders.expires
},
statusCode: 200,
body: 'cache',
Expand All @@ -105,9 +105,9 @@ describe('cacheControl Tests:', function() {
let _event = Object.assign({},event,{ path: '/cacheFalse' })
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({
headers: {
'content-type': 'application/json',
'cache-control': 'no-cache, no-store, must-revalidate'
multiValueHeaders: {
'content-type': ['application/json'],
'cache-control': ['no-cache, no-store, must-revalidate']
},
statusCode: 200,
body: 'cache',
Expand All @@ -119,10 +119,10 @@ describe('cacheControl Tests:', function() {
let _event = Object.assign({},event,{ path: '/cacheMaxAge' })
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({
headers: {
'content-type': 'application/json',
'cache-control': 'max-age=1',
'expires': result.headers.expires
multiValueHeaders: {
'content-type': ['application/json'],
'cache-control': ['max-age=1'],
'expires': result.multiValueHeaders.expires
},
statusCode: 200,
body: 'cache',
Expand All @@ -134,10 +134,10 @@ describe('cacheControl Tests:', function() {
let _event = Object.assign({},event,{ path: '/cachePrivate' })
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({
headers: {
'content-type': 'application/json',
'cache-control': 'private, max-age=1',
'expires': result.headers.expires
multiValueHeaders: {
'content-type': ['application/json'],
'cache-control': ['private, max-age=1'],
'expires': result.multiValueHeaders.expires
},
statusCode: 200,
body: 'cache',
Expand All @@ -149,10 +149,10 @@ describe('cacheControl Tests:', function() {
let _event = Object.assign({},event,{ path: '/cachePrivateFalse' })
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({
headers: {
'content-type': 'application/json',
'cache-control': 'max-age=1',
'expires': result.headers.expires
multiValueHeaders: {
'content-type': ['application/json'],
'cache-control': ['max-age=1'],
'expires': result.multiValueHeaders.expires
},
statusCode: 200,
body: 'cache',
Expand All @@ -164,10 +164,10 @@ describe('cacheControl Tests:', function() {
let _event = Object.assign({},event,{ path: '/cachePrivateInvalid' })
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({
headers: {
'content-type': 'application/json',
'cache-control': 'max-age=1',
'expires': result.headers.expires
multiValueHeaders: {
'content-type': ['application/json'],
'cache-control': ['max-age=1'],
'expires': result.multiValueHeaders.expires
},
statusCode: 200,
body: 'cache',
Expand All @@ -179,10 +179,10 @@ describe('cacheControl Tests:', function() {
let _event = Object.assign({},event,{ path: '/cacheCustomUndefined' })
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({
headers: {
'content-type': 'application/json',
'cache-control': 'max-age=0',
'expires': result.headers.expires
multiValueHeaders: {
'content-type': ['application/json'],
'cache-control': ['max-age=0'],
'expires': result.multiValueHeaders.expires
},
statusCode: 200,
body: 'cache',
Expand All @@ -194,10 +194,10 @@ describe('cacheControl Tests:', function() {
let _event = Object.assign({},event,{ path: '/cacheCustomNull' })
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({
headers: {
'content-type': 'application/json',
'cache-control': 'max-age=0',
'expires': result.headers.expires
multiValueHeaders: {
'content-type': ['application/json'],
'cache-control': ['max-age=0'],
'expires': result.multiValueHeaders.expires
},
statusCode: 200,
body: 'cache',
Expand All @@ -209,9 +209,9 @@ describe('cacheControl Tests:', function() {
let _event = Object.assign({},event,{ path: '/cacheCustom' })
let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) }))
expect(result).to.deep.equal({
headers: {
'content-type': 'application/json',
'cache-control': 'custom value'
multiValueHeaders: {
'content-type': ['application/json'],
'cache-control': ['custom value']
},
statusCode: 200,
body: 'cache',
Expand Down
Loading

0 comments on commit c2cfe6b

Please sign in to comment.