Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(options): allow to control headers casing #246

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
Expand All @@ -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.
Expand Down Expand Up @@ -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 `*`)
Expand Down
3 changes: 2 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export declare interface Options {
compression?: boolean;
headers?: object;
s3Config?: S3ClientConfig;
lowercaseHeaderKeys?: boolean;
}

export declare class Request {
Expand Down Expand Up @@ -200,7 +201,7 @@ export declare class Response {

header(key: string, value?: string | Array<string>, append?: boolean): this;

getHeader(key: string): string;
getHeader(key: string, asArr?: boolean): string;

hasHeader(key: string): boolean;

Expand Down
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
65 changes: 43 additions & 22 deletions lib/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
}

Expand All @@ -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
Expand Down Expand Up @@ -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 = '';
Expand All @@ -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];
}
}

Expand Down Expand Up @@ -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;
}
}
}
Expand Down