diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42160f48..7e196d88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,13 +8,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Use Node.js 16.x - uses: actions/setup-node@v2 + - uses: actions/checkout@v4 + - name: Use Node.js 22.x + uses: actions/setup-node@v4 with: - node-version: 16.x + node-version: 22.x - - uses: actions/cache@v2 + - uses: actions/cache@v4 id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) with: path: '**/node_modules' @@ -38,16 +38,16 @@ jobs: strategy: matrix: - node-version: [12.x, 14.x, 16.x] + node-version: [18.x, 20.x, 22.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - uses: actions/cache@v2 + - uses: actions/cache@v4 id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) with: path: '**/node_modules' @@ -70,13 +70,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Use Node.js 16.x - uses: actions/setup-node@v2 + - uses: actions/checkout@v4 + - name: Use Node.js 22.x + uses: actions/setup-node@v4 with: - node-version: 16.x + node-version: 22.x - - uses: actions/cache@v2 + - uses: actions/cache@v4 id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) with: path: '**/node_modules' @@ -100,7 +100,7 @@ jobs: name: Spellcheck runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: streetsidesoftware/cspell-action@main with: # Github token used to fetch the list of changed files in the commit. diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..328f853f --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,33 @@ +name: publish to npmjs +on: + release: + types: [prereleased, released] +jobs: + build-and-publish: + # prevents this action from running on forks + if: github.repository == 'chimurai/http-proxy-middleware' + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22.x' + registry-url: 'https://registry.npmjs.org' + + - name: Install Dependencies + run: yarn install + + - name: Publish to NPM (beta) + if: 'github.event.release.prerelease' + run: npm publish --provenance --access public --tag v2-beta + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Publish to NPM (stable) + if: '!github.event.release.prerelease' + run: npm publish --provenance --access public --tag v2-latest + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bc441d0..4412d3e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [v2.0.9](https://github.com/chimurai/http-proxy-middleware/releases/tag/v2.0.9) + +- fix(fixRequestBody): check readableLength + +## [v2.0.8](https://github.com/chimurai/http-proxy-middleware/releases/tag/v2.0.8) + +- fix(fixRequestBody): prevent multiple .write() calls +- fix(fixRequestBody): handle invalid request + +## [v2.0.7](https://github.com/chimurai/http-proxy-middleware/releases/tag/v2.0.7) + +- ci(github actions): add publish.yml +- fix(filter): handle errors + ## [v2.0.6](https://github.com/chimurai/http-proxy-middleware/releases/tag/v2.0.6) - fix(proxyReqWs): catch socket errors ([#763](https://github.com/chimurai/http-proxy-middleware/pull/763)) diff --git a/README.md b/README.md index 4eb8e111..31b2a081 100644 --- a/README.md +++ b/README.md @@ -595,4 +595,4 @@ $ yarn spellcheck The MIT License (MIT) -Copyright (c) 2015-2022 Steven Chim +Copyright (c) 2015-2025 Steven Chim diff --git a/package.json b/package.json index 4b47b4ca..041ed2ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "http-proxy-middleware", - "version": "2.0.6", + "version": "2.0.9", "description": "The one-liner node.js proxy middleware for connect, express and browser-sync", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/handlers/fix-request-body.ts b/src/handlers/fix-request-body.ts index 0f45a2f7..2a19c880 100644 --- a/src/handlers/fix-request-body.ts +++ b/src/handlers/fix-request-body.ts @@ -6,6 +6,11 @@ import * as querystring from 'querystring'; * Fix proxied body if bodyParser is involved. */ export function fixRequestBody(proxyReq: http.ClientRequest, req: http.IncomingMessage): void { + // skip fixRequestBody() when req.readableLength not 0 (bodyParser failure) + if (req.readableLength !== 0) { + return; + } + const requestBody = (req as Request).body; if (!requestBody) { @@ -13,17 +18,20 @@ export function fixRequestBody(proxyReq: http.ClientRequest, req: http.IncomingM } const contentType = proxyReq.getHeader('Content-Type') as string; + + if (!contentType) { + return; + } + const writeBody = (bodyData: string) => { // deepcode ignore ContentLengthInCode: bodyParser fix proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData)); proxyReq.write(bodyData); }; - if (contentType && contentType.includes('application/json')) { + if (contentType.includes('application/json')) { writeBody(JSON.stringify(requestBody)); - } - - if (contentType && contentType.includes('application/x-www-form-urlencoded')) { + } else if (contentType.includes('application/x-www-form-urlencoded')) { writeBody(querystring.stringify(requestBody)); } } diff --git a/src/http-proxy-middleware.ts b/src/http-proxy-middleware.ts index b93ae92f..a241cd68 100644 --- a/src/http-proxy-middleware.ts +++ b/src/http-proxy-middleware.ts @@ -109,8 +109,13 @@ export class HttpProxyMiddleware { * @return {Boolean} */ private shouldProxy = (context, req: Request): boolean => { - const path = req.originalUrl || req.url; - return contextMatcher.match(context, path, req); + try { + const path = req.originalUrl || req.url; + return contextMatcher.match(context, path, req); + } catch (error) { + this.logger.error(error); + return false; + } }; /** diff --git a/test/e2e/http-proxy-middleware.spec.ts b/test/e2e/http-proxy-middleware.spec.ts index 4685cc07..15355e91 100644 --- a/test/e2e/http-proxy-middleware.spec.ts +++ b/test/e2e/http-proxy-middleware.spec.ts @@ -153,6 +153,34 @@ describe('E2E http-proxy-middleware', () => { const response = await agent.get(`/api/b/c/d`).expect(404); expect(response.status).toBe(404); }); + + it('should not proxy when filter throws Error', async () => { + const myError = new Error('MY_ERROR'); + const filter = (path, req) => { + throw myError; + }; + + const logger = { + log: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + agent = request( + createApp( + createProxyMiddleware(filter, { + target: `http://localhost:${mockTargetServer.port}`, + logProvider: () => logger, + }) + ) + ); + + await mockTargetServer.get('/api/b/c/d').thenReply(200, 'HELLO WEB'); + const response = await agent.get(`/api/b/c/d`).expect(404); + expect(response.status).toBe(404); + expect(logger.error).toHaveBeenCalledWith(expect.objectContaining(myError)); + }); }); describe('multi path', () => { diff --git a/test/unit/fix-request-body.spec.ts b/test/unit/fix-request-body.spec.ts index 5bc42f8c..0348a2df 100644 --- a/test/unit/fix-request-body.spec.ts +++ b/test/unit/fix-request-body.spec.ts @@ -1,4 +1,5 @@ -import { ClientRequest } from 'http'; +import { Socket } from 'net'; +import { ClientRequest, ServerResponse, IncomingMessage } from 'http'; import * as querystring from 'querystring'; import { fixRequestBody } from '../../src/handlers/fix-request-body'; @@ -11,6 +12,18 @@ const fakeProxyRequest = () => { return proxyRequest; }; +const fakeProxyResponse = (): ServerResponse => { + const res = new ServerResponse(new IncomingMessage(new Socket())); + return res; +}; + +const createRequestWithBody = (body: unknown): Request => { + const req = new IncomingMessage(new Socket()) as Request; + req.url = '/test_path'; + req.body = body; + return req; +}; + describe('fixRequestBody', () => { it('should not write when body is undefined', () => { const proxyRequest = fakeProxyRequest(); @@ -18,7 +31,7 @@ describe('fixRequestBody', () => { jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); - fixRequestBody(proxyRequest, { body: undefined } as Request); + fixRequestBody(proxyRequest, createRequestWithBody(undefined)); expect(proxyRequest.setHeader).not.toHaveBeenCalled(); expect(proxyRequest.write).not.toHaveBeenCalled(); @@ -31,7 +44,7 @@ describe('fixRequestBody', () => { jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); - fixRequestBody(proxyRequest, { body: {} } as Request); + fixRequestBody(proxyRequest, createRequestWithBody({})); expect(proxyRequest.setHeader).toHaveBeenCalled(); expect(proxyRequest.write).toHaveBeenCalled(); @@ -44,7 +57,7 @@ describe('fixRequestBody', () => { jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); - fixRequestBody(proxyRequest, { body: { someField: 'some value' } } as Request); + fixRequestBody(proxyRequest, createRequestWithBody({ someField: 'some value' })); const expectedBody = JSON.stringify({ someField: 'some value' }); expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length); @@ -58,7 +71,7 @@ describe('fixRequestBody', () => { jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); - fixRequestBody(proxyRequest, { body: { someField: 'some value' } } as Request); + fixRequestBody(proxyRequest, createRequestWithBody({ someField: 'some value' })); const expectedBody = querystring.stringify({ someField: 'some value' }); expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length); @@ -72,10 +85,48 @@ describe('fixRequestBody', () => { jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); - fixRequestBody(proxyRequest, { body: { someField: 'some value' } } as Request); + fixRequestBody(proxyRequest, createRequestWithBody({ someField: 'some value' })); const expectedBody = querystring.stringify({ someField: 'some value' }); expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length); expect(proxyRequest.write).toHaveBeenCalledWith(expectedBody); }); + + it('should parse json and call write() once with incorrect content-type application/x-www-form-urlencoded+application/json', () => { + const proxyRequest = fakeProxyRequest(); + proxyRequest.setHeader('content-type', 'application/x-www-form-urlencoded+application/json'); + + jest.spyOn(proxyRequest, 'setHeader'); + jest.spyOn(proxyRequest, 'write'); + + fixRequestBody(proxyRequest, createRequestWithBody({ someField: 'some value' })); + + const expectedBody = JSON.stringify({ someField: 'some value' }); + expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length); + expect(proxyRequest.write).toHaveBeenCalledTimes(1); + expect(proxyRequest.write).toHaveBeenCalledWith(expectedBody); + }); + + it('should not fixRequestBody() when there bodyParser fails', () => { + const proxyRequest = fakeProxyRequest(); + const request = { + get readableLength() { + return 4444; // simulate bodyParser failure + }, + } as Request; + + const proxyResponse = fakeProxyResponse(); + proxyRequest.setHeader('content-type', 'application/x-www-form-urlencoded'); + + jest.spyOn(proxyRequest, 'write'); + jest.spyOn(proxyRequest, 'destroy'); + jest.spyOn(proxyResponse, 'writeHead'); + jest.spyOn(proxyResponse, 'end'); + + fixRequestBody(proxyRequest, request); + + expect(proxyResponse.end).toHaveBeenCalledTimes(0); + expect(proxyRequest.write).toHaveBeenCalledTimes(0); + expect(proxyRequest.destroy).toHaveBeenCalledTimes(0); + }); });