diff --git a/README.md b/README.md index a08c981..4bd869d 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,8 @@ Require the `lambda-api` module into your Lambda handler script and instantiate | errorHeaderWhitelist | `Array` | Array of headers to maintain on errors | | s3Config | `Object` | Optional object to provide as config to S3 sdk. [S3ClientConfig](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/interfaces/s3clientconfig.html) | +| lowercaseHeaderKeys | `Boolean` | Decide whether to lowercase all header names and values. This allows you to decide whether to compile with the [http/2 spec](https://www.rfc-editor.org/rfc/rfc9113#name-http-fields). + ```javascript // Require the framework and instantiate it with optional version and base parameters const api = require('lambda-api')({ version: 'v1.0', base: 'v1' }); @@ -163,6 +165,16 @@ const api = require('lambda-api')({ version: 'v1.0', base: 'v1' }); For detailed release notes see [Releases](https://github.com/jeremydaly/lambda-api/releases). +# v1.0.3: allow to control header keys behavior + +In the past, by default, we normalized all headers to be lowercased based on [the http/2 spec](https://www.rfc-editor.org/rfc/rfc9113#name-http-fields). +This has caused issues for some of our consumers, therefore we're adding a new API option called `lowercaseHeaderKeys`. +By default it's set to true, in order to not break the already existing implementation. + +# v1.0: move to AWS-SDK v3 + +Lambda API is now using AWS SDK v3. In case you're still using AWS SDK v2, please use a lambda-api version that is lower than 1.0. + ### v0.11: API Gateway v2 payload support and automatic compression Lambda API now supports API Gateway v2 payloads for use with HTTP APIs. The library automatically detects the payload, so no extra configuration is needed. Automatic [compression](#compression) has also been added and supports Brotli, Gzip and Deflate. @@ -656,6 +668,8 @@ api.get('/redirectToS3File', (req, res) => { Convenience method for adding [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) headers to responses. An optional `options` object can be passed in to customize the defaults. +NOTE: in order to properly allow CORS for all browsers when using http/1, please set `lowercaseHeaderKeys` option to `false`. + The six defined **CORS** headers are as follows: - Access-Control-Allow-Origin (defaults to `*`) diff --git a/index.d.ts b/index.d.ts index 0d7a523..c9014d0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -134,6 +134,7 @@ export declare interface Options { compression?: boolean; headers?: object; s3Config?: S3ClientConfig; + lowercaseHeaderKeys?: boolean; } export declare class Request { @@ -200,7 +201,7 @@ export declare class Response { header(key: string, value?: string | Array, append?: boolean): this; - getHeader(key: string): string; + getHeader(key: string, asArr?: boolean): string; hasHeader(key: string): boolean; diff --git a/index.js b/index.js index cdd9b43..35af322 100644 --- a/index.js +++ b/index.js @@ -16,6 +16,8 @@ const { ConfigurationError } = require('./lib/errors'); class API { constructor(props) { this._version = props && props.version ? props.version : 'v1'; + this._lowercaseHeaders = + props && props._lowercaseHeaders ? props._lowercaseHeaders : true; this._base = props && props.base && typeof props.base === 'string' ? props.base.trim() diff --git a/lib/response.js b/lib/response.js index 3b1bf04..f53bc35 100644 --- a/lib/response.js +++ b/lib/response.js @@ -68,8 +68,8 @@ class RESPONSE { } // Adds a header field - header(key, value, append) { - let _key = key.toLowerCase(); // store as lowercase + header(key, value, append = false) { + let _key = this._normalizeHeaderKey(key); let _values = value ? (Array.isArray(value) ? value : [value]) : ['']; this._headers[_key] = append ? this.hasHeader(_key) @@ -81,18 +81,23 @@ class RESPONSE { // Gets a header field getHeader(key, asArr) { - if (!key) + let _key = this._normalizeHeaderKey(key); + if (!_key) return asArr ? this._headers - : Object.keys(this._headers).reduce( - (headers, key) => - Object.assign(headers, { [key]: this._headers[key].toString() }), - {} - ); // return all headers + : Object.keys(this._headers).reduce((headers, childKkey) => { + let _childKey = this._normalizeHeaderKey(childKkey); + + return { + ...headers, + [_childKey]: this._headers[_childKey].toString(), + }; + }, {}); + return asArr - ? this._headers[key.toLowerCase()] - : this._headers[key.toLowerCase()] - ? this._headers[key.toLowerCase()].toString() + ? this._headers[_key] + : this._headers[_key] + ? this._headers[_key].toString() : undefined; } @@ -102,13 +107,19 @@ class RESPONSE { // Removes a header field removeHeader(key) { - delete this._headers[key.toLowerCase()]; + let _key = this._normalizeHeaderKey(key); + delete this._headers[_key]; return this; } // Returns boolean if header exists hasHeader(key) { - return this.getHeader(key ? key : '') !== undefined; + let _key = this._normalizeHeaderKey(key); + return this.getHeader(_key || '') !== undefined; + } + + _normalizeHeaderKey(key = '') { + return this.app._lowercaseHeaders ? key.toLowerCase() : key; } // Convenience method for JSON @@ -508,16 +519,20 @@ class RESPONSE { if ( this._etag && // if etag support enabled ['GET', 'HEAD'].includes(this._request.method) && - !this.hasHeader('etag') && + !this.hasHeader('ETag') && this._statusCode === 200 ) { - this.header('etag', '"' + UTILS.generateEtag(body) + '"'); + this.header('ETag', '"' + UTILS.generateEtag(body) + '"'); } + const etagHeaderKey = this._normalizeHeaderKey('ETag'); + const ifNoneMatchHeaderKey = this._normalizeHeaderKey('If-None-Match'); + // Check for matching Etag if ( - this._request.headers['if-none-match'] && - this._request.headers['if-none-match'] === this.getHeader('etag') + this._request.headers[ifNoneMatchHeaderKey] && + this._request.headers[ifNoneMatchHeaderKey] === + this.getHeader(etagHeaderKey) ) { this.status(304); body = ''; @@ -527,9 +542,11 @@ class RESPONSE { let cookies = {}; if (this._request.payloadVersion === '2.0') { - if (this._headers['set-cookie']) { - cookies = { cookies: this._headers['set-cookie'] }; - delete this._headers['set-cookie']; + const setCookieHeaderKey = this._normalizeHeaderKey('set-cookie'); + + if (this._headers[setCookieHeaderKey]) { + cookies = { cookies: this._headers[setCookieHeaderKey] }; + delete this._headers[setCookieHeaderKey]; } } @@ -573,12 +590,16 @@ class RESPONSE { body: data.toString('base64'), isBase64Encoded: true, }); + + const contentEncodingHeaderKey = + this._normalizeHeaderKey('content-encoding'); + if (this._response.multiValueHeaders) { - this._response.multiValueHeaders['content-encoding'] = [ + this._response.multiValueHeaders[contentEncodingHeaderKey] = [ contentEncoding, ]; } else { - this._response.headers['content-encoding'] = contentEncoding; + this._response.headers[contentEncodingHeaderKey] = contentEncoding; } } }