-
Notifications
You must be signed in to change notification settings - Fork 0
feat: manage bad requests with 'allow request methods' package #1325
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
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
905db6c
feat: manage bad requests with 'allow request methods' package
CyntiBinti 1424931
test: unit tests added for full coverage
CyntiBinti 6fe1f01
chore: refactor code and declare import type for module
CyntiBinti 00436e7
chore: make request method not optional param
CyntiBinti File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
CHANGELOG.md | ||
docs | ||
test |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
|
||
# @dotcom-reliability-kit/middleware-allow-request-methods | ||
|
||
Express middleware that returns 405 (rather than 404) for disallowed request methods. This module is part of [FT.com Reliability Kit](https://github.com/Financial-Times/dotcom-reliability-kit#readme). | ||
|
||
* [Usage](#usage) | ||
* [Configuration options](#configuration-options) | ||
* [`options.allowedMethods`](#optionsallowedmethods) | ||
* [Migrating](#migrating) | ||
* [Contributing](#contributing) | ||
* [License](#license) | ||
|
||
## Usage | ||
|
||
Install `@dotcom-reliability-kit/middleware-allow-request-methods` as a dependency: | ||
|
||
```bash | ||
npm install --save @dotcom-reliability-kit/middleware-allow-request-methods | ||
``` | ||
|
||
Include in your code: | ||
|
||
```js | ||
import { allowRequestMethods } from '@dotcom-reliability-kit/middleware-allow-request-methods'; | ||
// or | ||
const { allowRequestMethods } = require('@dotcom-reliability-kit/middleware-allow-request-methods'); | ||
``` | ||
|
||
We recommend always using this middleware globally with app.use as a first middleware in your app. This is because, if a bad actor is making requests to your app to find attack vectors, you throw their request out as early as possible. | ||
|
||
Route-specific blocking of methods is an additional layer of protection you can explore. It may be that your app does support POST requests for a form but the main view is GET only. You can filter out further junk requests on a per-route basis by using the app.route('...').all() method or use with a path. | ||
|
||
Example usage: | ||
|
||
```js | ||
const express = require('express'); | ||
const { allowRequestMethods } = require('@dotcom-reliability-kit/middleware-allow-request-methods'); | ||
|
||
const app = express(); | ||
|
||
// Allow only certain request methods for the entire app. If you're | ||
// doing this, it must be above ALL routes you want it to apply to: | ||
app.use(allowRequestMethods({ allowedMethods: ['GET', 'HEAD', 'POST'] })); | ||
|
||
// Allow only certain request methods for a specific route, e.g. here | ||
// we only allow `GET` and `HEAD` methods for the home page. Note that | ||
// we have to use `all` for the allowed methods here THEN define the get | ||
// request handler: | ||
app | ||
.route('/') | ||
.all(allowRequestMethods({ allowedMethods: ['GET', 'HEAD'] })) | ||
.get((request, response) => { | ||
response.send('Homepage'); | ||
}); | ||
|
||
// You can also allow methods for a subset of routes. Remember that this | ||
// applies for all routes that START with the value. E.g. the following | ||
// will also only allow POST requests on `/submit/example`: | ||
app.use('/submit', allowRequestMethods({ allowedMethods: ['POST'] })); | ||
|
||
app.post('/submit', (request, response) => { | ||
response.send('Form submitted'); | ||
}); | ||
|
||
app.listen(3000, () => console.log('Server running on port 3000')); | ||
``` | ||
|
||
### Configuration options | ||
|
||
Config options can be passed into the `allowRequestMethods` function as an object with any of the keys below. | ||
|
||
```js | ||
app.use(allowRequestMethods({ | ||
// Config options go here | ||
})); | ||
``` | ||
|
||
#### `options.allowedMethods` | ||
|
||
An array of HTTP methods that are allowed for the route. This must be an `Array` of `String`s, with each string being an HTTP method. It's important that you do not include methods which are not supported by the route. | ||
|
||
This option defaults to `[]`. | ||
|
||
## Contributing | ||
|
||
See the [central contributing guide for Reliability Kit](https://github.com/Financial-Times/dotcom-reliability-kit/blob/main/docs/contributing.md). | ||
|
||
## License | ||
|
||
Licensed under the [MIT](https://github.com/Financial-Times/dotcom-reliability-kit/blob/main/LICENSE) license.<br/> | ||
Copyright © 2025, The Financial Times Ltd. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
const { UserInputError } = require('@dotcom-reliability-kit/errors'); | ||
|
||
/** | ||
* @import { RequestMethodOptions } from '@dotcom-reliability-kit/middleware-allow-request-methods' | ||
*/ | ||
|
||
/** | ||
* @import { RequestHandler } from 'express' | ||
*/ | ||
|
||
/** | ||
* Create a middleware function to return 405 (rather than 404) for disallowed request methods. | ||
* | ||
* @param {RequestMethodOptions} options | ||
* @returns {RequestHandler} - Returns an Express middleware function. | ||
*/ | ||
function allowRequestMethods(options = { allowedMethods: [] }) { | ||
// Check if allowed methods have been specified and are valid | ||
const allowedMethodsSpecified = options?.allowedMethods; | ||
if ( | ||
!Array.isArray(allowedMethodsSpecified) || | ||
allowedMethodsSpecified.length === 0 || | ||
allowedMethodsSpecified.every((method) => typeof method !== 'string') | ||
) { | ||
throw new TypeError( | ||
'The `allowedMethods` option must be an array of strings' | ||
); | ||
} | ||
|
||
const normalisedAllowedRequestMethods = normaliseAllowedRequestMethods( | ||
allowedMethodsSpecified | ||
); | ||
|
||
return function allowRequestMethodsMiddleware(request, response, next) { | ||
// We can't set the Allow header if headers have already been sent, otherwise the middleware will error | ||
if (!response.headersSent) { | ||
response.header('Allow', normalisedAllowedRequestMethods.join(', ')); | ||
} | ||
|
||
// If the incoming request method is not in the allowed methods array, then send a 405 error | ||
if (!normalisedAllowedRequestMethods.includes(request.method)) { | ||
return next(new UserInputError({ statusCode: 405 })); | ||
} else { | ||
// Else if it is, then pass the request to the next() middleware | ||
next(); | ||
} | ||
}; | ||
} | ||
|
||
/** | ||
* Normalise an array of HTTP methods. | ||
* | ||
* @param {string[]} methods - The HTTP methods to normalise. | ||
* @returns {string[]} - Returns an array of capitalised HTTP methods. | ||
*/ | ||
function normaliseAllowedRequestMethods(methods) { | ||
return methods.map((method) => method.toUpperCase()); | ||
} | ||
|
||
exports.allowRequestMethods = allowRequestMethods; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
{ | ||
"name": "@dotcom-reliability-kit/middleware-allow-request-methods", | ||
"version": "0.0.0", | ||
"description": "Express middleware that returns 405 (rather than 404) for disallowed request methods", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/Financial-Times/dotcom-reliability-kit.git", | ||
"directory": "packages/middleware-allow-request-methods" | ||
}, | ||
"homepage": "https://github.com/Financial-Times/dotcom-reliability-kit/tree/main/packages/middleware-allow-request-methods#readme", | ||
"bugs": "https://github.com/Financial-Times/dotcom-reliability-kit/issues?q=label:\"package: middleware-allow-request-methods\"", | ||
"license": "MIT", | ||
"engines": { | ||
"node": "20.x || 22.x" | ||
}, | ||
"main": "lib/index.js", | ||
"types": "types/index.d.ts" | ||
} |
119 changes: 119 additions & 0 deletions
119
packages/middleware-allow-request-methods/test/unit/lib/index.spec.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
const { allowRequestMethods } = require('../../../lib/index'); | ||
const { UserInputError } = require('@dotcom-reliability-kit/errors'); | ||
|
||
// Mock Express request and response objects | ||
let mockRequest; | ||
let mockResponse; | ||
let mockNext; | ||
|
||
describe('allowRequestMethods', () => { | ||
CyntiBinti marked this conversation as resolved.
Show resolved
Hide resolved
|
||
beforeEach(() => { | ||
// Reset all mocks before each test | ||
mockRequest = { | ||
method: 'GET' | ||
}; | ||
mockResponse = { | ||
headersSent: false, | ||
header: jest.fn() | ||
}; | ||
mockNext = jest.fn(); | ||
}); | ||
|
||
afterEach(() => { | ||
// Clear all mocks after each test | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
describe('initialisation and validation', () => { | ||
it('throws TypeError when no allowedMethods are provided', () => { | ||
expect(() => { | ||
allowRequestMethods(); | ||
}).toThrow(TypeError); | ||
|
||
expect(() => { | ||
allowRequestMethods({}); | ||
}).toThrow('The `allowedMethods` option must be an array of strings'); | ||
}); | ||
|
||
it('throws TypeError when allowedMethods is an empty array', () => { | ||
expect(() => { | ||
allowRequestMethods({ allowedMethods: [] }); | ||
}).toThrow(TypeError); | ||
}); | ||
|
||
it('throws TypeError when allowedMethods contains non-string values', () => { | ||
expect(() => { | ||
allowRequestMethods({ allowedMethods: [123, true] }); | ||
}).toThrow(TypeError); | ||
}); | ||
|
||
it('creates middleware function when valid allowedMethods are provided', () => { | ||
const middleware = allowRequestMethods({ | ||
allowedMethods: ['GET', 'POST'] | ||
}); | ||
expect(typeof middleware).toBe('function'); | ||
}); | ||
}); | ||
|
||
describe('middleware behavior', () => { | ||
it('sets Allow header with normalised methods', () => { | ||
const middleware = allowRequestMethods({ | ||
allowedMethods: ['get', 'post'] | ||
}); | ||
|
||
middleware(mockRequest, mockResponse, mockNext); | ||
|
||
expect(mockResponse.header).toHaveBeenCalledWith('Allow', 'GET, POST'); | ||
}); | ||
|
||
it('skips setting header if headers are already sent', () => { | ||
mockResponse.headersSent = true; | ||
const middleware = allowRequestMethods({ | ||
allowedMethods: ['GET', 'POST'] | ||
}); | ||
|
||
middleware(mockRequest, mockResponse, mockNext); | ||
|
||
expect(mockResponse.header).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('calls next() with 405 error for disallowed method', () => { | ||
mockRequest.method = 'DELETE'; | ||
const middleware = allowRequestMethods({ | ||
allowedMethods: ['GET', 'POST'] | ||
}); | ||
|
||
middleware(mockRequest, mockResponse, mockNext); | ||
|
||
expect(mockNext).toHaveBeenCalledWith(expect.any(UserInputError)); | ||
const error = mockNext.mock.calls[0][0]; | ||
expect(error.statusCode).toBe(405); | ||
}); | ||
|
||
it('calls next() without error for allowed method', () => { | ||
mockRequest.method = 'GET'; | ||
const middleware = allowRequestMethods({ | ||
allowedMethods: ['GET', 'POST'] | ||
}); | ||
|
||
middleware(mockRequest, mockResponse, mockNext); | ||
|
||
expect(mockNext).toHaveBeenCalledWith(); | ||
}); | ||
}); | ||
|
||
describe('normaliseAllowedRequestMethods', () => { | ||
it('normalises methods to uppercase', () => { | ||
const middleware = allowRequestMethods({ | ||
allowedMethods: ['get', 'Post', 'DELETE'] | ||
}); | ||
|
||
middleware(mockRequest, mockResponse, mockNext); | ||
|
||
expect(mockResponse.header).toHaveBeenCalledWith( | ||
'Allow', | ||
'GET, POST, DELETE' | ||
); | ||
}); | ||
}); | ||
}); |
11 changes: 11 additions & 0 deletions
11
packages/middleware-allow-request-methods/types/index.d.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { RequestHandler } from 'express'; | ||
|
||
declare module '@dotcom-reliability-kit/middleware-allow-request-methods' { | ||
export type RequestMethodOptions = { | ||
allowedMethods: string[]; | ||
}; | ||
|
||
declare function allowRequestMethods( | ||
options: RequestMethodOptions | ||
): RequestHandler; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.