From 11bcc54205766d5e30baa39e703f1b98b81bfa0e Mon Sep 17 00:00:00 2001 From: Steven Chim <655241+chimurai@users.noreply.github.com> Date: Sun, 3 May 2026 18:00:49 +0000 Subject: [PATCH 1/5] feat(definePlugin): helper function to create plugins --- src/index.ts | 5 ++ .../default/debug-proxy-errors-plugin.ts | 5 +- src/plugins/default/error-response-plugin.ts | 5 +- src/plugins/default/logger-plugin.ts | 7 +- src/plugins/default/proxy-events.ts | 5 +- src/plugins/define-plugin.ts | 22 ++++++ test/types.spec.ts | 43 ++++++++++++ test/unit/get-plugins.spec.ts | 67 +++++++------------ 8 files changed, 109 insertions(+), 50 deletions(-) create mode 100644 src/plugins/define-plugin.ts diff --git a/src/index.ts b/src/index.ts index cd7d84c3..9e33577e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,3 +8,8 @@ export type { Plugin, Filter, Options, RequestHandler } from './types.js'; * Default plugins */ export * from './plugins/default/index.js'; + +/** + * Export definePlugin helper function for better typing when defining user plugins + */ +export * from './plugins/define-plugin.js'; diff --git a/src/plugins/default/debug-proxy-errors-plugin.ts b/src/plugins/default/debug-proxy-errors-plugin.ts index e1010116..a9464d08 100644 --- a/src/plugins/default/debug-proxy-errors-plugin.ts +++ b/src/plugins/default/debug-proxy-errors-plugin.ts @@ -1,5 +1,6 @@ import { Debug } from '../../debug.js'; import type { Plugin } from '../../types.js'; +import { definePlugin } from '../define-plugin.js'; const debug = Debug.extend('debug-proxy-errors-plugin'); @@ -7,7 +8,7 @@ const debug = Debug.extend('debug-proxy-errors-plugin'); * Subscribe to {@link https://github.com/unjs/httpxy#events `httpxy` error events} to prevent server from crashing. * Errors are logged with {@link https://www.npmjs.com/package/debug debug} library. */ -export const debugProxyErrorsPlugin: Plugin = (proxyServer): void => { +export const debugProxyErrorsPlugin: Plugin = definePlugin((proxyServer, options) => { /** * The old `http-proxy` doesn't handle any errors by default (https://github.com/http-party/node-http-proxy#listening-for-proxy-events) * > We do not do any error handling of messages passed between client and proxy, and messages passed between proxy and target, so it is recommended that you listen on errors and handle them. @@ -64,4 +65,4 @@ export const debugProxyErrorsPlugin: Plugin = (proxyServer): void => { proxyServer.on('econnreset', (error, req, res, target) => { debug(`httpxy econnreset event: \n%O`, error); }); -}; +}); diff --git a/src/plugins/default/error-response-plugin.ts b/src/plugins/default/error-response-plugin.ts index 5a91d43c..4717fffc 100644 --- a/src/plugins/default/error-response-plugin.ts +++ b/src/plugins/default/error-response-plugin.ts @@ -4,6 +4,7 @@ import type { Socket } from 'node:net'; import { getStatusCode } from '../../status-code.js'; import type { Plugin } from '../../types.js'; import { sanitize } from '../../utils/sanitize.js'; +import { definePlugin } from '../define-plugin.js'; function isResponseLike(obj: any): obj is http.ServerResponse { return obj && typeof obj.writeHead === 'function'; @@ -13,7 +14,7 @@ function isSocketLike(obj: any): obj is Socket { return obj && typeof obj.write === 'function' && !('writeHead' in obj); } -export const errorResponsePlugin: Plugin = (proxyServer, options) => { +export const errorResponsePlugin: Plugin = definePlugin((proxyServer, options) => { proxyServer.on('error', (err, req, res, target?) => { // Re-throw error. Not recoverable since req & res are empty. if (!req || !res) { @@ -32,4 +33,4 @@ export const errorResponsePlugin: Plugin = (proxyServer, options) => { res.destroy(); } }); -}; +}); diff --git a/src/plugins/default/logger-plugin.ts b/src/plugins/default/logger-plugin.ts index e8d8e5ea..4b288e62 100644 --- a/src/plugins/default/logger-plugin.ts +++ b/src/plugins/default/logger-plugin.ts @@ -5,6 +5,7 @@ import { getLogger } from '../../logger.js'; import type { Plugin } from '../../types.js'; import { createUrl } from '../../utils/create-url.js'; import { getPort } from '../../utils/logger-plugin.js'; +import { definePlugin } from '../define-plugin.js'; type ExpressRequest = { /** Express req.baseUrl */ @@ -19,7 +20,7 @@ type BrowserSyncRequest = { /** Request Types from different server libs */ type FrameworkRequest = IncomingMessage & ExpressRequest & BrowserSyncRequest; -export const loggerPlugin: Plugin = (proxyServer, options) => { +export const loggerPlugin: Plugin = definePlugin((proxyServer, options) => { const logger = getLogger(options); proxyServer.on('error', (err, req, res, target?) => { @@ -40,7 +41,7 @@ export const loggerPlugin: Plugin = (proxyServer, options) => { * [HPM] GET /users/ -> http://jsonplaceholder.typicode.com/users/ [304] * ``` */ - proxyServer.on('proxyRes', (proxyRes: any, req: FrameworkRequest, res) => { + proxyServer.on('proxyRes', (proxyRes: any, req, res) => { // BrowserSync uses req.originalUrl // Next.js doesn't have req.baseUrl const originalUrl = req.originalUrl ?? `${req.baseUrl || ''}${req.url}`; @@ -80,4 +81,4 @@ export const loggerPlugin: Plugin = (proxyServer, options) => { proxyServer.on('close', (req, proxySocket, proxyHead) => { logger.info('[HPM] Client disconnected: %o', proxySocket.address()); }); -}; +}); diff --git a/src/plugins/default/proxy-events.ts b/src/plugins/default/proxy-events.ts index 02c6c6fa..ddefb831 100644 --- a/src/plugins/default/proxy-events.ts +++ b/src/plugins/default/proxy-events.ts @@ -1,6 +1,7 @@ import { Debug } from '../../debug.js'; import type { Plugin } from '../../types.js'; import { getFunctionName } from '../../utils/function.js'; +import { definePlugin } from '../define-plugin.js'; const debug = Debug.extend('proxy-events-plugin'); @@ -24,7 +25,7 @@ const debug = Debug.extend('proxy-events-plugin'); * }); * ``` */ -export const proxyEventsPlugin: Plugin = (proxyServer, options) => { +export const proxyEventsPlugin: Plugin = definePlugin((proxyServer, options) => { if (!options.on) { return; } @@ -42,4 +43,4 @@ export const proxyEventsPlugin: Plugin = (proxyServer, options) => { proxyServer.on(eventName, handler as (...args: unknown[]) => void); } } -}; +}); diff --git a/src/plugins/define-plugin.ts b/src/plugins/define-plugin.ts new file mode 100644 index 00000000..239a0160 --- /dev/null +++ b/src/plugins/define-plugin.ts @@ -0,0 +1,22 @@ +import type * as http from 'node:http'; + +import type { Plugin } from '../types.js'; + +/** + * Helper function to define a http-proxy-middleware plugin + * + * @example + * ```js + * export const myPlugin = definePlugin((proxyServer, options) => { + * // plugin implementation + * }); + * ``` + * + * @since 4.1.0 + */ +export function definePlugin< + TReq extends http.IncomingMessage = http.IncomingMessage, + TRes extends http.ServerResponse = http.ServerResponse, +>(fn: Plugin): Plugin { + return fn; +} diff --git a/test/types.spec.ts b/test/types.spec.ts index f994e17d..7114c4dd 100644 --- a/test/types.spec.ts +++ b/test/types.spec.ts @@ -5,6 +5,7 @@ import express from 'express'; import { beforeEach, describe, expect, it } from 'vitest'; import { + definePlugin, fixRequestBody, createProxyMiddleware as middleware, responseInterceptor, @@ -442,4 +443,46 @@ describe('http-proxy-middleware TypeScript Types', () => { expect(app).toBeDefined(); }); }); + + describe('definePlugin()', () => { + it('should define plugin with correct types', () => { + const myPlugin = definePlugin((proxyServer, options) => { + proxyServer.on('proxyReq', (proxyReq, req, res, options) => { + req.url; + res.statusCode; + + // @ts-expect-error: should error when request is typed as `any` + req.unknownProperty; + // @ts-expect-error: should error when response is typed as `any` + res.unknownProperty; + }); + }); + + expect(myPlugin).toBeDefined(); + }); + + it('should define plugin with custom req & res types', () => { + interface MyRequest extends http.IncomingMessage { + myRequestParams: { [key: string]: string }; + } + + interface MyResponse extends http.ServerResponse { + myResponseParams: { [key: string]: string }; + } + + const myPlugin = definePlugin((proxyServer, options) => { + proxyServer.on('proxyReq', (proxyReq, req, res, options) => { + req.myRequestParams; + res.myResponseParams; + + // @ts-expect-error: should error when request is typed as `any` + req.unknownProperty; + // @ts-expect-error: should error when response is typed as `any` + res.unknownProperty; + }); + }); + + expect(myPlugin).toBeDefined(); + }); + }); }); diff --git a/test/unit/get-plugins.spec.ts b/test/unit/get-plugins.spec.ts index 6ede79c2..b7cf6a9c 100644 --- a/test/unit/get-plugins.spec.ts +++ b/test/unit/get-plugins.spec.ts @@ -7,6 +7,7 @@ import { loggerPlugin, proxyEventsPlugin, } from '../../src/index.js'; +import { definePlugin } from '../../src/plugins/define-plugin.js'; import type { Plugin } from '../../src/types.js'; describe('getPlugins', () => { @@ -16,14 +17,12 @@ describe('getPlugins', () => { plugins = getPlugins({}); expect(plugins).toHaveLength(4); - expect(plugins.map((plugin) => plugin.name)).toMatchInlineSnapshot(` - [ - "debugProxyErrorsPlugin", - "proxyEventsPlugin", - "loggerPlugin", - "errorResponsePlugin", - ] - `); + expect(plugins).toStrictEqual([ + debugProxyErrorsPlugin, + proxyEventsPlugin, + loggerPlugin, + errorResponsePlugin, + ]); }); it('should return no plugins when ejectPlugins is configured in option', () => { @@ -35,40 +34,34 @@ describe('getPlugins', () => { }); it('should return user plugins with default plugins when user plugins are provided', () => { - const myPlugin: Plugin = () => { + const myPlugin: Plugin = definePlugin(() => { /* noop */ - }; + }); plugins = getPlugins({ plugins: [myPlugin], }); expect(plugins).toHaveLength(5); - expect(plugins.map((plugin) => plugin.name)).toMatchInlineSnapshot(` - [ - "debugProxyErrorsPlugin", - "proxyEventsPlugin", - "loggerPlugin", - "errorResponsePlugin", - "myPlugin", - ] - `); + expect(plugins).toStrictEqual([ + debugProxyErrorsPlugin, + proxyEventsPlugin, + loggerPlugin, + errorResponsePlugin, + myPlugin, + ]); }); it('should only return user plugins when user plugins are provided with ejectPlugins option', () => { - const myPlugin: Plugin = () => { + const myPlugin: Plugin = definePlugin(() => { /* noop */ - }; + }); plugins = getPlugins({ ejectPlugins: true, plugins: [myPlugin], }); expect(plugins).toHaveLength(1); - expect(plugins.map((plugin) => plugin.name)).toMatchInlineSnapshot(` - [ - "myPlugin", - ] - `); + expect(plugins).toStrictEqual([myPlugin]); }); it('should return manually added default plugins in different order after using ejectPlugins', () => { @@ -78,14 +71,12 @@ describe('getPlugins', () => { }); expect(plugins).toHaveLength(4); - expect(plugins.map((plugin) => plugin.name)).toMatchInlineSnapshot(` - [ - "debugProxyErrorsPlugin", - "errorResponsePlugin", - "loggerPlugin", - "proxyEventsPlugin", - ] - `); + expect(plugins).toStrictEqual([ + debugProxyErrorsPlugin, + errorResponsePlugin, + loggerPlugin, + proxyEventsPlugin, + ]); }); it('should not configure errorResponsePlugin when user specifies their own error handler', () => { @@ -99,12 +90,6 @@ describe('getPlugins', () => { }); expect(plugins).toHaveLength(3); - expect(plugins.map((plugin) => plugin.name)).toMatchInlineSnapshot(` - [ - "debugProxyErrorsPlugin", - "proxyEventsPlugin", - "loggerPlugin", - ] - `); + expect(plugins).toStrictEqual([debugProxyErrorsPlugin, proxyEventsPlugin, loggerPlugin]); }); }); From e69397a94bc22f12632365c5f83c5ea9920725bc Mon Sep 17 00:00:00 2001 From: Steven Chim <655241+chimurai@users.noreply.github.com> Date: Sat, 9 May 2026 15:24:20 +0000 Subject: [PATCH 2/5] docs(definePlugin): add documentation --- CHANGELOG.md | 4 ++++ README.md | 28 ++++++++++++++++++++++------ src/plugins/define-plugin.ts | 18 ++++++++++++++++-- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 434ab6b2..ba6ce8e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## next + +- feat(definePlugin): helper function to create plugins + ## [v4.0.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v4.0.0) - fix(types): fix Logger type diff --git a/README.md b/README.md index 979b2743..255234c1 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ _All_ `httpxy` [options](https://github.com/unjs/httpxy#options) can be used, al - [`router` (object/function)](#router-objectfunction) - [`plugins` (Array)](#plugins-array) - [`ejectPlugins` (boolean) default: `false`](#ejectplugins-boolean-default-false) +- [`definePlugin` helper](#defineplugin-helper) - [`logger` (Object)](#logger-object) - [`httpxy` events](#httpxy-events) - [`httpxy` options](#httpxy-options) @@ -281,12 +282,9 @@ NOTE: register your own error handlers to prevent server from crashing. ```js // eject default plugins and manually add them back import { - debugProxyErrorsPlugin, - // log proxy events to a logger (ie. console) - errorResponsePlugin, - // subscribe to proxy errors to prevent server from crashing - loggerPlugin, - // return 5xx response on proxy error + debugProxyErrorsPlugin, // subscribe to proxy errors to prevent server from crashing + errorResponsePlugin, // return 5xx response on proxy error + loggerPlugin, // log proxy events to a logger (ie. console) proxyEventsPlugin, // implements the "on:" option } from 'http-proxy-middleware'; @@ -298,6 +296,24 @@ createProxyMiddleware({ }); ``` +## `definePlugin` helper + +Create your own `http-proxy-middleware` plugin. + +(Default plugins are created with `definePlugin`) + +```ts +const myPlugin = definePlugin((proxyServer, options) => { + // plugin implementation +}); + +// use configure and use plugin +createProxyMiddleware({ + target: `http://example.org`, + plugins: [myPlugin], +}); +``` + ### `logger` (Object) Configure a logger to output information from http-proxy-middleware: ie. `console`, `winston`, `pino`, `bunyan`, `log4js`, etc... diff --git a/src/plugins/define-plugin.ts b/src/plugins/define-plugin.ts index 239a0160..3a871b72 100644 --- a/src/plugins/define-plugin.ts +++ b/src/plugins/define-plugin.ts @@ -1,17 +1,31 @@ import type * as http from 'node:http'; -import type { Plugin } from '../types.js'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type { ProxyServer } from 'httpxy'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type { Plugin, Options } from '../types.js'; /** * Helper function to define a http-proxy-middleware plugin + * @see proxyServer {@link ProxyServer} - proxy server instance to which the plugin is being applied + * @see options {@link Options} - options object passed to `createProxyMiddleware` * - * @example + * @example defining a plugin * ```js * export const myPlugin = definePlugin((proxyServer, options) => { * // plugin implementation * }); * ``` * + * @example using a plugin + * ```js + * createProxyMiddleware({ + * target: 'http://example.com', + * plugins: [myPlugin], + * }); + * ``` + * * @since 4.1.0 */ export function definePlugin< From cc0a662f5549e8fdc63694e91e89127dc27ffc72 Mon Sep 17 00:00:00 2001 From: Steven Chim <655241+chimurai@users.noreply.github.com> Date: Sat, 9 May 2026 16:48:53 +0000 Subject: [PATCH 3/5] test(plugins): refactor test with definePlugin() --- test/e2e/plugins.spec.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/e2e/plugins.spec.ts b/test/e2e/plugins.spec.ts index f69ba9d1..45b28ef0 100644 --- a/test/e2e/plugins.spec.ts +++ b/test/e2e/plugins.spec.ts @@ -3,7 +3,8 @@ import { getLocal } from 'mockttp'; import request from 'supertest'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { Options, Plugin } from '../../src/types.js'; +import { definePlugin } from '../../src/index.js'; +import type { Options, Plugin } from '../../src/index.js'; import { createApp, createProxyMiddleware } from './test-kit.js'; describe('E2E Plugins', () => { @@ -24,10 +25,10 @@ describe('E2E Plugins', () => { mockTargetServer.forGet('/users/1').thenReply(200, '{"userName":"John"}'); - const simplePlugin: Plugin = (proxy) => { + const simplePlugin: Plugin = definePlugin((proxy) => { proxy.on('proxyReq', (proxyReq, req, res, options) => (proxyReqUrl = req.url)); proxy.on('proxyRes', (proxyRes, req, res) => (responseStatusCode = proxyRes.statusCode)); - }; + }); const config: Options = { target: mockTargetServer.url, From dc340f2552ff674ef1c1225b9a1be95aa84159c5 Mon Sep 17 00:00:00 2001 From: Steven Chim <655241+chimurai@users.noreply.github.com> Date: Sat, 9 May 2026 16:56:01 +0000 Subject: [PATCH 4/5] refactor(plugins): refactor plugins export --- src/index.ts | 10 +--------- src/plugins/index.ts | 5 +++++ 2 files changed, 6 insertions(+), 9 deletions(-) create mode 100644 src/plugins/index.ts diff --git a/src/index.ts b/src/index.ts index 9e33577e..686921a5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,12 +4,4 @@ export * from './handlers/index.js'; export type { Plugin, Filter, Options, RequestHandler } from './types.js'; -/** - * Default plugins - */ -export * from './plugins/default/index.js'; - -/** - * Export definePlugin helper function for better typing when defining user plugins - */ -export * from './plugins/define-plugin.js'; +export * from './plugins/index.js'; diff --git a/src/plugins/index.ts b/src/plugins/index.ts new file mode 100644 index 00000000..521bcb46 --- /dev/null +++ b/src/plugins/index.ts @@ -0,0 +1,5 @@ +// definePlugin() +export * from './define-plugin.js'; + +// default plugins +export * from './default/index.js'; From 697d14cebda1902439b3de0658f82199aa9bb751 Mon Sep 17 00:00:00 2001 From: Steven Chim <655241+chimurai@users.noreply.github.com> Date: Sat, 9 May 2026 17:01:36 +0000 Subject: [PATCH 5/5] docs(README.md): improve definePlugin() example --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 255234c1..48e68542 100644 --- a/README.md +++ b/README.md @@ -303,6 +303,8 @@ Create your own `http-proxy-middleware` plugin. (Default plugins are created with `definePlugin`) ```ts +import { createProxyMiddleware, definePlugin } from 'http-proxy-middleware'; + const myPlugin = definePlugin((proxyServer, options) => { // plugin implementation });