From c4b6eb450c1d7c7601b738db3a7ab15b1e30664d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Ormaechea?= Date: Fri, 26 Jun 2026 14:31:36 -0300 Subject: [PATCH 1/2] [JV2-158] Normalize request header keys to lowercase HTTP header names are case-insensitive (RFC 7230). Over HTTP/2 they are forced to lowercase, but over HTTP/1 a consumer may send them in any case (e.g. 'x-janis-Page'), causing APIs that read headers in lowercase to miss them and fall back to defaults (e.g. an infinite pagination loop in api-list). Normalize header keys to lowercase in the dispatcher so every API reads them consistently regardless of the casing received. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 2 ++ README.md | 2 +- lib/dispatcher.js | 7 +++++-- lib/utils.js | 22 +++++++++++++++++++++- tests/dispatcher-test.js | 20 ++++++++++++++++++++ tests/utils-test.js | 29 +++++++++++++++++++++++++++++ 6 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 tests/utils-test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 8abea4e..40ac822 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 7287594..b694546 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/lib/dispatcher.js b/lib/dispatcher.js index ebe44f3..2e9511e 100644 --- a/lib/dispatcher.js +++ b/lib/dispatcher.js @@ -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'); @@ -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(); diff --git a/lib/utils.js b/lib/utils.js index f3493c6..427a1b0 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -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) @@ -141,5 +160,6 @@ const trimObjectValues = value => { module.exports = { isObject, omitRecursive, - trimObjectValues + trimObjectValues, + lowerCaseKeys }; diff --git a/tests/dispatcher-test.js b/tests/dispatcher-test.js index 6f0eb31..ed3aab1 100644 --- a/tests/dispatcher-test.js +++ b/tests/dispatcher-test.js @@ -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; diff --git a/tests/utils-test.js b/tests/utils-test.js new file mode 100644 index 0000000..4318544 --- /dev/null +++ b/tests/utils-test.js @@ -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); + }); + }); + }); +}); From cc12cc29ebeaf327adfae0371d98ea5c0566a65f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Ormaechea?= Date: Mon, 29 Jun 2026 16:26:50 -0300 Subject: [PATCH 2/2] Release 8.1.1 Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 4 +++- package.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40ac822..709b304 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,10 @@ 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] + +## [8.1.1] - 2026-06-29 ### 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) +- 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 ## [8.1.0] - 2024-08-20 ### Changed diff --git a/package.json b/package.json index 4626164..7939d2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@janiscommerce/api", - "version": "8.1.0", + "version": "8.1.1", "description": "A package for managing API from any origin", "main": "lib/index.js", "scripts": {