From 1540dac10c5bdda635bed0bb055ea058e7518181 Mon Sep 17 00:00:00 2001 From: chimurai <655241+chimurai@users.noreply.github.com> Date: Sat, 19 Feb 2022 23:38:52 +0100 Subject: [PATCH 01/10] feat: remove shorthand usage [BREAKING CHANGE] (#716) --- README.md | 63 ++++----------- recipes/shorthand.md | 62 --------------- src/config-factory.ts | 29 ------- test/e2e/http-proxy-middleware.spec.ts | 14 ---- test/e2e/websocket.spec.ts | 26 ------- test/unit/config-factory.spec.ts | 103 +------------------------ 6 files changed, 17 insertions(+), 280 deletions(-) delete mode 100644 recipes/shorthand.md diff --git a/README.md b/README.md index 4eb8e111..d4560a12 100644 --- a/README.md +++ b/README.md @@ -57,13 +57,12 @@ _All_ `http-proxy` [options](https://github.com/nodejitsu/node-http-proxy#option - [Install](#install) - [Core concept](#core-concept) - [Example](#example) + - [app.use(path, proxy)](#appusepath-proxy) - [Context matching](#context-matching) - [Options](#options) - [http-proxy-middleware options](#http-proxy-middleware-options) - [http-proxy events](#http-proxy-events) - [http-proxy options](#http-proxy-options) -- [Shorthand](#shorthand) - - [app.use(path, proxy)](#appusepath-proxy) - [WebSocket](#websocket) - [External WebSocket upgrade](#external-websocket-upgrade) - [Intercept and manipulate requests](#intercept-and-manipulate-requests) @@ -104,15 +103,6 @@ const apiProxy = createProxyMiddleware('/api', { target: 'http://www.example.org (full list of [`http-proxy-middleware` configuration options](#options)) -#### createProxyMiddleware(uri [, config]) - -```javascript -// shorthand syntax for the example above: -const apiProxy = createProxyMiddleware('http://www.example.org/api'); -``` - -More about the [shorthand configuration](#shorthand). - ## Example An example with `express` server. @@ -148,6 +138,21 @@ app.use('/api', exampleProxy); app.listen(3000); ``` +### app.use(path, proxy) + +If you want to use the server's `app.use` `path` parameter to match requests; +Create and mount the proxy without the http-proxy-middleware `context` parameter: + +```javascript +app.use('/api', createProxyMiddleware({ target: 'http://www.example.org', changeOrigin: true })); +``` + +`app.use` documentation: + +- express: http://expressjs.com/en/4x/api.html#app.use +- connect: https://github.com/senchalabs/connect#mount-middleware +- polka: https://github.com/lukeed/polka#usebase-fn + ## Context matching Providing an alternative way to decide which requests should be proxied; In case you are not able to use the server's [`path` parameter](http://expressjs.com/en/4x/api.html#app.use) to mount the proxy or when you need more flexibility. @@ -422,47 +427,11 @@ The following options are provided by the underlying [http-proxy](https://github }; ``` -## Shorthand - -Use the shorthand syntax when verbose configuration is not needed. The `context` and `option.target` will be automatically configured when shorthand is used. Options can still be used if needed. - -```javascript -createProxyMiddleware('http://www.example.org:8000/api'); -// createProxyMiddleware('/api', {target: 'http://www.example.org:8000'}); - -createProxyMiddleware('http://www.example.org:8000/api/books/*/**.json'); -// createProxyMiddleware('/api/books/*/**.json', {target: 'http://www.example.org:8000'}); - -createProxyMiddleware('http://www.example.org:8000/api', { changeOrigin: true }); -// createProxyMiddleware('/api', {target: 'http://www.example.org:8000', changeOrigin: true}); -``` - -### app.use(path, proxy) - -If you want to use the server's `app.use` `path` parameter to match requests; -Create and mount the proxy without the http-proxy-middleware `context` parameter: - -```javascript -app.use('/api', createProxyMiddleware({ target: 'http://www.example.org', changeOrigin: true })); -``` - -`app.use` documentation: - -- express: http://expressjs.com/en/4x/api.html#app.use -- connect: https://github.com/senchalabs/connect#mount-middleware -- polka: https://github.com/lukeed/polka#usebase-fn - ## WebSocket ```javascript // verbose api createProxyMiddleware('/', { target: 'http://echo.websocket.org', ws: true }); - -// shorthand -createProxyMiddleware('http://echo.websocket.org', { ws: true }); - -// shorter shorthand -createProxyMiddleware('ws://echo.websocket.org'); ``` ### External WebSocket upgrade diff --git a/recipes/shorthand.md b/recipes/shorthand.md deleted file mode 100644 index 30ca8b7f..00000000 --- a/recipes/shorthand.md +++ /dev/null @@ -1,62 +0,0 @@ -# Shorthand - -This example will create a proxy middleware using the shorthand notation. - -The http-proxy-middleware `context` and `config.target` will be set automatically. - -```javascript -const { createProxyMiddleware } = require('http-proxy-middleware'); - -const apiProxy = createProxyMiddleware('http://localhost:3000/api'); - -// equivalent: -// const apiProxy = createProxyMiddleware('/api', {target:'http://localhost:3000'}); -``` - -## Shorthand - Wildcard context - -This example will create a proxy middleware with shorthand wildcard context. - -```javascript -const { createProxyMiddleware } = require('http-proxy-middleware'); - -const apiProxy = createProxyMiddleware('http://localhost:3000/api/books/*/**.json'); -// equals: -// const apiProxy = createProxyMiddleware('/api/books/*/**.json', {target:'http://localhost:3000'}); -``` - -## Shorthand with additional configuration - -This example will create a proxy middleware with shorthand and additional configuration. - -```javascript -const { createProxyMiddleware } = require('http-proxy-middleware'); - -const apiProxy = createProxyMiddleware('http://localhost:3000/api', { changeOrigin: true }); -// equals: -// const apiProxy = createProxyMiddleware('/api', {target:'http://localhost:3000', {changeOrigin:true}}); -``` - -## Shorthand - WebSocket - -This example will create a proxy middleware with shorthand and additional configuration for WebSocket support. - -```javascript -const { createProxyMiddleware } = require('http-proxy-middleware'); - -const apiProxy = createProxyMiddleware('http://localhost:3000/api', { ws: true }); -// equals: -// const apiProxy = createProxyMiddleware('/api', {target:'http://localhost:3000', ws: true}); -``` - -## Shorthand - WebSocket only - -This example will create a proxy middleware with websocket shorthand only configuration. - -```javascript -const { createProxyMiddleware } = require('http-proxy-middleware'); - -const apiProxy = createProxyMiddleware('ws://localhost:3000/api'); -// equals: -// const apiProxy = createProxyMiddleware('/api', {target:'ws://localhost:3000', ws: true}); -``` diff --git a/src/config-factory.ts b/src/config-factory.ts index f57dd702..3053dfa2 100644 --- a/src/config-factory.ts +++ b/src/config-factory.ts @@ -1,5 +1,4 @@ import isPlainObj = require('is-plain-obj'); -import * as url from 'url'; import { ERRORS } from './errors'; import { getInstance } from './logger'; import { Filter, Options } from './types'; @@ -22,17 +21,6 @@ export function createConfig(context, opts?: Options): Config { // app.use('/api', proxy('http://localhost:9000')); // app.use(proxy('http://localhost:9000/api')); - } else if (isStringShortHand(context)) { - const oUrl = url.parse(context); - const target = [oUrl.protocol, '//', oUrl.host].join(''); - - config.context = oUrl.pathname || '/'; - config.options = Object.assign(config.options, { target }, opts); - - if (oUrl.protocol === 'ws:' || oUrl.protocol === 'wss:') { - config.options.ws = true; - } - // app.use('/api', proxy({target:'http://localhost:9000'})); } else { config.context = context; config.options = Object.assign(config.options, opts); @@ -47,23 +35,6 @@ export function createConfig(context, opts?: Options): Config { return config; } -/** - * Checks if a String only target/config is provided. - * This can be just the host or with the optional path. - * - * @example - * app.use('/api', proxy('http://localhost:9000')); - * app.use(proxy('http://localhost:9000/api')); - * - * @param {String} context [description] - * @return {Boolean} [description] - */ -function isStringShortHand(context: Filter) { - if (typeof context === 'string') { - return !!url.parse(context).host; - } -} - /** * Checks if a Object only config is provided, without a context. * In this case the all paths will be proxied. diff --git a/test/e2e/http-proxy-middleware.spec.ts b/test/e2e/http-proxy-middleware.spec.ts index 4685cc07..772430c1 100644 --- a/test/e2e/http-proxy-middleware.spec.ts +++ b/test/e2e/http-proxy-middleware.spec.ts @@ -389,20 +389,6 @@ describe('E2E http-proxy-middleware', () => { }); }); - describe('shorthand usage', () => { - beforeEach(() => { - agent = request( - createApp(createProxyMiddleware(`http://localhost:${mockTargetServer.port}/api`)) - ); - }); - - it('should have proxy with shorthand configuration', async () => { - await mockTargetServer.get('/api/foo/bar').thenReply(200, 'HELLO /api/foo/bar'); - const response = await agent.get(`/api/foo/bar`).expect(200); - expect(response.text).toBe('HELLO /api/foo/bar'); - }); - }); - describe('express with path + proxy', () => { beforeEach(() => { agent = request( diff --git a/test/e2e/websocket.spec.ts b/test/e2e/websocket.spec.ts index ad946d4a..d3f15e8a 100644 --- a/test/e2e/websocket.spec.ts +++ b/test/e2e/websocket.spec.ts @@ -95,32 +95,6 @@ describe('E2E WebSocket proxy', () => { }); }); - describe('option.ws with external server "upgrade" and shorthand usage', () => { - beforeEach(() => { - proxyServer = createApp( - createProxyMiddleware(`ws://localhost:${WS_SERVER_PORT}`, { - pathRewrite: { '^/socket': '' }, - }) - ).listen(SERVER_PORT); - - proxyServer.on('upgrade', proxyMiddleware.upgrade); - }); - - beforeEach((done) => { - ws = new WebSocket(`ws://localhost:${SERVER_PORT}/socket`); - ws.on('open', done); - }); - - it('should proxy to path', (done) => { - ws.on('message', (data, isBinary) => { - const message = isBinary ? data : data.toString(); - expect(message).toBe('foobar'); - done(); - }); - ws.send('foobar'); - }); - }); - describe('with router and pathRewrite', () => { beforeEach(() => { // override diff --git a/test/unit/config-factory.spec.ts b/test/unit/config-factory.spec.ts index e2f18243..610ec3bc 100644 --- a/test/unit/config-factory.spec.ts +++ b/test/unit/config-factory.spec.ts @@ -26,92 +26,7 @@ describe('configFactory', () => { }); }); - describe('shorthand String', () => { - describe('shorthand String config', () => { - beforeEach(() => { - result = createConfig('http://www.example.org:8000/api'); - }); - - it('should return config object', () => { - expect(Object.keys(result)).toEqual(['context', 'options']); - }); - - it('should return config object with context', () => { - expect(result.context).toBe('/api'); - }); - - it('should return config object with options', () => { - expect(result.options).toEqual({ - target: 'http://www.example.org:8000', - }); - }); - }); - - describe('shorthand String config for whole domain', () => { - beforeEach(() => { - result = createConfig('http://www.example.org:8000'); - }); - - it('should return config object with context', () => { - expect(result.context).toBe('/'); - }); - }); - - describe('shorthand String config for websocket url', () => { - beforeEach(() => { - result = createConfig('ws://www.example.org:8000'); - }); - - it('should return config object with context', () => { - expect(result.context).toBe('/'); - }); - - it('should return options with ws = true', () => { - expect(result.options.ws).toBe(true); - }); - }); - - describe('shorthand String config for secure websocket url', () => { - beforeEach(() => { - result = createConfig('wss://www.example.org:8000'); - }); - - it('should return config object with context', () => { - expect(result.context).toBe('/'); - }); - - it('should return options with ws = true', () => { - expect(result.options.ws).toBe(true); - }); - }); - - describe('shorthand String config with globbing', () => { - beforeEach(() => { - result = createConfig('http://www.example.org:8000/api/*.json'); - }); - - it('should return config object with context', () => { - expect(result.context).toBe('/api/*.json'); - }); - }); - - describe('shorthand String config with options', () => { - beforeEach(() => { - result = createConfig('http://www.example.org:8000/api', { - changeOrigin: true, - }); - }); - - it('should return config object with additional options', () => { - expect(result.options).toEqual({ - changeOrigin: true, - target: 'http://www.example.org:8000', - }); - }); - }); - }); - - describe('shorthand Object config', () => { + describe('Object config', () => { beforeEach(() => { result = createConfig({ target: 'http://www.example.org:8000' }); }); @@ -156,21 +71,5 @@ describe('configFactory', () => { expect(fn).not.toThrowError(Error); }); }); - - describe('faulty config. mixing classic with shorthand', () => { - beforeEach(() => { - result = createConfig('http://localhost:3000/api', { - target: 'http://localhost:8000', - }); - }); - - it('should use the target in the configuration as target', () => { - expect(result.options.target).toBe('http://localhost:8000'); - }); - - it('should not use the host from the shorthand as target', () => { - expect(result.options.target).not.toBe('http://localhost:3000'); - }); - }); }); }); From f4595a44dbe1b874a7c31c04c12dbe40c3be88bf Mon Sep 17 00:00:00 2001 From: cdaringe Date: Sat, 26 Feb 2022 16:50:01 -0800 Subject: [PATCH 02/10] refactor: use base node http types BREAKING CHANGE: `RequestHandler` => `RequestMiddleware`. Middleware now is expressed in terms of node core http.* types, which express req/res are compatible with. Orienting to base http.* types allows better typing accross all node servers, as eventually each implementation has these values in their servers. That is to say, express, koa, fastify, next, etc all use http.IncomingMessage/http.ServerResponse under the hood, thus the new middleware types are compatible with everyone. --- src/handlers/fix-request-body.ts | 4 +-- src/http-proxy-middleware.ts | 13 +++---- src/index.ts | 2 +- src/types.ts | 27 ++++++++++---- test/e2e/{ => servers}/express-router.spec.ts | 4 +-- test/e2e/servers/http.spec.ts | 35 +++++++++++++++++++ test/e2e/websocket.spec.ts | 4 +-- test/unit/fix-request-body.spec.ts | 4 +-- 8 files changed, 69 insertions(+), 24 deletions(-) rename test/e2e/{ => servers}/express-router.spec.ts (93%) create mode 100644 test/e2e/servers/http.spec.ts diff --git a/src/handlers/fix-request-body.ts b/src/handlers/fix-request-body.ts index 1855f569..24108511 100644 --- a/src/handlers/fix-request-body.ts +++ b/src/handlers/fix-request-body.ts @@ -5,8 +5,8 @@ import * as querystring from 'querystring'; /** * Fix proxied body if bodyParser is involved. */ -export function fixRequestBody(proxyReq: http.ClientRequest, req: http.IncomingMessage): void { - const requestBody = (req as Request).body; +export function fixRequestBody(proxyReq: http.ClientRequest, req: Request): void { + const requestBody = req.body; if (!requestBody) { return; diff --git a/src/http-proxy-middleware.ts b/src/http-proxy-middleware.ts index b93ae92f..5928bd5b 100644 --- a/src/http-proxy-middleware.ts +++ b/src/http-proxy-middleware.ts @@ -1,6 +1,5 @@ import type * as https from 'https'; -import type * as express from 'express'; -import type { Filter, Request, RequestHandler, Response, Options } from './types'; +import type { Filter, Request, RequestMiddleware, Response, Options } from './types'; import * as httpProxy from 'http-proxy'; import { createConfig, Config } from './config-factory'; import * as contextMatcher from './context-matcher'; @@ -8,6 +7,7 @@ import * as handlers from './_handlers'; import { getArrow, getInstance } from './logger'; import * as PathRewriter from './path-rewriter'; import * as Router from './router'; + export class HttpProxyMiddleware { private logger = getInstance(); private config: Config; @@ -35,7 +35,7 @@ export class HttpProxyMiddleware { // https://github.com/chimurai/http-proxy-middleware/issues/19 // expose function to upgrade externally - (this.middleware as any).upgrade = (req, socket, head) => { + this.middleware.upgrade = (req, socket, head) => { if (!this.wsInternalSubscribed) { this.handleUpgrade(req, socket, head); } @@ -43,11 +43,7 @@ export class HttpProxyMiddleware { } // https://github.com/Microsoft/TypeScript/wiki/'this'-in-TypeScript#red-flags-for-this - public middleware: RequestHandler = async ( - req: Request, - res: Response, - next: express.NextFunction - ) => { + public middleware: RequestMiddleware = async (req, res, next) => { if (this.shouldProxy(this.config.context, req)) { try { const activeProxyOptions = await this.prepareProxyRequest(req); @@ -58,7 +54,6 @@ export class HttpProxyMiddleware { } else { next(); } - /** * Get the server object to subscribe to server events; * 'upgrade' for websocket and 'close' for graceful shutdown diff --git a/src/index.ts b/src/index.ts index 79a95325..fbbdc935 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,4 +8,4 @@ export function createProxyMiddleware(context: Filter | Options, options?: Optio export * from './handlers'; -export { Filter, Options, RequestHandler } from './types'; +export { Filter, Options, RequestMiddleware as RequestHandler } from './types'; diff --git a/src/types.ts b/src/types.ts index 55628aab..253036cc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,20 +4,35 @@ */ /* eslint-disable @typescript-eslint/no-empty-interface */ - -import type * as express from 'express'; import type * as http from 'http'; import type * as httpProxy from 'http-proxy'; import type * as net from 'net'; import type * as url from 'url'; -export interface Request extends express.Request {} -export interface Response extends express.Response {} +export type Request = http.IncomingMessage; +export type Response = http.ServerResponse; -export interface RequestHandler extends express.RequestHandler { - upgrade?: (req: Request, socket: net.Socket, head: any) => void; +declare module 'http' { + interface IncomingMessage { + originalUrl?: string; + hostname?: string; + host?: string; + body?: Record; + } } +export type Next = (...args: unknown[]) => unknown; + +type RequestMiddlewareFunction = ( + req: http.IncomingMessage, + res: http.ServerResponse, + next: Next +) => unknown | Promise; + +export type RequestMiddleware = RequestMiddlewareFunction & { + upgrade?: (req: Request, socket: net.Socket, head: any) => void; +}; + export type Filter = string | string[] | ((pathname: string, req: Request) => boolean); export interface Options extends httpProxy.ServerOptions { diff --git a/test/e2e/express-router.spec.ts b/test/e2e/servers/express-router.spec.ts similarity index 93% rename from test/e2e/express-router.spec.ts rename to test/e2e/servers/express-router.spec.ts index 611247be..39930340 100644 --- a/test/e2e/express-router.spec.ts +++ b/test/e2e/servers/express-router.spec.ts @@ -1,7 +1,7 @@ import * as express from 'express'; import * as request from 'supertest'; -import { createProxyMiddleware } from './test-kit'; -import { Options } from '../../src/index'; +import { createProxyMiddleware } from '../test-kit'; +import { Options } from '../../../src/index'; describe('Usage in Express', () => { let app: express.Express; diff --git a/test/e2e/servers/http.spec.ts b/test/e2e/servers/http.spec.ts new file mode 100644 index 00000000..f46ae4e6 --- /dev/null +++ b/test/e2e/servers/http.spec.ts @@ -0,0 +1,35 @@ +import * as http from 'http'; +import { createProxyMiddleware } from '../test-kit'; +import { Options } from '../../../src/index'; +import * as request from 'supertest'; +import * as getPort from 'get-port'; + +describe('http integration', () => { + it('should work with raw node http RequestHandler', async () => { + await new Promise(async (resolve, reject) => { + const port = await getPort(); + const server = http + .createServer((req, res) => { + const proxyConfig: Options = { + changeOrigin: true, + logLevel: 'silent', + target: 'http://jsonplaceholder.typicode.com', + }; + const handler = createProxyMiddleware(proxyConfig); + return handler(req, res, resolve); + }) + .listen(port); + request(server) + .get('/') + .expect(200) + .end((err, res) => { + if (err) { + reject(err); + } else { + expect(res.ok).toBe(true); + resolve(res); + } + }); + }); + }); +}); diff --git a/test/e2e/websocket.spec.ts b/test/e2e/websocket.spec.ts index ad946d4a..38b24bae 100644 --- a/test/e2e/websocket.spec.ts +++ b/test/e2e/websocket.spec.ts @@ -3,7 +3,7 @@ import * as WebSocket from 'ws'; import { Server as WebSocketServer } from 'ws'; import * as getPort from 'get-port'; import { createProxyMiddleware, createApp } from './test-kit'; -import type { RequestHandler } from '../../src/types'; +import type { RequestMiddleware } from '../../src/types'; /******************************************************************** * - Not possible to use `supertest` to test WebSockets @@ -14,7 +14,7 @@ describe('E2E WebSocket proxy', () => { let proxyServer: http.Server; let ws: WebSocket; let wss: WebSocketServer; - let proxyMiddleware: RequestHandler; + let proxyMiddleware: RequestMiddleware; let WS_SERVER_PORT: number; let SERVER_PORT: number; diff --git a/test/unit/fix-request-body.spec.ts b/test/unit/fix-request-body.spec.ts index 96677502..e9a37717 100644 --- a/test/unit/fix-request-body.spec.ts +++ b/test/unit/fix-request-body.spec.ts @@ -44,7 +44,7 @@ describe('fixRequestBody', () => { jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); - fixRequestBody(proxyRequest, { body: { someField: 'some value' } } as Request); + fixRequestBody(proxyRequest, { body: { someField: 'some value' } } as unknown as Request); const expectedBody = JSON.stringify({ someField: 'some value' }); expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length); @@ -58,7 +58,7 @@ describe('fixRequestBody', () => { jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); - fixRequestBody(proxyRequest, { body: { someField: 'some value' } } as Request); + fixRequestBody(proxyRequest, { body: { someField: 'some value' } } as unknown as Request); const expectedBody = querystring.stringify({ someField: 'some value' }); expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length); From a6ae7439e1fc6b39635084ca494fca6751b04626 Mon Sep 17 00:00:00 2001 From: cdaringe Date: Sat, 26 Feb 2022 16:50:01 -0800 Subject: [PATCH 03/10] refactor: use base node http types BREAKING CHANGE: `RequestHandler` => `RequestMiddleware`. Middleware now is expressed in terms of node core http.* types, which express req/res are compatible with. Orienting to base http.* types allows better typing accross all node servers, as eventually each implementation has these values in their servers. That is to say, express, koa, fastify, next, etc all use http.IncomingMessage/http.ServerResponse under the hood, thus the new middleware types are compatible with everyone. --- src/handlers/fix-request-body.ts | 4 +-- src/http-proxy-middleware.ts | 13 +++---- src/index.ts | 2 +- src/types.ts | 32 +++++++++++++---- test/e2e/{ => servers}/express-router.spec.ts | 4 +-- test/e2e/servers/http.spec.ts | 35 +++++++++++++++++++ test/e2e/websocket.spec.ts | 4 +-- test/unit/fix-request-body.spec.ts | 4 +-- 8 files changed, 74 insertions(+), 24 deletions(-) rename test/e2e/{ => servers}/express-router.spec.ts (93%) create mode 100644 test/e2e/servers/http.spec.ts diff --git a/src/handlers/fix-request-body.ts b/src/handlers/fix-request-body.ts index 1855f569..24108511 100644 --- a/src/handlers/fix-request-body.ts +++ b/src/handlers/fix-request-body.ts @@ -5,8 +5,8 @@ import * as querystring from 'querystring'; /** * Fix proxied body if bodyParser is involved. */ -export function fixRequestBody(proxyReq: http.ClientRequest, req: http.IncomingMessage): void { - const requestBody = (req as Request).body; +export function fixRequestBody(proxyReq: http.ClientRequest, req: Request): void { + const requestBody = req.body; if (!requestBody) { return; diff --git a/src/http-proxy-middleware.ts b/src/http-proxy-middleware.ts index b93ae92f..5928bd5b 100644 --- a/src/http-proxy-middleware.ts +++ b/src/http-proxy-middleware.ts @@ -1,6 +1,5 @@ import type * as https from 'https'; -import type * as express from 'express'; -import type { Filter, Request, RequestHandler, Response, Options } from './types'; +import type { Filter, Request, RequestMiddleware, Response, Options } from './types'; import * as httpProxy from 'http-proxy'; import { createConfig, Config } from './config-factory'; import * as contextMatcher from './context-matcher'; @@ -8,6 +7,7 @@ import * as handlers from './_handlers'; import { getArrow, getInstance } from './logger'; import * as PathRewriter from './path-rewriter'; import * as Router from './router'; + export class HttpProxyMiddleware { private logger = getInstance(); private config: Config; @@ -35,7 +35,7 @@ export class HttpProxyMiddleware { // https://github.com/chimurai/http-proxy-middleware/issues/19 // expose function to upgrade externally - (this.middleware as any).upgrade = (req, socket, head) => { + this.middleware.upgrade = (req, socket, head) => { if (!this.wsInternalSubscribed) { this.handleUpgrade(req, socket, head); } @@ -43,11 +43,7 @@ export class HttpProxyMiddleware { } // https://github.com/Microsoft/TypeScript/wiki/'this'-in-TypeScript#red-flags-for-this - public middleware: RequestHandler = async ( - req: Request, - res: Response, - next: express.NextFunction - ) => { + public middleware: RequestMiddleware = async (req, res, next) => { if (this.shouldProxy(this.config.context, req)) { try { const activeProxyOptions = await this.prepareProxyRequest(req); @@ -58,7 +54,6 @@ export class HttpProxyMiddleware { } else { next(); } - /** * Get the server object to subscribe to server events; * 'upgrade' for websocket and 'close' for graceful shutdown diff --git a/src/index.ts b/src/index.ts index 79a95325..59ccf6d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,4 +8,4 @@ export function createProxyMiddleware(context: Filter | Options, options?: Optio export * from './handlers'; -export { Filter, Options, RequestHandler } from './types'; +export { Filter, Options, RequestMiddleware } from './types'; diff --git a/src/types.ts b/src/types.ts index 55628aab..a2cfbc02 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,20 +4,40 @@ */ /* eslint-disable @typescript-eslint/no-empty-interface */ - -import type * as express from 'express'; import type * as http from 'http'; import type * as httpProxy from 'http-proxy'; import type * as net from 'net'; import type * as url from 'url'; -export interface Request extends express.Request {} -export interface Response extends express.Response {} +export type Request = http.IncomingMessage; +export type Response = http.ServerResponse; -export interface RequestHandler extends express.RequestHandler { - upgrade?: (req: Request, socket: net.Socket, head: any) => void; +/** + * http-proxy-middleware supports framework specific values. The following + * values are primarily decorated onto IncomingMessage by express, but are + * not required for use. + */ +declare module 'http' { + interface IncomingMessage { + originalUrl?: string; + hostname?: string; + host?: string; + body?: Record; + } } +export type Next = (...args: unknown[]) => unknown; + +type RequestMiddlewareFunction = ( + req: http.IncomingMessage, + res: http.ServerResponse, + next: Next +) => unknown | Promise; + +export type RequestMiddleware = RequestMiddlewareFunction & { + upgrade?: (req: Request, socket: net.Socket, head: any) => void; +}; + export type Filter = string | string[] | ((pathname: string, req: Request) => boolean); export interface Options extends httpProxy.ServerOptions { diff --git a/test/e2e/express-router.spec.ts b/test/e2e/servers/express-router.spec.ts similarity index 93% rename from test/e2e/express-router.spec.ts rename to test/e2e/servers/express-router.spec.ts index 611247be..39930340 100644 --- a/test/e2e/express-router.spec.ts +++ b/test/e2e/servers/express-router.spec.ts @@ -1,7 +1,7 @@ import * as express from 'express'; import * as request from 'supertest'; -import { createProxyMiddleware } from './test-kit'; -import { Options } from '../../src/index'; +import { createProxyMiddleware } from '../test-kit'; +import { Options } from '../../../src/index'; describe('Usage in Express', () => { let app: express.Express; diff --git a/test/e2e/servers/http.spec.ts b/test/e2e/servers/http.spec.ts new file mode 100644 index 00000000..f46ae4e6 --- /dev/null +++ b/test/e2e/servers/http.spec.ts @@ -0,0 +1,35 @@ +import * as http from 'http'; +import { createProxyMiddleware } from '../test-kit'; +import { Options } from '../../../src/index'; +import * as request from 'supertest'; +import * as getPort from 'get-port'; + +describe('http integration', () => { + it('should work with raw node http RequestHandler', async () => { + await new Promise(async (resolve, reject) => { + const port = await getPort(); + const server = http + .createServer((req, res) => { + const proxyConfig: Options = { + changeOrigin: true, + logLevel: 'silent', + target: 'http://jsonplaceholder.typicode.com', + }; + const handler = createProxyMiddleware(proxyConfig); + return handler(req, res, resolve); + }) + .listen(port); + request(server) + .get('/') + .expect(200) + .end((err, res) => { + if (err) { + reject(err); + } else { + expect(res.ok).toBe(true); + resolve(res); + } + }); + }); + }); +}); diff --git a/test/e2e/websocket.spec.ts b/test/e2e/websocket.spec.ts index ad946d4a..38b24bae 100644 --- a/test/e2e/websocket.spec.ts +++ b/test/e2e/websocket.spec.ts @@ -3,7 +3,7 @@ import * as WebSocket from 'ws'; import { Server as WebSocketServer } from 'ws'; import * as getPort from 'get-port'; import { createProxyMiddleware, createApp } from './test-kit'; -import type { RequestHandler } from '../../src/types'; +import type { RequestMiddleware } from '../../src/types'; /******************************************************************** * - Not possible to use `supertest` to test WebSockets @@ -14,7 +14,7 @@ describe('E2E WebSocket proxy', () => { let proxyServer: http.Server; let ws: WebSocket; let wss: WebSocketServer; - let proxyMiddleware: RequestHandler; + let proxyMiddleware: RequestMiddleware; let WS_SERVER_PORT: number; let SERVER_PORT: number; diff --git a/test/unit/fix-request-body.spec.ts b/test/unit/fix-request-body.spec.ts index 96677502..e9a37717 100644 --- a/test/unit/fix-request-body.spec.ts +++ b/test/unit/fix-request-body.spec.ts @@ -44,7 +44,7 @@ describe('fixRequestBody', () => { jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); - fixRequestBody(proxyRequest, { body: { someField: 'some value' } } as Request); + fixRequestBody(proxyRequest, { body: { someField: 'some value' } } as unknown as Request); const expectedBody = JSON.stringify({ someField: 'some value' }); expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length); @@ -58,7 +58,7 @@ describe('fixRequestBody', () => { jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); - fixRequestBody(proxyRequest, { body: { someField: 'some value' } } as Request); + fixRequestBody(proxyRequest, { body: { someField: 'some value' } } as unknown as Request); const expectedBody = querystring.stringify({ someField: 'some value' }); expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length); From 54205f0ad466e231e94d349f50d0c114f5d81257 Mon Sep 17 00:00:00 2001 From: cdaringe Date: Sat, 26 Feb 2022 17:01:43 -0800 Subject: [PATCH 04/10] refactor: drop coupling on @types/express --- package.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/package.json b/package.json index 779939d4..8fecb034 100644 --- a/package.json +++ b/package.json @@ -90,14 +90,6 @@ "is-plain-obj": "^3.0.0", "micromatch": "^4.0.2" }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } - }, "engines": { "node": ">=12.0.0" }, From 83dade28e631479ff872650ec6db594da3132d1c Mon Sep 17 00:00:00 2001 From: cdaringe Date: Sat, 26 Feb 2022 17:03:15 -0800 Subject: [PATCH 05/10] fix: drop final express usage in src/* --- src/_handlers.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/_handlers.ts b/src/_handlers.ts index 6cdefd79..4a8b6be2 100644 --- a/src/_handlers.ts +++ b/src/_handlers.ts @@ -1,5 +1,4 @@ -import type * as express from 'express'; -import type { Options } from './types'; +import type { Request, Response, Options } from './types'; import type * as httpProxy from 'http-proxy'; import { getInstance } from './logger'; const logger = getInstance(); @@ -53,7 +52,7 @@ export function getHandlers(options: Options) { return handlers; } -function defaultErrorHandler(err, req: express.Request, res: express.Response) { +function defaultErrorHandler(err, req: Request, res: Response) { // Re-throw error. Not recoverable since req & res are empty. if (!req && !res) { throw err; // "Error: Must provide a proper URL as target" From 0b30c5de54a5e796d26d7d29a53f6ca11e9e460e Mon Sep 17 00:00:00 2001 From: chimurai <655241+chimurai@users.noreply.github.com> Date: Sun, 27 Feb 2022 12:28:48 +0100 Subject: [PATCH 06/10] feat(option): refactor context to pathFilter option [BREAKING CHANGE] (#722) --- README.md | 234 +++++++-------- examples/browser-sync/index.js | 3 +- examples/websocket/index.html | 2 +- examples/websocket/index.js | 4 +- recipes/README.md | 10 +- recipes/basic.md | 8 +- .../{context-matching.md => pathFilter.md} | 38 +-- src/config-factory.ts | 61 ---- src/configuration.ts | 23 ++ src/errors.ts | 2 +- src/http-proxy-middleware.ts | 29 +- src/index.ts | 13 +- src/{context-matcher.ts => path-filter.ts} | 44 +-- src/types.ts | 1 + test/e2e/express-router.spec.ts | 3 +- test/e2e/http-proxy-middleware.spec.ts | 48 ++-- test/e2e/websocket.spec.ts | 5 +- test/types.spec.ts | 8 +- test/unit/config-factory.spec.ts | 75 ----- test/unit/configuration.spec.ts | 36 +++ test/unit/context-matcher.spec.ts | 266 ------------------ test/unit/path-filter.spec.ts | 254 +++++++++++++++++ 22 files changed, 558 insertions(+), 609 deletions(-) rename recipes/{context-matching.md => pathFilter.md} (69%) delete mode 100644 src/config-factory.ts create mode 100644 src/configuration.ts rename src/{context-matcher.ts => path-filter.ts} (55%) delete mode 100644 test/unit/config-factory.spec.ts create mode 100644 test/unit/configuration.spec.ts delete mode 100644 test/unit/context-matcher.spec.ts create mode 100644 test/unit/path-filter.spec.ts diff --git a/README.md b/README.md index d4560a12..905de29e 100644 --- a/README.md +++ b/README.md @@ -54,15 +54,20 @@ _All_ `http-proxy` [options](https://github.com/nodejitsu/node-http-proxy#option ## Table of Contents + + - [Install](#install) - [Core concept](#core-concept) -- [Example](#example) +- [Express Server Example](#express-server-example) - [app.use(path, proxy)](#appusepath-proxy) -- [Context matching](#context-matching) - [Options](#options) - - [http-proxy-middleware options](#http-proxy-middleware-options) - - [http-proxy events](#http-proxy-events) - - [http-proxy options](#http-proxy-options) + - [`pathFilter` (string, []string, glob, []glob, function)](#pathfilter-string-string-glob-glob-function) + - [`pathRewrite` (object/function)](#pathrewrite-objectfunction) + - [`router` (object/function)](#router-objectfunction) + - [`logLevel` (string)](#loglevel-string) + - [`logProvider` (function)](#logprovider-function) +- [`http-proxy` events](#http-proxy-events) +- [`http-proxy` options](#http-proxy-options) - [WebSocket](#websocket) - [External WebSocket upgrade](#external-websocket-upgrade) - [Intercept and manipulate requests](#intercept-and-manipulate-requests) @@ -74,36 +79,36 @@ _All_ `http-proxy` [options](https://github.com/nodejitsu/node-http-proxy#option - [Changelog](#changelog) - [License](#license) + + ## Install -```bash -$ npm install --save-dev http-proxy-middleware +```shell +npm install --save-dev http-proxy-middleware ``` ## Core concept -Proxy middleware configuration. - -#### createProxyMiddleware([context,] config) +Create and configure a proxy middleware with: `createProxyMiddleware(config)`. ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); -const apiProxy = createProxyMiddleware('/api', { target: 'http://www.example.org' }); -// \____/ \_____________________________/ -// | | -// context options +const apiProxy = createProxyMiddleware({ + pathFilter: '/api', + target: 'http://www.example.org', +}); // 'apiProxy' is now ready to be used as middleware in a server. ``` -- **context**: Determine which requests should be proxied to the target host. - (more on [context matching](#context-matching)) +- **options.pathFilter**: Determine which requests should be proxied to the target host. + (more on [path filter](#path-filter)) - **options.target**: target host to proxy to. _(protocol + host)_ -(full list of [`http-proxy-middleware` configuration options](#options)) +- see full list of [`http-proxy-middleware` configuration options](#options) -## Example +## Express Server Example An example with `express` server. @@ -129,7 +134,7 @@ const options = { }, }; -// create the proxy (without context) +// create the proxy const exampleProxy = createProxyMiddleware(options); // mount `exampleProxy` in web server @@ -140,8 +145,8 @@ app.listen(3000); ### app.use(path, proxy) -If you want to use the server's `app.use` `path` parameter to match requests; -Create and mount the proxy without the http-proxy-middleware `context` parameter: +If you want to use the server's `app.use` `path` parameter to match requests. +Use `pathFilter` option to further include/exclude requests which you want to proxy. ```javascript app.use('/api', createProxyMiddleware({ target: 'http://www.example.org', changeOrigin: true })); @@ -153,11 +158,15 @@ app.use('/api', createProxyMiddleware({ target: 'http://www.example.org', change - connect: https://github.com/senchalabs/connect#mount-middleware - polka: https://github.com/lukeed/polka#usebase-fn -## Context matching +## Options + +http-proxy-middleware options: + +### `pathFilter` (string, []string, glob, []glob, function) -Providing an alternative way to decide which requests should be proxied; In case you are not able to use the server's [`path` parameter](http://expressjs.com/en/4x/api.html#app.use) to mount the proxy or when you need more flexibility. +Decide which requests should be proxied; In case you are not able to use the server's [`path` parameter](http://expressjs.com/en/4x/api.html#app.use) to mount the proxy or when you need more flexibility. -[RFC 3986 `path`](https://tools.ietf.org/html/rfc3986#section-3.3) is used for context matching. +[RFC 3986 `path`](https://tools.ietf.org/html/rfc3986#section-3.3) is used in `pathFilter`. ```ascii foo://example.com:8042/over/there?name=ferret#nose @@ -169,23 +178,22 @@ Providing an alternative way to decide which requests should be proxied; In case - **path matching** - `createProxyMiddleware({...})` - matches any path, all requests will be proxied. - - `createProxyMiddleware('/', {...})` - matches any path, all requests will be proxied. - - `createProxyMiddleware('/api', {...})` - matches paths starting with `/api` + - `createProxyMiddleware({ pathFilter: '/api', ...})` - matches paths starting with `/api` - **multiple path matching** - - `createProxyMiddleware(['/api', '/ajax', '/someotherpath'], {...})` + - `createProxyMiddleware({ pathFilter: ['/api', '/ajax', '/someotherpath'], ...})` - **wildcard path matching** For fine-grained control you can use wildcard matching. Glob pattern matching is done by _micromatch_. Visit [micromatch](https://www.npmjs.com/package/micromatch) or [glob](https://www.npmjs.com/package/glob) for more globbing examples. - - `createProxyMiddleware('**', {...})` matches any path, all requests will be proxied. - - `createProxyMiddleware('**/*.html', {...})` matches any path which ends with `.html` - - `createProxyMiddleware('/*.html', {...})` matches paths directly under path-absolute - - `createProxyMiddleware('/api/**/*.html', {...})` matches requests ending with `.html` in the path of `/api` - - `createProxyMiddleware(['/api/**', '/ajax/**'], {...})` combine multiple patterns - - `createProxyMiddleware(['/api/**', '!**/bad.json'], {...})` exclusion + - `createProxyMiddleware({ pathFilter: '**', ...})` matches any path, all requests will be proxied. + - `createProxyMiddleware({ pathFilter: '**/*.html', ...})` matches any path which ends with `.html` + - `createProxyMiddleware({ pathFilter: '/*.html', ...})` matches paths directly under path-absolute + - `createProxyMiddleware({ pathFilter: '/api/**/*.html', ...})` matches requests ending with `.html` in the path of `/api` + - `createProxyMiddleware({ pathFilter: ['/api/**', '/ajax/**'], ...})` combine multiple patterns + - `createProxyMiddleware({ pathFilter: ['/api/**', '!**/bad.json'], ...})` exclusion **Note**: In multiple path matching, you cannot use string paths and wildcard paths together. @@ -197,8 +205,8 @@ Providing an alternative way to decide which requests should be proxied; In case /** * @return {Boolean} */ - const filter = function (pathname, req) { - return pathname.match('^/api') && req.method === 'GET'; + const filter = function (path, req) { + return path.match('^/api') && req.method === 'GET'; }; const apiProxy = createProxyMiddleware(filter, { @@ -206,95 +214,101 @@ Providing an alternative way to decide which requests should be proxied; In case }); ``` -## Options +### `pathRewrite` (object/function) -### http-proxy-middleware options +Rewrite target's url path. Object-keys will be used as _RegExp_ to match paths. -- **option.pathRewrite**: object/function, rewrite target's url path. Object-keys will be used as _RegExp_ to match paths. +```javascript +// rewrite path +pathRewrite: {'^/old/api' : '/new/api'} - ```javascript - // rewrite path - pathRewrite: {'^/old/api' : '/new/api'} +// remove path +pathRewrite: {'^/remove/api' : ''} - // remove path - pathRewrite: {'^/remove/api' : ''} +// add base path +pathRewrite: {'^/' : '/basepath/'} - // add base path - pathRewrite: {'^/' : '/basepath/'} +// custom rewriting +pathRewrite: function (path, req) { return path.replace('/api', '/base/api') } - // custom rewriting - pathRewrite: function (path, req) { return path.replace('/api', '/base/api') } +// custom rewriting, returning Promise +pathRewrite: async function (path, req) { + const should_add_something = await httpRequestToDecideSomething(path); + if (should_add_something) path += "something"; + return path; +} +``` - // custom rewriting, returning Promise - pathRewrite: async function (path, req) { - const should_add_something = await httpRequestToDecideSomething(path); - if (should_add_something) path += "something"; - return path; - } - ``` +### `router` (object/function) -- **option.router**: object/function, re-target `option.target` for specific requests. +Re-target `option.target` for specific requests. - ```javascript - // Use `host` and/or `path` to match requests. First match will be used. - // The order of the configuration matters. - router: { - 'integration.localhost:3000' : 'http://localhost:8001', // host only - 'staging.localhost:3000' : 'http://localhost:8002', // host only - 'localhost:3000/api' : 'http://localhost:8003', // host + path - '/rest' : 'http://localhost:8004' // path only - } +```javascript +// Use `host` and/or `path` to match requests. First match will be used. +// The order of the configuration matters. +router: { + 'integration.localhost:3000' : 'http://localhost:8001', // host only + 'staging.localhost:3000' : 'http://localhost:8002', // host only + 'localhost:3000/api' : 'http://localhost:8003', // host + path + '/rest' : 'http://localhost:8004' // path only +} + +// Custom router function (string target) +router: function(req) { + return 'http://localhost:8004'; +} + +// Custom router function (target object) +router: function(req) { + return { + protocol: 'https:', // The : is required + host: 'localhost', + port: 8004 + }; +} - // Custom router function (string target) - router: function(req) { - return 'http://localhost:8004'; - } +// Asynchronous router function which returns promise +router: async function(req) { + const url = await doSomeIO(); + return url; +} +``` - // Custom router function (target object) - router: function(req) { - return { - protocol: 'https:', // The : is required - host: 'localhost', - port: 8004 - }; - } +### `logLevel` (string) - // Asynchronous router function which returns promise - router: async function(req) { - const url = await doSomeIO(); - return url; - } - ``` +Default: `'info'` -- **option.logLevel**: string, ['debug', 'info', 'warn', 'error', 'silent']. Default: `'info'` +Values: ['debug', 'info', 'warn', 'error', 'silent']. -- **option.logProvider**: function, modify or replace log provider. Default: `console`. +### `logProvider` (function) - ```javascript - // simple replace - function logProvider(provider) { - // replace the default console log provider. - return require('winston'); - } - ``` +Modify or replace log provider. Default: `console`. - ```javascript - // verbose replacement - function logProvider(provider) { - const logger = new (require('winston').Logger)(); - - const myCustomProvider = { - log: logger.log, - debug: logger.debug, - info: logger.info, - warn: logger.warn, - error: logger.error, - }; - return myCustomProvider; - } - ``` +```javascript +// simple replace +function logProvider(provider) { + // replace the default console log provider. + return require('winston'); +} +``` + +```javascript +// verbose replacement +function logProvider(provider) { + const logger = new (require('winston').Logger)(); + + const myCustomProvider = { + log: logger.log, + debug: logger.debug, + info: logger.info, + warn: logger.warn, + error: logger.error, + }; + return myCustomProvider; +} +``` -### http-proxy events +## `http-proxy` events Subscribe to [http-proxy events](https://github.com/nodejitsu/node-http-proxy#listening-for-proxy-events): @@ -355,7 +369,7 @@ Subscribe to [http-proxy events](https://github.com/nodejitsu/node-http-proxy#li } ``` -### http-proxy options +## `http-proxy` options The following options are provided by the underlying [http-proxy](https://github.com/nodejitsu/node-http-proxy#options) library. @@ -431,7 +445,7 @@ The following options are provided by the underlying [http-proxy](https://github ```javascript // verbose api -createProxyMiddleware('/', { target: 'http://echo.websocket.org', ws: true }); +createProxyMiddleware({ pathFilter: '/', target: 'http://echo.websocket.org', ws: true }); ``` ### External WebSocket upgrade @@ -439,7 +453,7 @@ createProxyMiddleware('/', { target: 'http://echo.websocket.org', ws: true }); In the previous WebSocket examples, http-proxy-middleware relies on a initial http request in order to listen to the http `upgrade` event. If you need to proxy WebSockets without the initial http request, you can subscribe to the server's http `upgrade` event manually. ```javascript -const wsProxy = createProxyMiddleware('ws://echo.websocket.org', { changeOrigin: true }); +const wsProxy = createProxyMiddleware({ target: 'ws://echo.websocket.org', changeOrigin: true }); const app = express(); app.use(wsProxy); diff --git a/examples/browser-sync/index.js b/examples/browser-sync/index.js index 1705647c..2342b3bd 100644 --- a/examples/browser-sync/index.js +++ b/examples/browser-sync/index.js @@ -7,8 +7,9 @@ const { createProxyMiddleware } = require('../../dist'); // require('http-proxy- /** * Configure proxy middleware */ -const jsonPlaceholderProxy = createProxyMiddleware('/users', { +const jsonPlaceholderProxy = createProxyMiddleware({ target: 'http://jsonplaceholder.typicode.com', + pathFilter: '/users', changeOrigin: true, // for vhosted sites, changes host header to match to target's host logLevel: 'debug', }); diff --git a/examples/websocket/index.html b/examples/websocket/index.html index 38674154..b295129c 100644 --- a/examples/websocket/index.html +++ b/examples/websocket/index.html @@ -21,7 +21,7 @@

WebSocket demo

-

Proxy ws://localhost:3000 to ws://echo.websocket.org

+

Proxy ws://localhost:3000 to ws://ws.ifelse.io

diff --git a/examples/websocket/index.js b/examples/websocket/index.js index 6104036a..061f88ae 100644 --- a/examples/websocket/index.js +++ b/examples/websocket/index.js @@ -7,8 +7,8 @@ const { createProxyMiddleware } = require('../../dist'); // require('http-proxy- /** * Configure proxy middleware */ -const wsProxy = createProxyMiddleware('/', { - target: 'http://echo.websocket.org', +const wsProxy = createProxyMiddleware({ + target: 'http://ws.ifelse.io', // pathRewrite: { // '^/websocket' : '/socket', // rewrite path. // '^/removepath' : '' // remove path. diff --git a/recipes/README.md b/recipes/README.md index ed325a7f..62e2d01f 100644 --- a/recipes/README.md +++ b/recipes/README.md @@ -12,15 +12,13 @@ http-proxy-middleware uses Nodejitsu's [http-proxy](https://github.com/nodejitsu const { createProxyMiddleware } = require('http-proxy-middleware'); const winston = require('winston'); -/** - * Context matching: decide which path(s) should be proxied. (wildcards supported) - **/ -const context = '/api'; - /** * Proxy options */ const options = { + // decide which path(s) should be proxied. (wildcards supported) + pathFilter: '/api', + // hostname to the target server target: 'http://localhost:3000', @@ -104,5 +102,5 @@ const options = { /** * Create the proxy middleware, so it can be used in a server. */ -const apiProxy = createProxyMiddleware(context, options); +const apiProxy = createProxyMiddleware(options); ``` diff --git a/recipes/basic.md b/recipes/basic.md index c22b0957..d1695d2a 100644 --- a/recipes/basic.md +++ b/recipes/basic.md @@ -5,10 +5,10 @@ This example will create a basic proxy middleware. ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); -const apiProxy = createProxyMiddleware('/api', { target: 'http://localhost:3000' }); -// \____/ \________________________________/ -// | | -// context options +const apiProxy = createProxyMiddleware({ + pathFilter: '/api', + target: 'http://localhost:3000', +}); ``` ## Alternative configuration diff --git a/recipes/context-matching.md b/recipes/pathFilter.md similarity index 69% rename from recipes/context-matching.md rename to recipes/pathFilter.md index 83150868..fdc0953a 100644 --- a/recipes/context-matching.md +++ b/recipes/pathFilter.md @@ -1,12 +1,12 @@ -# Context matching +# Path Filter Determine which requests should be proxied. -Context matching is optional and is useful in cases where you are not able to use the regular [middleware mounting](http://expressjs.com/en/4x/api.html#app.use). +`pathFilter` is optional and is useful in cases where you are not able to use the regular [middleware mounting](http://expressjs.com/en/4x/api.html#app.use). -The [RFC 3986 `path`](https://tools.ietf.org/html/rfc3986#section-3.3) is used for context matching. +The [RFC 3986 `path`](https://tools.ietf.org/html/rfc3986#section-3.3) is used for `pathFilter`. -``` +```text foo://example.com:8042/over/there?name=ferret#nose \_/ \______________/\_________/ \_________/ \__/ | | | | | @@ -15,8 +15,6 @@ The [RFC 3986 `path`](https://tools.ietf.org/html/rfc3986#section-3.3) is used f `http-proxy-middleware` offers several ways to do this: - - - [Path](#path) - [Multi Path](#multi-path) - [Wildcard](#wildcard) @@ -24,8 +22,6 @@ The [RFC 3986 `path`](https://tools.ietf.org/html/rfc3986#section-3.3) is used f - [Wildcard / Exclusion](#wildcard--exclusion) - [Custom filtering](#custom-filtering) - - ## Path This will match paths starting with `/api` @@ -33,7 +29,8 @@ This will match paths starting with `/api` ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); -const apiProxy = createProxyMiddleware('/api', { +const apiProxy = createProxyMiddleware({ + pathFilter: '/api', target: 'http://localhost:3000', }); @@ -47,7 +44,10 @@ This will match paths starting with `/api` or `/rest` ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); -const apiProxy = createProxyMiddleware(['/api', '/rest'], { target: 'http://localhost:3000' }); +const apiProxy = createProxyMiddleware({ + pathFilter: ['/api', '/rest'], + target: 'http://localhost:3000', +}); // `/api/foo/bar` -> `http://localhost:3000/api/foo/bar` // `/rest/lorum/ipsum` -> `http://localhost:3000/rest/lorum/ipsum` @@ -60,7 +60,8 @@ This will match paths starting with `/api/` and should also end with `.json` ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); -const apiProxy = createProxyMiddleware('/api/**/*.json', { +const apiProxy = createProxyMiddleware({ + pathFilter: '/api/**/*.json', target: 'http://localhost:3000', }); ``` @@ -72,26 +73,28 @@ Multiple wildcards can be used. ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); -const apiProxy = createProxyMiddleware(['/api/**/*.json', '/rest/**'], { +const apiProxy = createProxyMiddleware({ + pathFilter: ['/api/**/*.json', '/rest/**'], target: 'http://localhost:3000', }); ``` ## Wildcard / Exclusion -This example will create a proxy with wildcard context matching. +This example will create a proxy with globs. ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); -const apiProxy = createProxyMiddleware(['foo/*.js', '!bar.js'], { +const apiProxy = createProxyMiddleware({ + pathFilter: ['foo/*.js', '!bar.js'], target: 'http://localhost:3000', }); ``` ## Custom filtering -Write your custom context matching function to have full control on the matching behavior. +Write your custom `pathFilter` function to have full control on the matching behavior. The request `pathname` and `req` object are provided to determine which requests should be proxied or not. ```javascript @@ -101,5 +104,8 @@ const filter = function (pathname, req) { return pathname.match('^/api') && req.method === 'GET'; }; -const apiProxy = createProxyMiddleware(filter, { target: 'http://localhost:3000' }); +const apiProxy = createProxyMiddleware({ + pathFilter: filter, + target: 'http://localhost:3000', +}); ``` diff --git a/src/config-factory.ts b/src/config-factory.ts deleted file mode 100644 index 3053dfa2..00000000 --- a/src/config-factory.ts +++ /dev/null @@ -1,61 +0,0 @@ -import isPlainObj = require('is-plain-obj'); -import { ERRORS } from './errors'; -import { getInstance } from './logger'; -import { Filter, Options } from './types'; - -const logger = getInstance(); - -export type Config = { context: Filter; options: Options }; - -export function createConfig(context, opts?: Options): Config { - // structure of config object to be returned - const config: Config = { - context: undefined, - options: {} as Options, - }; - - // app.use('/api', proxy({target:'http://localhost:9000'})); - if (isContextless(context, opts)) { - config.context = '/'; - config.options = Object.assign(config.options, context); - - // app.use('/api', proxy('http://localhost:9000')); - // app.use(proxy('http://localhost:9000/api')); - } else { - config.context = context; - config.options = Object.assign(config.options, opts); - } - - configureLogger(config.options); - - if (!config.options.target && !config.options.router) { - throw new Error(ERRORS.ERR_CONFIG_FACTORY_TARGET_MISSING); - } - - return config; -} - -/** - * Checks if a Object only config is provided, without a context. - * In this case the all paths will be proxied. - * - * @example - * app.use('/api', proxy({target:'http://localhost:9000'})); - * - * @param {Object} context [description] - * @param {*} opts [description] - * @return {Boolean} [description] - */ -function isContextless(context: Filter, opts: Options) { - return isPlainObj(context) && (opts == null || Object.keys(opts).length === 0); -} - -function configureLogger(options: Options) { - if (options.logLevel) { - logger.setLevel(options.logLevel); - } - - if (options.logProvider) { - logger.setProvider(options.logProvider); - } -} diff --git a/src/configuration.ts b/src/configuration.ts new file mode 100644 index 00000000..54c55a08 --- /dev/null +++ b/src/configuration.ts @@ -0,0 +1,23 @@ +import { ERRORS } from './errors'; +import { getInstance } from './logger'; +import { Options } from './types'; + +const logger = getInstance(); + +export function verifyConfig(options: Options): void { + configureLogger(options); + + if (!options.target && !options.router) { + throw new Error(ERRORS.ERR_CONFIG_FACTORY_TARGET_MISSING); + } +} + +function configureLogger(options: Options): void { + if (options.logLevel) { + logger.setLevel(options.logLevel); + } + + if (options.logProvider) { + logger.setProvider(options.logProvider); + } +} diff --git a/src/errors.ts b/src/errors.ts index 2052ecfa..73dcdd81 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,6 +1,6 @@ export enum ERRORS { ERR_CONFIG_FACTORY_TARGET_MISSING = '[HPM] Missing "target" option. Example: {target: "http://www.example.org"}', ERR_CONTEXT_MATCHER_GENERIC = '[HPM] Invalid context. Expecting something like: "/api" or ["/api", "/ajax"]', - ERR_CONTEXT_MATCHER_INVALID_ARRAY = '[HPM] Invalid context. Expecting something like: ["/api", "/ajax"] or ["/api/**", "!**.html"]', + ERR_CONTEXT_MATCHER_INVALID_ARRAY = '[HPM] Invalid pathFilter. Expecting something like: ["/api", "/ajax"] or ["/api/**", "!**.html"]', ERR_PATH_REWRITER_CONFIG = '[HPM] Invalid pathRewrite config. Expecting object with pathRewrite config or a rewrite function', } diff --git a/src/http-proxy-middleware.ts b/src/http-proxy-middleware.ts index b93ae92f..32b68519 100644 --- a/src/http-proxy-middleware.ts +++ b/src/http-proxy-middleware.ts @@ -1,29 +1,29 @@ import type * as https from 'https'; import type * as express from 'express'; -import type { Filter, Request, RequestHandler, Response, Options } from './types'; +import type { Request, RequestHandler, Response, Options, Filter } from './types'; import * as httpProxy from 'http-proxy'; -import { createConfig, Config } from './config-factory'; -import * as contextMatcher from './context-matcher'; +import { verifyConfig } from './configuration'; +import { matchPathFilter } from './path-filter'; import * as handlers from './_handlers'; import { getArrow, getInstance } from './logger'; import * as PathRewriter from './path-rewriter'; import * as Router from './router'; + export class HttpProxyMiddleware { private logger = getInstance(); - private config: Config; private wsInternalSubscribed = false; private serverOnCloseSubscribed = false; private proxyOptions: Options; private proxy: httpProxy; private pathRewriter; - constructor(context: Filter | Options, opts?: Options) { - this.config = createConfig(context, opts); - this.proxyOptions = this.config.options; + constructor(options: Options) { + verifyConfig(options); + this.proxyOptions = options; // create proxy this.proxy = httpProxy.createProxyServer({}); - this.logger.info(`[HPM] Proxy created: ${this.config.context} -> ${this.proxyOptions.target}`); + this.logger.info(`[HPM] Proxy created: ${options.pathFilter ?? '/'} -> ${options.target}`); this.pathRewriter = PathRewriter.createPathRewriter(this.proxyOptions.pathRewrite); // returns undefined when "pathRewrite" is not provided @@ -48,7 +48,7 @@ export class HttpProxyMiddleware { res: Response, next: express.NextFunction ) => { - if (this.shouldProxy(this.config.context, req)) { + if (this.shouldProxy(this.proxyOptions.pathFilter, req)) { try { const activeProxyOptions = await this.prepareProxyRequest(req); this.proxy.web(req, res, activeProxyOptions); @@ -93,7 +93,7 @@ export class HttpProxyMiddleware { }; private handleUpgrade = async (req: Request, socket, head) => { - if (this.shouldProxy(this.config.context, req)) { + if (this.shouldProxy(this.proxyOptions.pathFilter, req)) { const activeProxyOptions = await this.prepareProxyRequest(req); this.proxy.ws(req, socket, head, activeProxyOptions); this.logger.info('[HPM] Upgrading to WebSocket'); @@ -102,15 +102,10 @@ export class HttpProxyMiddleware { /** * Determine whether request should be proxied. - * - * @private - * @param {String} context [description] - * @param {Object} req [description] - * @return {Boolean} */ - private shouldProxy = (context, req: Request): boolean => { + private shouldProxy = (pathFilter: Filter, req: Request): boolean => { const path = req.originalUrl || req.url; - return contextMatcher.match(context, path, req); + return matchPathFilter(pathFilter, path, req); }; /** diff --git a/src/index.ts b/src/index.ts index 79a95325..553f649b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,18 @@ import { HttpProxyMiddleware } from './http-proxy-middleware'; -import { Filter, Options } from './types'; +import { Options } from './types'; -export function createProxyMiddleware(context: Filter | Options, options?: Options) { - const { middleware } = new HttpProxyMiddleware(context, options); +export function createProxyMiddleware(options: Options) { + const { middleware } = new HttpProxyMiddleware(options); return middleware; } +/** + * @deprecated + */ +// export function legacyCreateProxyMiddleware(pathFilter: Filter, options: Options) { +// return createProxyMiddleware({ ...options, pathFilter }); +// } + export * from './handlers'; export { Filter, Options, RequestHandler } from './types'; diff --git a/src/context-matcher.ts b/src/path-filter.ts similarity index 55% rename from src/context-matcher.ts rename to src/path-filter.ts index 5a3ee7eb..37b1a5fb 100644 --- a/src/context-matcher.ts +++ b/src/path-filter.ts @@ -4,46 +4,46 @@ import * as micromatch from 'micromatch'; import * as url from 'url'; import { ERRORS } from './errors'; -export function match(context: Filter, uri: string, req: Request): boolean { +export function matchPathFilter(pathFilter: Filter = '/', uri: string, req: Request): boolean { // single path - if (isStringPath(context as string)) { - return matchSingleStringPath(context as string, uri); + if (isStringPath(pathFilter as string)) { + return matchSingleStringPath(pathFilter as string, uri); } // single glob path - if (isGlobPath(context as string)) { - return matchSingleGlobPath(context as string[], uri); + if (isGlobPath(pathFilter as string)) { + return matchSingleGlobPath(pathFilter as unknown as string[], uri); } // multi path - if (Array.isArray(context)) { - if (context.every(isStringPath)) { - return matchMultiPath(context, uri); + if (Array.isArray(pathFilter)) { + if (pathFilter.every(isStringPath)) { + return matchMultiPath(pathFilter, uri); } - if (context.every(isGlobPath)) { - return matchMultiGlobPath(context as string[], uri); + if (pathFilter.every(isGlobPath)) { + return matchMultiGlobPath(pathFilter as string[], uri); } throw new Error(ERRORS.ERR_CONTEXT_MATCHER_INVALID_ARRAY); } // custom matching - if (typeof context === 'function') { + if (typeof pathFilter === 'function') { const pathname = getUrlPathName(uri); - return context(pathname, req); + return pathFilter(pathname, req); } throw new Error(ERRORS.ERR_CONTEXT_MATCHER_GENERIC); } /** - * @param {String} context '/api' + * @param {String} pathFilter '/api' * @param {String} uri 'http://example.org/api/b/c/d.html' * @return {Boolean} */ -function matchSingleStringPath(context: string, uri: string) { +function matchSingleStringPath(pathFilter: string, uri: string) { const pathname = getUrlPathName(uri); - return pathname.indexOf(context) === 0; + return pathname.indexOf(pathFilter) === 0; } function matchSingleGlobPath(pattern: string | string[], uri: string) { @@ -57,14 +57,14 @@ function matchMultiGlobPath(patternList: string | string[], uri: string) { } /** - * @param {String} contextList ['/api', '/ajax'] + * @param {String} pathFilterList ['/api', '/ajax'] * @param {String} uri 'http://example.org/api/b/c/d.html' * @return {Boolean} */ -function matchMultiPath(contextList: string[], uri: string) { +function matchMultiPath(pathFilterList: string[], uri: string) { let isMultiPath = false; - for (const context of contextList) { + for (const context of pathFilterList) { if (matchSingleStringPath(context, uri)) { isMultiPath = true; break; @@ -84,10 +84,10 @@ function getUrlPathName(uri: string) { return uri && url.parse(uri).pathname; } -function isStringPath(context: string) { - return typeof context === 'string' && !isGlob(context); +function isStringPath(pathFilter: string) { + return typeof pathFilter === 'string' && !isGlob(pathFilter); } -function isGlobPath(context: string) { - return isGlob(context); +function isGlobPath(pathFilter: string) { + return isGlob(pathFilter); } diff --git a/src/types.ts b/src/types.ts index 55628aab..31e4ad66 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,6 +21,7 @@ export interface RequestHandler extends express.RequestHandler { export type Filter = string | string[] | ((pathname: string, req: Request) => boolean); export interface Options extends httpProxy.ServerOptions { + pathFilter?: Filter; pathRewrite?: | { [regexp: string]: string } | ((path: string, req: Request) => string) diff --git a/test/e2e/express-router.spec.ts b/test/e2e/express-router.spec.ts index 611247be..225e4555 100644 --- a/test/e2e/express-router.spec.ts +++ b/test/e2e/express-router.spec.ts @@ -30,8 +30,9 @@ describe('Usage in Express', () => { changeOrigin: true, logLevel: 'silent', target: 'http://jsonplaceholder.typicode.com', + pathFilter: filter, }; - sub.use(createProxyMiddleware(filter, proxyConfig)); + sub.use(createProxyMiddleware(proxyConfig)); sub.get('/hello', jsonMiddleware({ content: 'foobar' })); diff --git a/test/e2e/http-proxy-middleware.spec.ts b/test/e2e/http-proxy-middleware.spec.ts index 772430c1..999aa26b 100644 --- a/test/e2e/http-proxy-middleware.spec.ts +++ b/test/e2e/http-proxy-middleware.spec.ts @@ -8,28 +8,30 @@ import * as bodyParser from 'body-parser'; describe('E2E http-proxy-middleware', () => { describe('http-proxy-middleware creation', () => { it('should create a middleware', () => { - const middleware = createProxyMiddleware('/api', { + const middleware = createProxyMiddleware({ target: `http://localhost:8000`, + pathFilter: '/api', }); expect(typeof middleware).toBe('function'); }); }); - describe('context matching', () => { + describe('pathFilter matching', () => { describe('do not proxy', () => { const mockReq: Request = { url: '/foo/bar', originalUrl: '/foo/bar' } as Request; const mockRes: Response = {} as Response; const mockNext: NextFunction = jest.fn(); beforeEach(() => { - const middleware = createProxyMiddleware('/api', { + const middleware = createProxyMiddleware({ target: `http://localhost:8000`, + pathFilter: '/api', }); middleware(mockReq, mockRes, mockNext); }); - it('should not proxy requests when request url does not match context', () => { + it('should not proxy requests when request url does not match pathFilter', () => { expect(mockNext).toBeCalled(); }); }); @@ -52,8 +54,9 @@ describe('E2E http-proxy-middleware', () => { beforeEach(() => { agent = request( createApp( - createProxyMiddleware('/api', { + createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, + pathFilter: '/api', }) ) ); @@ -84,8 +87,9 @@ describe('E2E http-proxy-middleware', () => { agent = request( createApp( bodyParser.urlencoded({ extended: false }), - createProxyMiddleware('/api', { + createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, + pathFilter: '/api', onProxyReq: fixRequestBody, }) ) @@ -102,8 +106,9 @@ describe('E2E http-proxy-middleware', () => { agent = request( createApp( bodyParser.json(), - createProxyMiddleware('/api', { + createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, + pathFilter: '/api', onProxyReq: fixRequestBody, }) ) @@ -117,7 +122,7 @@ describe('E2E http-proxy-middleware', () => { }); }); - describe('custom context matcher/filter', () => { + describe('custom pathFilter matcher/filter', () => { it('should have response body: "HELLO WEB"', async () => { const filter = (path, req) => { return true; @@ -125,8 +130,9 @@ describe('E2E http-proxy-middleware', () => { agent = request( createApp( - createProxyMiddleware(filter, { + createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, + pathFilter: filter, }) ) ); @@ -143,8 +149,9 @@ describe('E2E http-proxy-middleware', () => { agent = request( createApp( - createProxyMiddleware(filter, { + createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, + pathFilter: filter, }) ) ); @@ -159,8 +166,9 @@ describe('E2E http-proxy-middleware', () => { beforeEach(() => { agent = request( createApp( - createProxyMiddleware(['/api', '/ajax'], { + createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, + pathFilter: ['/api', '/ajax'], }) ) ); @@ -188,8 +196,9 @@ describe('E2E http-proxy-middleware', () => { beforeEach(() => { agent = request( createApp( - createProxyMiddleware('/api/**', { + createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, + pathFilter: '/api/**', }) ) ); @@ -206,8 +215,9 @@ describe('E2E http-proxy-middleware', () => { beforeEach(() => { agent = request( createApp( - createProxyMiddleware(['**/*.html', '!**.json'], { + createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, + pathFilter: ['**/*.html', '!**.json'], }) ) ); @@ -230,8 +240,9 @@ describe('E2E http-proxy-middleware', () => { beforeEach(() => { agent = request( createApp( - createProxyMiddleware('/api', { + createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, + pathFilter: '/api', headers: { host: 'foobar.dev' }, }) ) @@ -301,8 +312,9 @@ describe('E2E http-proxy-middleware', () => { beforeEach(() => { agent = request( createApp( - createProxyMiddleware('/api', { + createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, + pathFilter: '/api', onProxyRes(proxyRes, req, res) { // tslint:disable-next-line: no-string-literal proxyRes['headers']['x-added'] = 'foobar'; // add custom header to response @@ -338,8 +350,9 @@ describe('E2E http-proxy-middleware', () => { beforeEach(() => { agent = request( createApp( - createProxyMiddleware('/api', { + createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, + pathFilter: '/api', onProxyReq(proxyReq, req, res) { proxyReq.setHeader('x-added', 'added-from-hpm'); // add custom header to request }, @@ -417,8 +430,9 @@ describe('E2E http-proxy-middleware', () => { agent = request( createApp( - createProxyMiddleware('/api', { + createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, + pathFilter: '/api', logLevel: 'info', logProvider(provider) { return { ...provider, debug: customLogger, info: customLogger }; diff --git a/test/e2e/websocket.spec.ts b/test/e2e/websocket.spec.ts index d3f15e8a..907709b5 100644 --- a/test/e2e/websocket.spec.ts +++ b/test/e2e/websocket.spec.ts @@ -33,7 +33,7 @@ describe('E2E WebSocket proxy', () => { }); beforeEach(() => { - proxyMiddleware = createProxyMiddleware('/', { + proxyMiddleware = createProxyMiddleware({ target: `http://localhost:${WS_SERVER_PORT}`, ws: true, pathRewrite: { '^/socket': '' }, @@ -100,7 +100,8 @@ describe('E2E WebSocket proxy', () => { // override proxyServer = createApp( // cSpell:ignore notworkinghost - createProxyMiddleware('ws://notworkinghost:6789', { + createProxyMiddleware({ + target: 'ws://notworkinghost:6789', router: { '/socket': `ws://localhost:${WS_SERVER_PORT}` }, pathRewrite: { '^/socket': '' }, }) diff --git a/test/types.spec.ts b/test/types.spec.ts index a4b82630..af6bba9c 100644 --- a/test/types.spec.ts +++ b/test/types.spec.ts @@ -17,22 +17,22 @@ describe('http-proxy-middleware TypeScript Types', () => { describe('HPM Filters', () => { it('should create proxy with path filter', () => { - const proxy = middleware('/path', options); + const proxy = middleware({ ...options, pathFilter: '/api' }); expect(proxy).toBeDefined(); }); it('should create proxy with glob filter', () => { - const proxy = middleware(['/path/**'], options); + const proxy = middleware({ ...options, pathFilter: ['/path/**'] }); expect(proxy).toBeDefined(); }); it('should create proxy with custom filter', () => { - const proxy = middleware((path, req) => true, options); + const proxy = middleware({ ...options, pathFilter: (path, req) => true }); expect(proxy).toBeDefined(); }); it('should create proxy with manual websocket upgrade function', () => { - const proxy = middleware((path, req) => true, options); + const proxy = middleware({ ...options, pathFilter: (path, req) => true }); expect(proxy.upgrade).toBeDefined(); }); }); diff --git a/test/unit/config-factory.spec.ts b/test/unit/config-factory.spec.ts deleted file mode 100644 index 610ec3bc..00000000 --- a/test/unit/config-factory.spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { createConfig } from '../../src/config-factory'; - -describe('configFactory', () => { - let result; - // var createConfig = configFactory.createConfig; - - describe('createConfig()', () => { - describe('classic config', () => { - const context = '/api'; - const options = { target: 'http://www.example.org' }; - - beforeEach(() => { - result = createConfig(context, options); - }); - - it('should return config object', () => { - expect(Object.keys(result)).toEqual(['context', 'options']); - }); - - it('should return config object with context', () => { - expect(result.context).toBe(context); - }); - - it('should return config object with options', () => { - expect(result.options).toEqual(options); - }); - }); - - describe('Object config', () => { - beforeEach(() => { - result = createConfig({ target: 'http://www.example.org:8000' }); - }); - - it('should set the proxy path to everything', () => { - expect(result.context).toBe('/'); - }); - - it('should return config object', () => { - expect(result.options).toEqual({ - target: 'http://www.example.org:8000', - }); - }); - }); - - describe('missing option.target', () => { - let fn; - - beforeEach(() => { - fn = () => { - createConfig('/api'); - }; - }); - - it('should throw an error when target and router option are missing', () => { - expect(fn).toThrowError(Error); - }); - }); - - describe('optional option.target when option.router is used', () => { - let fn; - - beforeEach(() => { - fn = () => { - createConfig('/api', { - router: (req) => 'http://www.example.com', - }); - }; - }); - - it('should not throw an error when target option is missing when router is used', () => { - expect(fn).not.toThrowError(Error); - }); - }); - }); -}); diff --git a/test/unit/configuration.spec.ts b/test/unit/configuration.spec.ts new file mode 100644 index 00000000..5bae306b --- /dev/null +++ b/test/unit/configuration.spec.ts @@ -0,0 +1,36 @@ +import { verifyConfig } from '../../src/configuration'; + +describe('configFactory', () => { + describe('verifyConfig()', () => { + describe('missing option.target', () => { + let fn; + + beforeEach(() => { + fn = () => { + verifyConfig({ pathFilter: '/api' }); + }; + }); + + it('should throw an error when target and router option are missing', () => { + expect(fn).toThrowError(Error); + }); + }); + + describe('optional option.target when option.router is used', () => { + let fn; + + beforeEach(() => { + fn = () => { + verifyConfig({ + pathFilter: '/api', + router: (req) => 'http://www.example.com', + }); + }; + }); + + it('should not throw an error when target option is missing when router is used', () => { + expect(fn).not.toThrowError(Error); + }); + }); + }); +}); diff --git a/test/unit/context-matcher.spec.ts b/test/unit/context-matcher.spec.ts deleted file mode 100644 index 789d9b42..00000000 --- a/test/unit/context-matcher.spec.ts +++ /dev/null @@ -1,266 +0,0 @@ -import type { Request } from '../../src/types'; -import * as contextMatcher from '../../src/context-matcher'; - -describe('Context Matching', () => { - const fakeReq = {} as Request; - - describe('String path matching', () => { - let result; - - describe('Single path matching', () => { - it('should match all paths', () => { - result = contextMatcher.match('', 'http://localhost/api/foo/bar', fakeReq); - expect(result).toBe(true); - }); - - it('should match all paths starting with forward-slash', () => { - result = contextMatcher.match('/', 'http://localhost/api/foo/bar', fakeReq); - expect(result).toBe(true); - }); - - it('should return true when the context is present in url', () => { - result = contextMatcher.match('/api', 'http://localhost/api/foo/bar', fakeReq); - expect(result).toBe(true); - }); - - it('should return false when the context is not present in url', () => { - result = contextMatcher.match('/abc', 'http://localhost/api/foo/bar', fakeReq); - expect(result).toBe(false); - }); - - it('should return false when the context is present half way in url', () => { - result = contextMatcher.match('/foo', 'http://localhost/api/foo/bar', fakeReq); - expect(result).toBe(false); - }); - - it('should return false when the context does not start with /', () => { - result = contextMatcher.match('api', 'http://localhost/api/foo/bar', fakeReq); - expect(result).toBe(false); - }); - }); - - describe('Multi path matching', () => { - it('should return true when the context is present in url', () => { - result = contextMatcher.match(['/api'], 'http://localhost/api/foo/bar', fakeReq); - expect(result).toBe(true); - }); - - it('should return true when the context is present in url', () => { - result = contextMatcher.match(['/api', '/ajax'], 'http://localhost/ajax/foo/bar', fakeReq); - expect(result).toBe(true); - }); - - it('should return false when the context does not match url', () => { - result = contextMatcher.match(['/api', '/ajax'], 'http://localhost/foo/bar', fakeReq); - expect(result).toBe(false); - }); - - it('should return false when empty array provided', () => { - result = contextMatcher.match([], 'http://localhost/api/foo/bar', fakeReq); - expect(result).toBe(false); - }); - }); - }); - - describe('Wildcard path matching', () => { - describe('Single glob', () => { - let url; - - beforeEach(() => { - url = 'http://localhost/api/foo/bar.html'; - }); - - describe('url-path matching', () => { - it('should match any path', () => { - expect(contextMatcher.match('**', url, fakeReq)).toBe(true); - expect(contextMatcher.match('/**', url, fakeReq)).toBe(true); - }); - - it('should only match paths starting with "/api" ', () => { - expect(contextMatcher.match('/api/**', url, fakeReq)).toBe(true); - expect(contextMatcher.match('/ajax/**', url, fakeReq)).toBe(false); - }); - - it('should only match paths starting with "foo" folder in it ', () => { - expect(contextMatcher.match('**/foo/**', url, fakeReq)).toBe(true); - expect(contextMatcher.match('**/invalid/**', url, fakeReq)).toBe(false); - }); - }); - - describe('file matching', () => { - it('should match any path, file and extension', () => { - expect(contextMatcher.match('**', url, fakeReq)).toBe(true); - expect(contextMatcher.match('**/*', url, fakeReq)).toBe(true); - expect(contextMatcher.match('**/*.*', url, fakeReq)).toBe(true); - expect(contextMatcher.match('/**', url, fakeReq)).toBe(true); - expect(contextMatcher.match('/**/*', url, fakeReq)).toBe(true); - expect(contextMatcher.match('/**/*.*', url, fakeReq)).toBe(true); - }); - - it('should only match .html files', () => { - expect(contextMatcher.match('**/*.html', url, fakeReq)).toBe(true); - expect(contextMatcher.match('/**/*.html', url, fakeReq)).toBe(true); - expect(contextMatcher.match('/*.htm', url, fakeReq)).toBe(false); - expect(contextMatcher.match('/*.jpg', url, fakeReq)).toBe(false); - }); - - it('should only match .html under root path', () => { - const pattern = '/*.html'; - expect(contextMatcher.match(pattern, 'http://localhost/index.html', fakeReq)).toBe(true); - expect( - contextMatcher.match(pattern, 'http://localhost/some/path/index.html', fakeReq) - ).toBe(false); - }); - - it('should ignore query params', () => { - expect( - contextMatcher.match('/**/*.php', 'http://localhost/a/b/c.php?d=e&e=f', fakeReq) - ).toBe(true); - expect( - contextMatcher.match('/**/*.php?*', 'http://localhost/a/b/c.php?d=e&e=f', fakeReq) - ).toBe(false); - }); - - it('should only match any file in root path', () => { - expect(contextMatcher.match('/*', 'http://localhost/bar.html', fakeReq)).toBe(true); - expect(contextMatcher.match('/*.*', 'http://localhost/bar.html', fakeReq)).toBe(true); - expect(contextMatcher.match('/*', 'http://localhost/foo/bar.html', fakeReq)).toBe(false); - }); - - it('should only match .html file is in root path', () => { - expect(contextMatcher.match('/*.html', 'http://localhost/bar.html', fakeReq)).toBe(true); - expect( - contextMatcher.match('/*.html', 'http://localhost/api/foo/bar.html', fakeReq) - ).toBe(false); - }); - - it('should only match .html files in "foo" folder', () => { - expect(contextMatcher.match('**/foo/*.html', url, fakeReq)).toBe(true); - expect(contextMatcher.match('**/bar/*.html', url, fakeReq)).toBe(false); - }); - - it('should not match .html files', () => { - expect(contextMatcher.match('!**/*.html', url, fakeReq)).toBe(false); - }); - }); - }); - - describe('Multi glob matching', () => { - describe('Multiple patterns', () => { - it('should return true when both path patterns match', () => { - const pattern = ['/api/**', '/ajax/**']; - expect(contextMatcher.match(pattern, 'http://localhost/api/foo/bar.json', fakeReq)).toBe( - true - ); - expect(contextMatcher.match(pattern, 'http://localhost/ajax/foo/bar.json', fakeReq)).toBe( - true - ); - expect(contextMatcher.match(pattern, 'http://localhost/rest/foo/bar.json', fakeReq)).toBe( - false - ); - }); - it('should return true when both file extensions pattern match', () => { - const pattern = ['/**/*.html', '/**/*.jpeg']; - expect(contextMatcher.match(pattern, 'http://localhost/api/foo/bar.html', fakeReq)).toBe( - true - ); - expect(contextMatcher.match(pattern, 'http://localhost/api/foo/bar.jpeg', fakeReq)).toBe( - true - ); - expect(contextMatcher.match(pattern, 'http://localhost/api/foo/bar.gif', fakeReq)).toBe( - false - ); - }); - }); - - describe('Negation patterns', () => { - it('should not match file extension', () => { - const url = 'http://localhost/api/foo/bar.html'; - expect(contextMatcher.match(['**', '!**/*.html'], url, fakeReq)).toBe(false); - expect(contextMatcher.match(['**', '!**/*.json'], url, fakeReq)).toBe(true); - }); - }); - }); - }); - - describe('Use function for matching', () => { - const testFunctionAsContext = (val) => { - return contextMatcher.match(fn, 'http://localhost/api/foo/bar', fakeReq); - - function fn(path, req) { - return val; - } - }; - - describe('truthy', () => { - it('should match when function returns true', () => { - expect(testFunctionAsContext(true)).toBeTruthy(); - expect(testFunctionAsContext('true')).toBeTruthy(); - }); - }); - - describe('falsy', () => { - it('should not match when function returns falsy value', () => { - expect(testFunctionAsContext(undefined)).toBeFalsy(); - expect(testFunctionAsContext(false)).toBeFalsy(); - expect(testFunctionAsContext('')).toBeFalsy(); - }); - }); - }); - - describe('Test invalid contexts', () => { - let testContext; - - beforeEach(() => { - testContext = (context) => { - return () => { - contextMatcher.match(context, 'http://localhost/api/foo/bar', fakeReq); - }; - }; - }); - - describe('Throw error', () => { - it('should throw error with undefined', () => { - expect(testContext(undefined)).toThrowError(Error); - }); - - it('should throw error with null', () => { - expect(testContext(null)).toThrowError(Error); - }); - - it('should throw error with object literal', () => { - expect(testContext(fakeReq)).toThrowError(Error); - }); - - it('should throw error with integers', () => { - expect(testContext(123)).toThrowError(Error); - }); - - it('should throw error with mixed string and glob pattern', () => { - expect(testContext(['/api', '!*.html'])).toThrowError(Error); - }); - }); - - describe('Do not throw error', () => { - it('should not throw error with string', () => { - expect(testContext('/123')).not.toThrowError(Error); - }); - - it('should not throw error with Array', () => { - expect(testContext(['/123'])).not.toThrowError(Error); - }); - it('should not throw error with glob', () => { - expect(testContext('/**')).not.toThrowError(Error); - }); - - it('should not throw error with Array of globs', () => { - expect(testContext(['/**', '!*.html'])).not.toThrowError(Error); - }); - - it('should not throw error with Function', () => { - // eslint-disable-next-line @typescript-eslint/no-empty-function - expect(testContext(() => {})).not.toThrowError(Error); - }); - }); - }); -}); diff --git a/test/unit/path-filter.spec.ts b/test/unit/path-filter.spec.ts new file mode 100644 index 00000000..66721d62 --- /dev/null +++ b/test/unit/path-filter.spec.ts @@ -0,0 +1,254 @@ +import type { Request } from '../../src/types'; +import { matchPathFilter } from '../../src/path-filter'; + +describe('Path Filter', () => { + const fakeReq = {} as Request; + + describe('String path matching', () => { + let result; + + describe('Single path matching', () => { + it('should match all paths', () => { + result = matchPathFilter('', 'http://localhost/api/foo/bar', fakeReq); + expect(result).toBe(true); + }); + + it('should match all paths starting with forward-slash', () => { + result = matchPathFilter('/', 'http://localhost/api/foo/bar', fakeReq); + expect(result).toBe(true); + }); + + it('should return true when the pathFilter is present in url', () => { + result = matchPathFilter('/api', 'http://localhost/api/foo/bar', fakeReq); + expect(result).toBe(true); + }); + + it('should return false when the pathFilter is not present in url', () => { + result = matchPathFilter('/abc', 'http://localhost/api/foo/bar', fakeReq); + expect(result).toBe(false); + }); + + it('should return false when the pathFilter is present half way in url', () => { + result = matchPathFilter('/foo', 'http://localhost/api/foo/bar', fakeReq); + expect(result).toBe(false); + }); + + it('should return false when the pathFilter does not start with /', () => { + result = matchPathFilter('api', 'http://localhost/api/foo/bar', fakeReq); + expect(result).toBe(false); + }); + }); + + describe('Multi path matching', () => { + it('should return true when the pathFilter is present in url', () => { + result = matchPathFilter(['/api'], 'http://localhost/api/foo/bar', fakeReq); + expect(result).toBe(true); + }); + + it('should return true when the pathFilter is present in url', () => { + result = matchPathFilter(['/api', '/ajax'], 'http://localhost/ajax/foo/bar', fakeReq); + expect(result).toBe(true); + }); + + it('should return false when the pathFilter does not match url', () => { + result = matchPathFilter(['/api', '/ajax'], 'http://localhost/foo/bar', fakeReq); + expect(result).toBe(false); + }); + + it('should return false when empty array provided', () => { + result = matchPathFilter([], 'http://localhost/api/foo/bar', fakeReq); + expect(result).toBe(false); + }); + }); + }); + + describe('Wildcard path matching', () => { + describe('Single glob', () => { + let url; + + beforeEach(() => { + url = 'http://localhost/api/foo/bar.html'; + }); + + describe('url-path matching', () => { + it('should match any path', () => { + expect(matchPathFilter('**', url, fakeReq)).toBe(true); + expect(matchPathFilter('/**', url, fakeReq)).toBe(true); + }); + + it('should only match paths starting with "/api" ', () => { + expect(matchPathFilter('/api/**', url, fakeReq)).toBe(true); + expect(matchPathFilter('/ajax/**', url, fakeReq)).toBe(false); + }); + + it('should only match paths starting with "foo" folder in it ', () => { + expect(matchPathFilter('**/foo/**', url, fakeReq)).toBe(true); + expect(matchPathFilter('**/invalid/**', url, fakeReq)).toBe(false); + }); + }); + + describe('file matching', () => { + it('should match any path, file and extension', () => { + expect(matchPathFilter('**', url, fakeReq)).toBe(true); + expect(matchPathFilter('**/*', url, fakeReq)).toBe(true); + expect(matchPathFilter('**/*.*', url, fakeReq)).toBe(true); + expect(matchPathFilter('/**', url, fakeReq)).toBe(true); + expect(matchPathFilter('/**/*', url, fakeReq)).toBe(true); + expect(matchPathFilter('/**/*.*', url, fakeReq)).toBe(true); + }); + + it('should only match .html files', () => { + expect(matchPathFilter('**/*.html', url, fakeReq)).toBe(true); + expect(matchPathFilter('/**/*.html', url, fakeReq)).toBe(true); + expect(matchPathFilter('/*.htm', url, fakeReq)).toBe(false); + expect(matchPathFilter('/*.jpg', url, fakeReq)).toBe(false); + }); + + it('should only match .html under root path', () => { + const pattern = '/*.html'; + expect(matchPathFilter(pattern, 'http://localhost/index.html', fakeReq)).toBe(true); + expect(matchPathFilter(pattern, 'http://localhost/some/path/index.html', fakeReq)).toBe( + false + ); + }); + + it('should ignore query params', () => { + expect(matchPathFilter('/**/*.php', 'http://localhost/a/b/c.php?d=e&e=f', fakeReq)).toBe( + true + ); + expect( + matchPathFilter('/**/*.php?*', 'http://localhost/a/b/c.php?d=e&e=f', fakeReq) + ).toBe(false); + }); + + it('should only match any file in root path', () => { + expect(matchPathFilter('/*', 'http://localhost/bar.html', fakeReq)).toBe(true); + expect(matchPathFilter('/*.*', 'http://localhost/bar.html', fakeReq)).toBe(true); + expect(matchPathFilter('/*', 'http://localhost/foo/bar.html', fakeReq)).toBe(false); + }); + + it('should only match .html file is in root path', () => { + expect(matchPathFilter('/*.html', 'http://localhost/bar.html', fakeReq)).toBe(true); + expect(matchPathFilter('/*.html', 'http://localhost/api/foo/bar.html', fakeReq)).toBe( + false + ); + }); + + it('should only match .html files in "foo" folder', () => { + expect(matchPathFilter('**/foo/*.html', url, fakeReq)).toBe(true); + expect(matchPathFilter('**/bar/*.html', url, fakeReq)).toBe(false); + }); + + it('should not match .html files', () => { + expect(matchPathFilter('!**/*.html', url, fakeReq)).toBe(false); + }); + }); + }); + + describe('Multi glob matching', () => { + describe('Multiple patterns', () => { + it('should return true when both path patterns match', () => { + const pattern = ['/api/**', '/ajax/**']; + expect(matchPathFilter(pattern, 'http://localhost/api/foo/bar.json', fakeReq)).toBe(true); + expect(matchPathFilter(pattern, 'http://localhost/ajax/foo/bar.json', fakeReq)).toBe( + true + ); + expect(matchPathFilter(pattern, 'http://localhost/rest/foo/bar.json', fakeReq)).toBe( + false + ); + }); + it('should return true when both file extensions pattern match', () => { + const pattern = ['/**/*.html', '/**/*.jpeg']; + expect(matchPathFilter(pattern, 'http://localhost/api/foo/bar.html', fakeReq)).toBe(true); + expect(matchPathFilter(pattern, 'http://localhost/api/foo/bar.jpeg', fakeReq)).toBe(true); + expect(matchPathFilter(pattern, 'http://localhost/api/foo/bar.gif', fakeReq)).toBe(false); + }); + }); + + describe('Negation patterns', () => { + it('should not match file extension', () => { + const url = 'http://localhost/api/foo/bar.html'; + expect(matchPathFilter(['**', '!**/*.html'], url, fakeReq)).toBe(false); + expect(matchPathFilter(['**', '!**/*.json'], url, fakeReq)).toBe(true); + }); + }); + }); + }); + + describe('Use function for matching', () => { + const testFunctionAsPathFilter = (val) => { + return matchPathFilter(fn, 'http://localhost/api/foo/bar', fakeReq); + + function fn(path, req) { + return val; + } + }; + + describe('truthy', () => { + it('should match when function returns true', () => { + expect(testFunctionAsPathFilter(true)).toBeTruthy(); + expect(testFunctionAsPathFilter('true')).toBeTruthy(); + }); + }); + + describe('falsy', () => { + it('should not match when function returns falsy value', () => { + expect(testFunctionAsPathFilter(undefined)).toBeFalsy(); + expect(testFunctionAsPathFilter(false)).toBeFalsy(); + expect(testFunctionAsPathFilter('')).toBeFalsy(); + }); + }); + }); + + describe('Test invalid pathFilters', () => { + let testPathFilter; + + beforeEach(() => { + testPathFilter = (pathFilter) => { + return () => { + matchPathFilter(pathFilter, 'http://localhost/api/foo/bar', fakeReq); + }; + }; + }); + + describe('Throw error', () => { + it('should throw error with null', () => { + expect(testPathFilter(null)).toThrowError(Error); + }); + + it('should throw error with object literal', () => { + expect(testPathFilter(fakeReq)).toThrowError(Error); + }); + + it('should throw error with integers', () => { + expect(testPathFilter(123)).toThrowError(Error); + }); + + it('should throw error with mixed string and glob pattern', () => { + expect(testPathFilter(['/api', '!*.html'])).toThrowError(Error); + }); + }); + + describe('Do not throw error', () => { + it('should not throw error with string', () => { + expect(testPathFilter('/123')).not.toThrowError(Error); + }); + + it('should not throw error with Array', () => { + expect(testPathFilter(['/123'])).not.toThrowError(Error); + }); + it('should not throw error with glob', () => { + expect(testPathFilter('/**')).not.toThrowError(Error); + }); + + it('should not throw error with Array of globs', () => { + expect(testPathFilter(['/**', '!*.html'])).not.toThrowError(Error); + }); + + it('should not throw error with Function', () => { + // eslint-disable-next-line @typescript-eslint/no-empty-function + expect(testPathFilter(() => {})).not.toThrowError(Error); + }); + }); + }); +}); From 1ddcd6b06bf11e13c0a7609683e1634cc721548a Mon Sep 17 00:00:00 2001 From: cdaringe Date: Sat, 12 Mar 2022 11:49:59 -0800 Subject: [PATCH 07/10] fix: close server --- test/e2e/servers/http.spec.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/e2e/servers/http.spec.ts b/test/e2e/servers/http.spec.ts index f46ae4e6..f1b18412 100644 --- a/test/e2e/servers/http.spec.ts +++ b/test/e2e/servers/http.spec.ts @@ -5,10 +5,16 @@ import * as request from 'supertest'; import * as getPort from 'get-port'; describe('http integration', () => { + let server: http.Server | null = null; + + afterEach(() => { + server?.close(); + }); + it('should work with raw node http RequestHandler', async () => { await new Promise(async (resolve, reject) => { const port = await getPort(); - const server = http + server = http .createServer((req, res) => { const proxyConfig: Options = { changeOrigin: true, From 878986acdbeb263dd13a91d3b9cdc1a402686cdc Mon Sep 17 00:00:00 2001 From: cdaringe Date: Sun, 13 Mar 2022 13:37:06 -0700 Subject: [PATCH 08/10] refactor: decouple from express --- jest.config.js | 1 + jest.setup.js | 2 + src/_handlers.ts | 3 +- src/handlers/fix-request-body.ts | 12 ++- src/http-proxy-middleware.ts | 37 ++++--- src/path-filter.ts | 64 ++++++------ src/router.ts | 3 +- src/types.ts | 24 ++--- src/url.ts | 11 +++ test/e2e/http-proxy-middleware.spec.ts | 16 ++- test/unit/fix-request-body.spec.ts | 4 +- test/unit/path-filter.spec.ts | 131 ++++++++++++++----------- 12 files changed, 176 insertions(+), 132 deletions(-) create mode 100644 jest.setup.js create mode 100644 src/url.ts diff --git a/jest.config.js b/jest.config.js index e5233956..7e55de7a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,4 +3,5 @@ module.exports = { testEnvironment: 'node', coverageReporters: ['text', 'lcov'], collectCoverageFrom: ['src/**/*.*'], + setupFilesAfterEnv: ['/jest.setup.js'], }; diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 00000000..8d926217 --- /dev/null +++ b/jest.setup.js @@ -0,0 +1,2 @@ +console.log = jest.fn(); +console.info = jest.fn(); diff --git a/src/_handlers.ts b/src/_handlers.ts index 4a8b6be2..312b82db 100644 --- a/src/_handlers.ts +++ b/src/_handlers.ts @@ -1,6 +1,7 @@ import type { Request, Response, Options } from './types'; import type * as httpProxy from 'http-proxy'; import { getInstance } from './logger'; +import { getUrl } from './url'; const logger = getInstance(); export function init(proxy: httpProxy, option: Options): void { @@ -78,7 +79,7 @@ function defaultErrorHandler(err, req: Request, res: Response) { } } - res.end(`Error occurred while trying to proxy: ${host}${req.url}`); + res.end(`Error occurred while trying to proxy: ${host}${getUrl(req)}`); } function logClose(req, socket, head) { diff --git a/src/handlers/fix-request-body.ts b/src/handlers/fix-request-body.ts index 24108511..d38fe16a 100644 --- a/src/handlers/fix-request-body.ts +++ b/src/handlers/fix-request-body.ts @@ -2,17 +2,25 @@ import type * as http from 'http'; import type { Request } from '../types'; import * as querystring from 'querystring'; +type BodyParserRequest = { + body?: Record; +}; + /** * Fix proxied body if bodyParser is involved. + * + * {@link https://www.npmjs.com/package/body-parser} + * {@link https://github.com/chimurai/http-proxy-middleware/pull/492} */ export function fixRequestBody(proxyReq: http.ClientRequest, req: Request): void { - const requestBody = req.body; + const requestBody = (req as BodyParserRequest).body; if (!requestBody) { return; } - const contentType = proxyReq.getHeader('Content-Type') as string; + const contentTypeRaw = proxyReq.getHeader('Content-Type'); + const contentType = typeof contentTypeRaw === 'string' ? contentTypeRaw : null; const writeBody = (bodyData: string) => { // deepcode ignore ContentLengthInCode: bodyParser fix proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData)); diff --git a/src/http-proxy-middleware.ts b/src/http-proxy-middleware.ts index d20964f1..cd32e199 100644 --- a/src/http-proxy-middleware.ts +++ b/src/http-proxy-middleware.ts @@ -1,6 +1,6 @@ import type * as https from 'https'; -import type { Request, Response, Options, Filter, RequestMiddleware } from './types'; +import type { Request, Response, Options, RequestMiddleware } from './types'; import * as httpProxy from 'http-proxy'; import { verifyConfig } from './configuration'; import { matchPathFilter } from './path-filter'; @@ -8,6 +8,8 @@ import * as handlers from './_handlers'; import { getArrow, getInstance } from './logger'; import * as PathRewriter from './path-rewriter'; import * as Router from './router'; +import { getUrl } from './url'; +import { IncomingMessage } from 'http'; export class HttpProxyMiddleware { private logger = getInstance(); @@ -98,10 +100,7 @@ export class HttpProxyMiddleware { /** * Determine whether request should be proxied. */ - private shouldProxy = (pathFilter: Filter, req: Request): boolean => { - const path = req.originalUrl || req.url; - return matchPathFilter(pathFilter, path, req); - }; + private shouldProxy = matchPathFilter; /** * Apply option.router and option.pathRewrite @@ -112,12 +111,17 @@ export class HttpProxyMiddleware { * @return {Object} proxy options */ private prepareProxyRequest = async (req: Request) => { + /** + * Store pathname before it gets rewritten for logging + * @warn May be hazardous to express users. HPM, Express, & + * http.IncomingMessage all write to this field. + */ + const originalPath = getUrl(req); + // https://github.com/chimurai/http-proxy-middleware/issues/17 // https://github.com/chimurai/http-proxy-middleware/issues/94 - req.url = req.originalUrl || req.url; + (req as IncomingMessage).url = originalPath; - // store uri before it gets rewritten for logging - const originalPath = req.url; const newProxyOptions = Object.assign({}, this.proxyOptions); // Apply in order: @@ -130,7 +134,7 @@ export class HttpProxyMiddleware { if (this.proxyOptions.logLevel === 'debug') { const arrow = getArrow( originalPath, - req.url, + getUrl(req), this.proxyOptions.target, newProxyOptions.target ); @@ -162,20 +166,25 @@ export class HttpProxyMiddleware { // rewrite path private applyPathRewrite = async (req: Request, pathRewriter) => { + const url = getUrl(req); if (pathRewriter) { - const path = await pathRewriter(req.url, req); + const path = await pathRewriter(url, req); if (typeof path === 'string') { - req.url = path; + /** + * @warn May be hazardous to express users. HPM, Express, & + * http.IncomingMessage all write to this field. + */ + (req as IncomingMessage).url = path; } else { - this.logger.info('[HPM] pathRewrite: No rewritten path found. (%s)', req.url); + this.logger.info('[HPM] pathRewrite: No rewritten path found. (%s)', url); } } }; private logError = (err, req: Request, res: Response, target?) => { - const hostname = req.headers?.host || req.hostname || req.host; // (websocket) || (node0.10 || node 4/5) - const requestHref = `${hostname}${req.url}`; + const hostname = req.headers?.host; + const requestHref = `${hostname || ''}${getUrl(req)}`; const targetHref = `${target?.href}`; // target is undefined when websocket errors const errorMessage = '[HPM] Error occurred while proxying request %s to %s [%s] (%s)'; diff --git a/src/path-filter.ts b/src/path-filter.ts index 37b1a5fb..44057a90 100644 --- a/src/path-filter.ts +++ b/src/path-filter.ts @@ -3,37 +3,41 @@ import * as isGlob from 'is-glob'; import * as micromatch from 'micromatch'; import * as url from 'url'; import { ERRORS } from './errors'; +import { getUrl } from './url'; -export function matchPathFilter(pathFilter: Filter = '/', uri: string, req: Request): boolean { - // single path - if (isStringPath(pathFilter as string)) { - return matchSingleStringPath(pathFilter as string, uri); - } - - // single glob path - if (isGlobPath(pathFilter as string)) { - return matchSingleGlobPath(pathFilter as unknown as string[], uri); - } - - // multi path - if (Array.isArray(pathFilter)) { - if (pathFilter.every(isStringPath)) { - return matchMultiPath(pathFilter, uri); - } - if (pathFilter.every(isGlobPath)) { - return matchMultiGlobPath(pathFilter as string[], uri); - } - - throw new Error(ERRORS.ERR_CONTEXT_MATCHER_INVALID_ARRAY); - } +/** + * Express mutates `Request::url`, demanding special case + * handling for this single field. + * {@link https://github.com/expressjs/express/issues/4854} + */ - // custom matching - if (typeof pathFilter === 'function') { - const pathname = getUrlPathName(uri); - return pathFilter(pathname, req); +export function matchPathFilter(pathFilter: Filter = '/', req: Request): boolean { + const url = getUrl(req); + switch (typeof pathFilter) { + case 'string': + // single glob path + if (isGlob(pathFilter)) { + return matchSingleGlobPath(pathFilter, url); + } + // single path + return matchSingleStringPath(pathFilter, url); + // custom matching + case 'function': + return pathFilter(getUrlPathName(url), req); + case 'object': + // multi path + if (Array.isArray(pathFilter)) { + if (pathFilter.every(isStringPath)) { + return matchMultiPath(pathFilter, url); + } + if (pathFilter.every((path) => isGlob(path))) { + return matchMultiGlobPath(pathFilter, url); + } + throw new Error(ERRORS.ERR_CONTEXT_MATCHER_INVALID_ARRAY); + } + default: + throw new Error(ERRORS.ERR_CONTEXT_MATCHER_GENERIC); } - - throw new Error(ERRORS.ERR_CONTEXT_MATCHER_GENERIC); } /** @@ -87,7 +91,3 @@ function getUrlPathName(uri: string) { function isStringPath(pathFilter: string) { return typeof pathFilter === 'string' && !isGlob(pathFilter); } - -function isGlobPath(pathFilter: string) { - return isGlob(pathFilter); -} diff --git a/src/router.ts b/src/router.ts index d2e63505..81fcb925 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,5 +1,6 @@ import isPlainObj = require('is-plain-obj'); import { getInstance } from './logger'; +import { getUrl } from './url'; const logger = getInstance(); export async function getTarget(req, config) { @@ -18,7 +19,7 @@ export async function getTarget(req, config) { function getTargetFromProxyTable(req, table) { let result; const host = req.headers.host; - const path = req.url; + const path = getUrl(req); const hostAndPath = host + path; diff --git a/src/types.ts b/src/types.ts index 792613a6..fd287215 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,22 +9,18 @@ import type * as httpProxy from 'http-proxy'; import type * as net from 'net'; import type * as url from 'url'; -export type Request = http.IncomingMessage; -export type Response = http.ServerResponse; - /** - * http-proxy-middleware supports framework specific values. The following - * values are primarily decorated onto IncomingMessage by express, but are - * not required for use. + * @warn Hide `req.url`, as it's unsafe for use due to express + * mutation. + * + * {@link https://github.com/expressjs/express/issues/4854} + * + * Use getUrl(req) to get a x-server safe URL. */ -declare module 'http' { - interface IncomingMessage { - originalUrl?: string; - hostname?: string; - host?: string; - body?: Record; - } -} +type ExpressCompatibleIncomingMessage = Omit; + +export type Request = ExpressCompatibleIncomingMessage; +export type Response = http.ServerResponse; export type Next = (...args: unknown[]) => unknown; diff --git a/src/url.ts b/src/url.ts new file mode 100644 index 00000000..b63f72e4 --- /dev/null +++ b/src/url.ts @@ -0,0 +1,11 @@ +import { IncomingMessage } from 'http'; + +/** + * {@link https://github.com/expressjs/express/issues/4854} + */ +interface ExpressLikeRequest extends IncomingMessage { + originalUrl: string; +} + +export const getUrl = (req: IncomingMessage | ExpressLikeRequest) => + 'originalUrl' in req ? req.originalUrl : req.url; diff --git a/test/e2e/http-proxy-middleware.spec.ts b/test/e2e/http-proxy-middleware.spec.ts index 999aa26b..3c94ea34 100644 --- a/test/e2e/http-proxy-middleware.spec.ts +++ b/test/e2e/http-proxy-middleware.spec.ts @@ -18,7 +18,7 @@ describe('E2E http-proxy-middleware', () => { describe('pathFilter matching', () => { describe('do not proxy', () => { - const mockReq: Request = { url: '/foo/bar', originalUrl: '/foo/bar' } as Request; + const mockReq: Request = { url: '/foo/bar' } as Partial as Request; const mockRes: Response = {} as Response; const mockNext: NextFunction = jest.fn(); @@ -274,7 +274,7 @@ describe('E2E http-proxy-middleware', () => { ); }); - it('should handle errors when host is not reachable', async () => { + it.only('should handle errors when host is not reachable', async () => { const response = await agent.get(`/api/some/endpoint`).expect(504); expect(response.status).toBe(504); }); @@ -404,16 +404,14 @@ describe('E2E http-proxy-middleware', () => { describe('express with path + proxy', () => { beforeEach(() => { - agent = request( - createAppWithPath( - '/api', - createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}` }) - ) - ); + const middleware = createProxyMiddleware({ + target: `http://localhost:${mockTargetServer.port}`, + }); + agent = request(createAppWithPath('/api', middleware)); }); it('should proxy to target with the baseUrl', async () => { - await mockTargetServer.get('/api/foo/bar').thenReply(200, 'HELLO /api/foo/bar'); + await mockTargetServer.forGet('/api/foo/bar').thenReply(200, 'HELLO /api/foo/bar'); const response = await agent.get(`/api/foo/bar`).expect(200); expect(response.text).toBe('HELLO /api/foo/bar'); }); diff --git a/test/unit/fix-request-body.spec.ts b/test/unit/fix-request-body.spec.ts index e9a37717..9df74ba9 100644 --- a/test/unit/fix-request-body.spec.ts +++ b/test/unit/fix-request-body.spec.ts @@ -18,7 +18,7 @@ describe('fixRequestBody', () => { jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); - fixRequestBody(proxyRequest, { body: undefined } as Request); + fixRequestBody(proxyRequest, { body: undefined } as Partial as Request); expect(proxyRequest.setHeader).not.toHaveBeenCalled(); expect(proxyRequest.write).not.toHaveBeenCalled(); @@ -31,7 +31,7 @@ describe('fixRequestBody', () => { jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); - fixRequestBody(proxyRequest, { body: {} } as Request); + fixRequestBody(proxyRequest, { body: {} } as Partial as Request); expect(proxyRequest.setHeader).toHaveBeenCalled(); expect(proxyRequest.write).toHaveBeenCalled(); diff --git a/test/unit/path-filter.spec.ts b/test/unit/path-filter.spec.ts index 66721d62..0171faa7 100644 --- a/test/unit/path-filter.spec.ts +++ b/test/unit/path-filter.spec.ts @@ -1,62 +1,70 @@ -import type { Request } from '../../src/types'; import { matchPathFilter } from '../../src/path-filter'; +import { IncomingMessage } from 'http'; describe('Path Filter', () => { - const fakeReq = {} as Request; + let fakeReq = { + url: 'http://localhost/api/foo/bar', + } as IncomingMessage; + + const asRequest = (url: string) => + ({ + ...fakeReq, + url, + } as IncomingMessage); describe('String path matching', () => { let result; describe('Single path matching', () => { it('should match all paths', () => { - result = matchPathFilter('', 'http://localhost/api/foo/bar', fakeReq); + result = matchPathFilter('', fakeReq); expect(result).toBe(true); }); it('should match all paths starting with forward-slash', () => { - result = matchPathFilter('/', 'http://localhost/api/foo/bar', fakeReq); + result = matchPathFilter('/', fakeReq); expect(result).toBe(true); }); it('should return true when the pathFilter is present in url', () => { - result = matchPathFilter('/api', 'http://localhost/api/foo/bar', fakeReq); + result = matchPathFilter('/api', fakeReq); expect(result).toBe(true); }); it('should return false when the pathFilter is not present in url', () => { - result = matchPathFilter('/abc', 'http://localhost/api/foo/bar', fakeReq); + result = matchPathFilter('/abc', fakeReq); expect(result).toBe(false); }); it('should return false when the pathFilter is present half way in url', () => { - result = matchPathFilter('/foo', 'http://localhost/api/foo/bar', fakeReq); + result = matchPathFilter('/foo', fakeReq); expect(result).toBe(false); }); it('should return false when the pathFilter does not start with /', () => { - result = matchPathFilter('api', 'http://localhost/api/foo/bar', fakeReq); + result = matchPathFilter('api', fakeReq); expect(result).toBe(false); }); }); describe('Multi path matching', () => { it('should return true when the pathFilter is present in url', () => { - result = matchPathFilter(['/api'], 'http://localhost/api/foo/bar', fakeReq); + result = matchPathFilter(['/api'], fakeReq); expect(result).toBe(true); }); it('should return true when the pathFilter is present in url', () => { - result = matchPathFilter(['/api', '/ajax'], 'http://localhost/ajax/foo/bar', fakeReq); + result = matchPathFilter(['/api', '/ajax'], asRequest('http://localhost/ajax/foo/bar')); expect(result).toBe(true); }); it('should return false when the pathFilter does not match url', () => { - result = matchPathFilter(['/api', '/ajax'], 'http://localhost/foo/bar', fakeReq); + result = matchPathFilter(['/api', '/ajax'], asRequest('http://localhost/foo/bar')); expect(result).toBe(false); }); it('should return false when empty array provided', () => { - result = matchPathFilter([], 'http://localhost/api/foo/bar', fakeReq); + result = matchPathFilter([], fakeReq); expect(result).toBe(false); }); }); @@ -64,83 +72,84 @@ describe('Path Filter', () => { describe('Wildcard path matching', () => { describe('Single glob', () => { - let url; - beforeEach(() => { - url = 'http://localhost/api/foo/bar.html'; + fakeReq = asRequest('http://localhost/api/foo/bar.html'); }); describe('url-path matching', () => { it('should match any path', () => { - expect(matchPathFilter('**', url, fakeReq)).toBe(true); - expect(matchPathFilter('/**', url, fakeReq)).toBe(true); + expect(matchPathFilter('**', fakeReq)).toBe(true); + expect(matchPathFilter('/**', fakeReq)).toBe(true); }); it('should only match paths starting with "/api" ', () => { - expect(matchPathFilter('/api/**', url, fakeReq)).toBe(true); - expect(matchPathFilter('/ajax/**', url, fakeReq)).toBe(false); + expect(matchPathFilter('/api/**', fakeReq)).toBe(true); + expect(matchPathFilter('/ajax/**', fakeReq)).toBe(false); }); it('should only match paths starting with "foo" folder in it ', () => { - expect(matchPathFilter('**/foo/**', url, fakeReq)).toBe(true); - expect(matchPathFilter('**/invalid/**', url, fakeReq)).toBe(false); + expect(matchPathFilter('**/foo/**', fakeReq)).toBe(true); + expect(matchPathFilter('**/invalid/**', fakeReq)).toBe(false); }); }); describe('file matching', () => { it('should match any path, file and extension', () => { - expect(matchPathFilter('**', url, fakeReq)).toBe(true); - expect(matchPathFilter('**/*', url, fakeReq)).toBe(true); - expect(matchPathFilter('**/*.*', url, fakeReq)).toBe(true); - expect(matchPathFilter('/**', url, fakeReq)).toBe(true); - expect(matchPathFilter('/**/*', url, fakeReq)).toBe(true); - expect(matchPathFilter('/**/*.*', url, fakeReq)).toBe(true); + expect(matchPathFilter('**', fakeReq)).toBe(true); + expect(matchPathFilter('**/*', fakeReq)).toBe(true); + expect(matchPathFilter('**/*.*', fakeReq)).toBe(true); + expect(matchPathFilter('/**', fakeReq)).toBe(true); + expect(matchPathFilter('/**/*', fakeReq)).toBe(true); + expect(matchPathFilter('/**/*.*', fakeReq)).toBe(true); }); it('should only match .html files', () => { - expect(matchPathFilter('**/*.html', url, fakeReq)).toBe(true); - expect(matchPathFilter('/**/*.html', url, fakeReq)).toBe(true); - expect(matchPathFilter('/*.htm', url, fakeReq)).toBe(false); - expect(matchPathFilter('/*.jpg', url, fakeReq)).toBe(false); + expect(matchPathFilter('**/*.html', fakeReq)).toBe(true); + expect(matchPathFilter('/**/*.html', fakeReq)).toBe(true); + expect(matchPathFilter('/*.htm', fakeReq)).toBe(false); + expect(matchPathFilter('/*.jpg', fakeReq)).toBe(false); }); it('should only match .html under root path', () => { const pattern = '/*.html'; - expect(matchPathFilter(pattern, 'http://localhost/index.html', fakeReq)).toBe(true); - expect(matchPathFilter(pattern, 'http://localhost/some/path/index.html', fakeReq)).toBe( + expect(matchPathFilter(pattern, asRequest('http://localhost/index.html'))).toBe(true); + expect(matchPathFilter(pattern, asRequest('http://localhost/some/path/index.html'))).toBe( false ); }); it('should ignore query params', () => { - expect(matchPathFilter('/**/*.php', 'http://localhost/a/b/c.php?d=e&e=f', fakeReq)).toBe( - true - ); expect( - matchPathFilter('/**/*.php?*', 'http://localhost/a/b/c.php?d=e&e=f', fakeReq) + matchPathFilter('/**/*.php', asRequest('http://localhost/a/b/c.php?d=e&e=f')) + ).toBe(true); + expect( + matchPathFilter('/**/*.php?*', asRequest('http://localhost/a/b/c.php?d=e&e=f')) ).toBe(false); }); it('should only match any file in root path', () => { - expect(matchPathFilter('/*', 'http://localhost/bar.html', fakeReq)).toBe(true); - expect(matchPathFilter('/*.*', 'http://localhost/bar.html', fakeReq)).toBe(true); - expect(matchPathFilter('/*', 'http://localhost/foo/bar.html', fakeReq)).toBe(false); + expect(matchPathFilter('/*', asRequest('http://localhost/bar.html'))).toBe(true); + expect(matchPathFilter('/*.*', asRequest('http://localhost/bar.html'))).toBe(true); + expect(matchPathFilter('/*', asRequest('http://localhost/foo/bar.html'))).toBe(false); }); it('should only match .html file is in root path', () => { - expect(matchPathFilter('/*.html', 'http://localhost/bar.html', fakeReq)).toBe(true); - expect(matchPathFilter('/*.html', 'http://localhost/api/foo/bar.html', fakeReq)).toBe( - false - ); + expect(matchPathFilter('/*.html', asRequest('http://localhost/bar.html'))).toBe(true); + expect( + matchPathFilter('/*.html', { + ...fakeReq, + url: 'http://localhost/api/foo/bar.html', + } as IncomingMessage) + ).toBe(false); }); it('should only match .html files in "foo" folder', () => { - expect(matchPathFilter('**/foo/*.html', url, fakeReq)).toBe(true); - expect(matchPathFilter('**/bar/*.html', url, fakeReq)).toBe(false); + expect(matchPathFilter('**/foo/*.html', fakeReq)).toBe(true); + expect(matchPathFilter('**/bar/*.html', fakeReq)).toBe(false); }); it('should not match .html files', () => { - expect(matchPathFilter('!**/*.html', url, fakeReq)).toBe(false); + expect(matchPathFilter('!**/*.html', fakeReq)).toBe(false); }); }); }); @@ -149,27 +158,35 @@ describe('Path Filter', () => { describe('Multiple patterns', () => { it('should return true when both path patterns match', () => { const pattern = ['/api/**', '/ajax/**']; - expect(matchPathFilter(pattern, 'http://localhost/api/foo/bar.json', fakeReq)).toBe(true); - expect(matchPathFilter(pattern, 'http://localhost/ajax/foo/bar.json', fakeReq)).toBe( + expect(matchPathFilter(pattern, asRequest('http://localhost/api/foo/bar.json'))).toBe( true ); - expect(matchPathFilter(pattern, 'http://localhost/rest/foo/bar.json', fakeReq)).toBe( + expect(matchPathFilter(pattern, asRequest('http://localhost/ajax/foo/bar.json'))).toBe( + true + ); + expect(matchPathFilter(pattern, asRequest('http://localhost/rest/foo/bar.json'))).toBe( false ); }); it('should return true when both file extensions pattern match', () => { const pattern = ['/**/*.html', '/**/*.jpeg']; - expect(matchPathFilter(pattern, 'http://localhost/api/foo/bar.html', fakeReq)).toBe(true); - expect(matchPathFilter(pattern, 'http://localhost/api/foo/bar.jpeg', fakeReq)).toBe(true); - expect(matchPathFilter(pattern, 'http://localhost/api/foo/bar.gif', fakeReq)).toBe(false); + expect(matchPathFilter(pattern, asRequest('http://localhost/api/foo/bar.html'))).toBe( + true + ); + expect(matchPathFilter(pattern, asRequest('http://localhost/api/foo/bar.jpeg'))).toBe( + true + ); + expect(matchPathFilter(pattern, asRequest('http://localhost/api/foo/bar.gif'))).toBe( + false + ); }); }); describe('Negation patterns', () => { it('should not match file extension', () => { const url = 'http://localhost/api/foo/bar.html'; - expect(matchPathFilter(['**', '!**/*.html'], url, fakeReq)).toBe(false); - expect(matchPathFilter(['**', '!**/*.json'], url, fakeReq)).toBe(true); + expect(matchPathFilter(['**', '!**/*.html'], asRequest(url))).toBe(false); + expect(matchPathFilter(['**', '!**/*.json'], asRequest(url))).toBe(true); }); }); }); @@ -177,7 +194,7 @@ describe('Path Filter', () => { describe('Use function for matching', () => { const testFunctionAsPathFilter = (val) => { - return matchPathFilter(fn, 'http://localhost/api/foo/bar', fakeReq); + return matchPathFilter(fn, fakeReq); function fn(path, req) { return val; @@ -206,7 +223,7 @@ describe('Path Filter', () => { beforeEach(() => { testPathFilter = (pathFilter) => { return () => { - matchPathFilter(pathFilter, 'http://localhost/api/foo/bar', fakeReq); + matchPathFilter(pathFilter, fakeReq); }; }; }); From 5460cb3d66da71f8250c63bf2134c821da6bfc1e Mon Sep 17 00:00:00 2001 From: cdaringe Date: Sun, 13 Mar 2022 13:52:09 -0700 Subject: [PATCH 09/10] docs: fix comment node assoc --- src/http-proxy-middleware.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/http-proxy-middleware.ts b/src/http-proxy-middleware.ts index cd32e199..477ad41d 100644 --- a/src/http-proxy-middleware.ts +++ b/src/http-proxy-middleware.ts @@ -111,15 +111,14 @@ export class HttpProxyMiddleware { * @return {Object} proxy options */ private prepareProxyRequest = async (req: Request) => { + // Store pathname before it gets rewritten for logging + const originalPath = getUrl(req); /** - * Store pathname before it gets rewritten for logging * @warn May be hazardous to express users. HPM, Express, & * http.IncomingMessage all write to this field. + * {@link https://github.com/chimurai/http-proxy-middleware/issues/17} + * {@link https://github.com/chimurai/http-proxy-middleware/issues/94} */ - const originalPath = getUrl(req); - - // https://github.com/chimurai/http-proxy-middleware/issues/17 - // https://github.com/chimurai/http-proxy-middleware/issues/94 (req as IncomingMessage).url = originalPath; const newProxyOptions = Object.assign({}, this.proxyOptions); From 4a8bb87c6303b08d83553d90c63f57572ad8c05a Mon Sep 17 00:00:00 2001 From: cdaringe Date: Sun, 13 Mar 2022 13:55:22 -0700 Subject: [PATCH 10/10] chore: restore prior middleware setup --- test/e2e/http-proxy-middleware.spec.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/e2e/http-proxy-middleware.spec.ts b/test/e2e/http-proxy-middleware.spec.ts index 3c94ea34..a0d16e74 100644 --- a/test/e2e/http-proxy-middleware.spec.ts +++ b/test/e2e/http-proxy-middleware.spec.ts @@ -404,10 +404,14 @@ describe('E2E http-proxy-middleware', () => { describe('express with path + proxy', () => { beforeEach(() => { - const middleware = createProxyMiddleware({ - target: `http://localhost:${mockTargetServer.port}`, - }); - agent = request(createAppWithPath('/api', middleware)); + agent = request( + createAppWithPath( + '/api', + createProxyMiddleware({ + target: `http://localhost:${mockTargetServer.port}`, + }) + ) + ); }); it('should proxy to target with the baseUrl', async () => {