Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Fixed
- Request header keys are now normalized to lowercase, so headers sent in camelCase over HTTP/1 (e.g. `x-janis-Page`) are read consistently by every API instead of falling back to defaults (JV2-158)

## [8.1.0] - 2024-08-20
### Changed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ The following methods will be inherited from the base API Class:
Returns the path parameters of the request. For example: /store/10/schedules will generate the following path parameters: ['10']

- **headers**. *object*.
Returns the the headers of the request as a key-value object.
Returns the the headers of the request as a key-value object. Header keys are normalized to lowercase (HTTP header names are case-insensitive per RFC 7230), so you can always read them in lowercase regardless of the casing sent by the client (e.g. `this.headers['x-janis-page']`).

- **cookies**. *object*.
Returns the the cookies of the request as a key-value object.
Expand Down
7 changes: 5 additions & 2 deletions lib/dispatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const { ApiSession } = require('@janiscommerce/api-session');

const cloneObj = require('lodash.clonedeep');
const Fetcher = require('./fetcher');
const { isObject, trimObjectValues } = require('./utils');
const { isObject, trimObjectValues, lowerCaseKeys } = require('./utils');

const API = require('./api');
const APIError = require('./error');
Expand Down Expand Up @@ -41,7 +41,10 @@ module.exports = class Dispatcher {
this.pristineData = request.data;
Object.freeze(this.pristineData);
this.rawData = request.rawData;
this.headers = request.headers || {};
// Normalize header keys to lowercase. HTTP header names are case-insensitive (RFC 7230) and HTTP/2
// forces them to lowercase, but over HTTP/1 a consumer may send them in any case (e.g. 'x-janis-Page').
// Normalizing here lets every API read headers consistently regardless of the casing received.
this.headers = lowerCaseKeys(request.headers || {});
this.cookies = request.cookies || {};
this.authenticationData = request.authenticationData || {};
this.executionStarted = Date.now();
Expand Down
22 changes: 21 additions & 1 deletion lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,25 @@ const omitRecursive = (object, pathPatterns) => {
return clonedObject;
};

/**
* Returns a new object with all the top-level keys lowercased.
* Useful to normalize HTTP header names, which are case-insensitive (RFC 7230),
* so they can be read consistently regardless of the casing sent by the consumer.
*
* @param {object} object - The input object
* @returns {object} A new object with its keys lowercased
*/
const lowerCaseKeys = object => {

if(!isObject(object))
return object;

return Object.entries(object).reduce((normalized, [key, value]) => {
normalized[key.toLowerCase()] = value;
return normalized;
}, {});
};

const trimObjectValues = value => {

if(typeof value !== 'object' || !value)
Expand All @@ -141,5 +160,6 @@ const trimObjectValues = value => {
module.exports = {
isObject,
omitRecursive,
trimObjectValues
trimObjectValues,
lowerCaseKeys
};
20 changes: 20 additions & 0 deletions tests/dispatcher-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,26 @@ describe('Dispatcher', () => {
}, 200);
});

it('Should normalize header keys to lowercase regardless of the casing received', async function() {

extraProcess = api => {
assert.deepStrictEqual(api.headers, {
'x-janis-page': '3',
'x-janis-page-size': '20',
'my-header': 'foo'
});
};

await test({
endpoint: 'api/valid-endpoint',
headers: {
'x-janis-Page': '3',
'X-Janis-Page-Size': '20',
'My-Header': 'foo'
}
}, 200);
});

it('Should response with a custom HTTP Code when given', async function() {

httpCode = 201;
Expand Down
29 changes: 29 additions & 0 deletions tests/utils-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';

const assert = require('assert');

const { lowerCaseKeys } = require('../lib/utils');

describe('Utils', () => {

describe('lowerCaseKeys()', () => {

it('Should lowercase every top-level key', () => {
assert.deepStrictEqual(lowerCaseKeys({
'x-janis-Page': '3',
'X-Janis-Page-Size': '20',
'My-Header': 'foo'
}), {
'x-janis-page': '3',
'x-janis-page-size': '20',
'my-header': 'foo'
});
});

it('Should return the value as-is when it is not an object', () => {
[null, undefined, 'string', 1, true, ['a', 'b']].forEach(value => {
assert.deepStrictEqual(lowerCaseKeys(value), value);
});
});
});
});
Loading