diff --git a/.eslintrc.js b/.eslintrc.js index b17cfb3f..29dcfaa8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -20,5 +20,11 @@ module.exports = { '@typescript-eslint/no-var-requires': 'off', }, }, + { + files: ['src/**/*.ts'], + rules: { + 'no-restricted-imports': ['error', { paths: ['express'] }], + }, + }, ], }; diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b15032a..2f83c78e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## next +- feat(typescript): type improvements ([#882](https://github.com/chimurai/http-proxy-middleware/pull/882)) - chore(deps): update micromatch to 4.0.5 - chore(package): bump devDependencies - feat(legacyCreateProxyMiddleware): show migration tips ([#756](https://github.com/chimurai/http-proxy-middleware/pull/756)) diff --git a/README.md b/README.md index 3363593a..03216ca6 100644 --- a/README.md +++ b/README.md @@ -141,15 +141,12 @@ const { createProxyMiddleware } = require('http-proxy-middleware'); const app = express(); -// proxy middleware options -/** @type {import('http-proxy-middleware/dist/types').Options} */ -const options = { +// create the proxy +/** @type {import('http-proxy-middleware/dist/types').RequestHandler} */ +const exampleProxy = createProxyMiddleware({ target: 'http://www.example.org/api', // target host with the same base path changeOrigin: true, // needed for virtual hosted sites -}; - -// create the proxy -const exampleProxy = createProxyMiddleware(options); +}); // mount `exampleProxy` in web server app.use('/api', exampleProxy); diff --git a/package.json b/package.json index c72ae489..3da7e831 100644 --- a/package.json +++ b/package.json @@ -83,21 +83,13 @@ "ws": "8.10.0" }, "dependencies": { - "@types/http-proxy": "^1.17.8", + "@types/http-proxy": "^1.17.10", "debug": "^4.3.4", "http-proxy": "^1.18.1", "is-glob": "^4.0.1", "is-plain-obj": "^3.0.0", "micromatch": "^4.0.5" }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } - }, "engines": { "node": ">=12.0.0" }, diff --git a/recipes/servers.md b/recipes/servers.md index 76ff8490..b2399c71 100644 --- a/recipes/servers.md +++ b/recipes/servers.md @@ -72,7 +72,7 @@ Next project: `/pages/api/users.ts` import type { NextApiRequest, NextApiResponse } from 'next'; import { createProxyMiddleware } from 'http-proxy-middleware'; -const proxyMiddleware = createProxyMiddleware({ +const proxyMiddleware = createProxyMiddleware({ target: 'http://jsonplaceholder.typicode.com', changeOrigin: true, pathRewrite: { diff --git a/src/configuration.ts b/src/configuration.ts index 64cb9ac0..1a88ec4f 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -1,7 +1,7 @@ import { ERRORS } from './errors'; import { Options } from './types'; -export function verifyConfig(options: Options): void { +export function verifyConfig(options: Options): void { if (!options.target && !options.router) { throw new Error(ERRORS.ERR_CONFIG_FACTORY_TARGET_MISSING); } diff --git a/src/get-plugins.ts b/src/get-plugins.ts index b70fdaf3..faf3709a 100644 --- a/src/get-plugins.ts +++ b/src/get-plugins.ts @@ -6,13 +6,13 @@ import { proxyEventsPlugin, } from './plugins/default'; -export function getPlugins(options: Options): Plugin[] { +export function getPlugins(options: Options): Plugin[] { // don't load default errorResponsePlugin if user has specified their own const maybeErrorResponsePlugin = !!options.on?.error ? [] : [errorResponsePlugin]; - const defaultPlugins: Plugin[] = !!options.ejectPlugins + const defaultPlugins = !!options.ejectPlugins ? [] // no default plugins when ejecting : [debugProxyErrorsPlugin, proxyEventsPlugin, loggerPlugin, ...maybeErrorResponsePlugin]; - const userPlugins: Plugin[] = options.plugins ?? []; - return [...defaultPlugins, ...userPlugins]; + const userPlugins: Plugin[] = options.plugins ?? []; + return [...defaultPlugins, ...userPlugins] as unknown as Plugin[]; } diff --git a/src/handlers/fix-request-body.ts b/src/handlers/fix-request-body.ts index b05fe382..2767fe08 100644 --- a/src/handlers/fix-request-body.ts +++ b/src/handlers/fix-request-body.ts @@ -1,13 +1,16 @@ import type * as http from 'http'; -import type * as express from 'express'; -import type { Request } from '../types'; import * as querystring from 'querystring'; +export type BodyParserLikeRequest = http.IncomingMessage & { body: any }; + /** * Fix proxied body if bodyParser is involved. */ -export function fixRequestBody(proxyReq: http.ClientRequest, req: Request): void { - const requestBody = (req as Request).body; +export function fixRequestBody( + proxyReq: http.ClientRequest, + req: TReq +): void { + const requestBody = (req as unknown as BodyParserLikeRequest).body; if (!requestBody) { return; diff --git a/src/handlers/response-interceptor.ts b/src/handlers/response-interceptor.ts index 5ddc310f..1631a5d0 100644 --- a/src/handlers/response-interceptor.ts +++ b/src/handlers/response-interceptor.ts @@ -5,11 +5,11 @@ import { getFunctionName } from '../utils/function'; const debug = Debug.extend('response-interceptor'); -type Interceptor = ( +type Interceptor = ( buffer: Buffer, - proxyRes: http.IncomingMessage, - req: http.IncomingMessage, - res: http.ServerResponse + proxyRes: TReq, + req: TReq, + res: TRes ) => Promise; /** @@ -19,18 +19,21 @@ type Interceptor = ( * * NOTE: must set options.selfHandleResponse=true (prevent automatic call of res.end()) */ -export function responseInterceptor(interceptor: Interceptor) { +export function responseInterceptor< + TReq extends http.IncomingMessage = http.IncomingMessage, + TRes extends http.ServerResponse = http.ServerResponse +>(interceptor: Interceptor) { return async function proxyResResponseInterceptor( - proxyRes: http.IncomingMessage, - req: http.IncomingMessage, - res: http.ServerResponse + proxyRes: TReq, + req: TReq, + res: TRes ): Promise { debug('intercept proxy response'); const originalProxyRes = proxyRes; let buffer = Buffer.from('', 'utf8'); // decompress proxy response - const _proxyRes = decompress(proxyRes, proxyRes.headers['content-encoding']); + const _proxyRes = decompress(proxyRes, proxyRes.headers['content-encoding']); // concat data stream _proxyRes.on('data', (chunk) => (buffer = Buffer.concat([buffer, chunk]))); @@ -62,7 +65,10 @@ export function responseInterceptor(interceptor: Interceptor) { * Streaming decompression of proxy response * source: https://github.com/apache/superset/blob/9773aba522e957ed9423045ca153219638a85d2f/superset-frontend/webpack.proxy-config.js#L116 */ -function decompress(proxyRes: http.IncomingMessage, contentEncoding: string) { +function decompress( + proxyRes: TReq, + contentEncoding: string +): TReq { let _proxyRes = proxyRes; let decompress; @@ -93,7 +99,7 @@ function decompress(proxyRes: http.IncomingMessage, contentEncoding: string) { * Copy original headers * https://github.com/apache/superset/blob/9773aba522e957ed9423045ca153219638a85d2f/superset-frontend/webpack.proxy-config.js#L78 */ -function copyHeaders(originalResponse, response) { +function copyHeaders(originalResponse, response): void { debug('copy original response headers'); response.statusCode = originalResponse.statusCode; diff --git a/src/http-proxy-middleware.ts b/src/http-proxy-middleware.ts index 83f81907..f1465ff9 100644 --- a/src/http-proxy-middleware.ts +++ b/src/http-proxy-middleware.ts @@ -1,5 +1,6 @@ +import type * as http from 'http'; import type * as https from 'https'; -import type { Request, RequestHandler, Options, Filter } from './types'; +import type { RequestHandler, Options, Filter } from './types'; import * as httpProxy from 'http-proxy'; import { verifyConfig } from './configuration'; import { getPlugins } from './get-plugins'; @@ -9,15 +10,15 @@ import * as Router from './router'; import { Debug as debug } from './debug'; import { getFunctionName } from './utils/function'; -export class HttpProxyMiddleware { +export class HttpProxyMiddleware { private wsInternalSubscribed = false; private serverOnCloseSubscribed = false; - private proxyOptions: Options; - private proxy: httpProxy; + private proxyOptions: Options; + private proxy: httpProxy; private pathRewriter; - constructor(options: Options) { - verifyConfig(options); + constructor(options: Options) { + verifyConfig(options); this.proxyOptions = options; debug(`create proxy server`); @@ -74,8 +75,8 @@ export class HttpProxyMiddleware { } }; - private registerPlugins(proxy: httpProxy, options: Options) { - const plugins = getPlugins(options); + private registerPlugins(proxy: httpProxy, options: Options) { + const plugins = getPlugins(options); plugins.forEach((plugin) => { debug(`register plugin: "${getFunctionName(plugin)}"`); plugin(proxy, options); @@ -92,7 +93,7 @@ export class HttpProxyMiddleware { } }; - private handleUpgrade = async (req: Request, socket, head) => { + private handleUpgrade = async (req: http.IncomingMessage, socket, head) => { if (this.shouldProxy(this.proxyOptions.pathFilter, req)) { const activeProxyOptions = await this.prepareProxyRequest(req); this.proxy.ws(req, socket, head, activeProxyOptions); @@ -103,7 +104,7 @@ export class HttpProxyMiddleware { /** * Determine whether request should be proxied. */ - private shouldProxy = (pathFilter: Filter, req: Request): boolean => { + private shouldProxy = (pathFilter: Filter, req: http.IncomingMessage): boolean => { return matchPathFilter(pathFilter, req.url, req); }; @@ -115,7 +116,7 @@ export class HttpProxyMiddleware { * @param {Object} req * @return {Object} proxy options */ - private prepareProxyRequest = async (req: Request) => { + private prepareProxyRequest = async (req: http.IncomingMessage) => { /** * Incorrect usage confirmed: https://github.com/expressjs/express/issues/4854#issuecomment-1066171160 * Temporary restore req.url patch for {@link src/legacy/create-proxy-middleware.ts legacyCreateProxyMiddleware()} @@ -137,7 +138,7 @@ export class HttpProxyMiddleware { }; // Modify option.target when router present. - private applyRouter = async (req: Request, options) => { + private applyRouter = async (req: http.IncomingMessage, options) => { let newTarget; if (options.router) { @@ -151,7 +152,7 @@ export class HttpProxyMiddleware { }; // rewrite path - private applyPathRewrite = async (req: Request, pathRewriter) => { + private applyPathRewrite = async (req: http.IncomingMessage, pathRewriter) => { if (pathRewriter) { const path = await pathRewriter(req.url, req); diff --git a/src/index.ts b/src/index.ts index 130f9c6b..82e96a97 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,14 @@ import { HttpProxyMiddleware } from './http-proxy-middleware'; -import type { Options, RequestHandler } from './types'; +import type { Options, RequestHandler, NextFunction } from './types'; +import type * as http from 'http'; -export function createProxyMiddleware(options: Options): RequestHandler { - const { middleware } = new HttpProxyMiddleware(options); - return middleware; +export function createProxyMiddleware< + TReq = http.IncomingMessage, + TRes = http.ServerResponse, + TNext = NextFunction +>(options: Options): RequestHandler { + const { middleware } = new HttpProxyMiddleware(options); + return middleware as unknown as RequestHandler; } export * from './handlers'; diff --git a/src/legacy/create-proxy-middleware.ts b/src/legacy/create-proxy-middleware.ts index 3a659131..f40297c0 100644 --- a/src/legacy/create-proxy-middleware.ts +++ b/src/legacy/create-proxy-middleware.ts @@ -3,6 +3,7 @@ import { Debug } from '../debug'; import { Filter, RequestHandler } from '../types'; import { legacyOptionsAdapter } from './options-adapter'; import { LegacyOptions } from './types'; +import type * as http from 'http'; const debug = Debug.extend('legacy-create-proxy-middleware'); @@ -12,18 +13,30 @@ const debug = Debug.extend('legacy-create-proxy-middleware'); * * Use {@link createProxyMiddleware} instead. */ -export function legacyCreateProxyMiddleware(shortHand: string): RequestHandler; -export function legacyCreateProxyMiddleware(legacyOptions: LegacyOptions): RequestHandler; -export function legacyCreateProxyMiddleware( - legacyContext: Filter, - legacyOptions: LegacyOptions -): RequestHandler; -export function legacyCreateProxyMiddleware(legacyContext, legacyOptions?): RequestHandler { +export function legacyCreateProxyMiddleware< + TReq = http.IncomingMessage, + TRes = http.ServerResponse +>(shortHand: string): RequestHandler; +export function legacyCreateProxyMiddleware< + TReq = http.IncomingMessage, + TRes = http.ServerResponse +>(legacyOptions: LegacyOptions): RequestHandler; +export function legacyCreateProxyMiddleware< + TReq = http.IncomingMessage, + TRes = http.ServerResponse +>( + legacyContext: Filter, + legacyOptions: LegacyOptions +): RequestHandler; +export function legacyCreateProxyMiddleware< + TReq = http.IncomingMessage, + TRes = http.ServerResponse +>(legacyContext, legacyOptions?): RequestHandler { debug('init'); - const options = legacyOptionsAdapter(legacyContext, legacyOptions); + const options = legacyOptionsAdapter(legacyContext, legacyOptions); - const proxyMiddleware = createProxyMiddleware(options); + const proxyMiddleware = createProxyMiddleware(options); // https://github.com/chimurai/http-proxy-middleware/pull/731/files#diff-07e6ad10bda0df091b737caed42767657cd0bd74a01246a1a0b7ab59c0f6e977L118 debug('add marker for patching req.url (old behavior)'); diff --git a/src/legacy/options-adapter.ts b/src/legacy/options-adapter.ts index b71ef609..acc14a51 100644 --- a/src/legacy/options-adapter.ts +++ b/src/legacy/options-adapter.ts @@ -20,11 +20,11 @@ const proxyEventMap = { /** * Convert {@link LegacyOptions legacy Options} to new {@link Options} */ -export function legacyOptionsAdapter( - legacyContext: Filter | LegacyOptions, - legacyOptions: LegacyOptions -): Options { - let options: LegacyOptions; +export function legacyOptionsAdapter( + legacyContext: Filter | LegacyOptions, + legacyOptions: LegacyOptions +): Options { + let options: LegacyOptions; let logger: Logger; // https://github.com/chimurai/http-proxy-middleware/pull/716 @@ -39,7 +39,7 @@ export function legacyOptionsAdapter( // https://github.com/chimurai/http-proxy-middleware/pull/722/files#diff-a2a171449d862fe29692ce031981047d7ab755ae7f84c707aef80701b3ea0c80L4 if (legacyContext && legacyOptions) { debug('map legacy context/filter to options.pathFilter'); - options = { ...legacyOptions, pathFilter: legacyContext as Filter }; + options = { ...legacyOptions, pathFilter: legacyContext as Filter }; logger = getLegacyLogger(options); logger.warn( @@ -53,7 +53,7 @@ export function legacyOptionsAdapter( ` ); } else if (legacyContext && !legacyOptions) { - options = { ...(legacyContext as Options) }; + options = { ...(legacyContext as LegacyOptions) }; logger = getLegacyLogger(options); } diff --git a/src/legacy/types.ts b/src/legacy/types.ts index 10137424..1e053ab3 100644 --- a/src/legacy/types.ts +++ b/src/legacy/types.ts @@ -1,4 +1,4 @@ -// import * as httpProxy from 'http-proxy'; +import type * as http from 'http'; import { Options } from '..'; /** @@ -6,7 +6,8 @@ import { Options } from '..'; * * Will be removed in a future version. */ -export interface LegacyOptions extends Options { +export interface LegacyOptions + extends Options { /** * @deprecated * Use `on.error` instead. diff --git a/src/path-filter.ts b/src/path-filter.ts index 37b1a5fb..6053fcbe 100644 --- a/src/path-filter.ts +++ b/src/path-filter.ts @@ -1,10 +1,15 @@ -import type { Filter, Request } from './types'; +import type { Filter } from './types'; import * as isGlob from 'is-glob'; import * as micromatch from 'micromatch'; import * as url from 'url'; import { ERRORS } from './errors'; +import type * as http from 'http'; -export function matchPathFilter(pathFilter: Filter = '/', uri: string, req: Request): boolean { +export function matchPathFilter( + pathFilter: Filter = '/', + uri: string, + req: http.IncomingMessage +): boolean { // single path if (isStringPath(pathFilter as string)) { return matchSingleStringPath(pathFilter as string, uri); @@ -30,7 +35,7 @@ export function matchPathFilter(pathFilter: Filter = '/', uri: string, req: Requ // custom matching if (typeof pathFilter === 'function') { const pathname = getUrlPathName(uri); - return pathFilter(pathname, req); + return pathFilter(pathname, req as unknown as TReq); } throw new Error(ERRORS.ERR_CONTEXT_MATCHER_GENERIC); diff --git a/src/plugins/default/error-response-plugin.ts b/src/plugins/default/error-response-plugin.ts index cc1e4803..25dd8378 100644 --- a/src/plugins/default/error-response-plugin.ts +++ b/src/plugins/default/error-response-plugin.ts @@ -1,8 +1,9 @@ import { getStatusCode } from '../../status-code'; -import { Plugin, Response } from '../../types'; +import { Plugin } from '../../types'; +import type * as http from 'http'; export const errorResponsePlugin: Plugin = (proxyServer, options) => { - proxyServer.on('error', (err, req, res: Response, target?) => { + proxyServer.on('error', (err, req, res: http.ServerResponse, target?) => { // Re-throw error. Not recoverable since req & res are empty. if (!req && !res) { throw err; // "Error: Must provide a proper URL as target" diff --git a/src/types.ts b/src/types.ts index e75421f3..69c29728 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,44 +9,52 @@ import type * as http from 'http'; import type * as httpProxy from 'http-proxy'; import type * as net from 'net'; -export type Request = T; -export type Response = T; export type NextFunction void> = T; -export interface RequestHandler { - (req: Request, res: Response, next?: NextFunction): void | Promise; - upgrade?: (req: Request, socket: net.Socket, head: any) => void; +export interface RequestHandler< + TReq = http.IncomingMessage, + TRes = http.ServerResponse, + TNext = NextFunction +> { + (req: TReq, res: TRes, next?: TNext): void | Promise; + upgrade?: (req: http.IncomingMessage, socket: net.Socket, head: any) => void; } -export type Filter = string | string[] | ((pathname: string, req: Request) => boolean); +export type Filter = + | string + | string[] + | ((pathname: string, req: TReq) => boolean); -export type Plugin = (proxyServer: httpProxy, options: Options) => void; +export interface Plugin { + (proxyServer: httpProxy, options: Options): void; +} -export type OnProxyEvent = { - error?: httpProxy.ErrorCallback; - proxyReq?: httpProxy.ProxyReqCallback; - proxyReqWs?: httpProxy.ProxyReqWsCallback; - proxyRes?: httpProxy.ProxyResCallback; +export interface OnProxyEvent { + error?: httpProxy.ErrorCallback; + proxyReq?: httpProxy.ProxyReqCallback; + proxyReqWs?: httpProxy.ProxyReqWsCallback; + proxyRes?: httpProxy.ProxyResCallback; open?: httpProxy.OpenCallback; - close?: httpProxy.CloseCallback; - start?: httpProxy.StartCallback; - end?: httpProxy.EndCallback; - econnreset?: httpProxy.EconnresetCallback; -}; + close?: httpProxy.CloseCallback; + start?: httpProxy.StartCallback; + end?: httpProxy.EndCallback; + econnreset?: httpProxy.EconnresetCallback; +} export type Logger = Pick; -export interface Options extends httpProxy.ServerOptions { +export interface Options + extends httpProxy.ServerOptions { /** * Narrow down requests to proxy or not. * Filter on {@link http.IncomingMessage.url `pathname`} which is relative to the proxy's "mounting" point in the server. * Or use the {@link http.IncomingMessage `req`} object for more complex filtering. */ - pathFilter?: Filter; + pathFilter?: Filter; pathRewrite?: | { [regexp: string]: string } - | ((path: string, req: Request) => string) - | ((path: string, req: Request) => Promise); + | ((path: string, req: TReq) => string) + | ((path: string, req: TReq) => Promise); /** * Access the internal http-proxy server instance to customize behavior * @@ -61,7 +69,7 @@ export interface Options extends httpProxy.ServerOptions { * }); * ``` */ - plugins?: Plugin[]; + plugins?: Plugin[]; /** * Eject pre-configured plugins. * NOTE: register your own error handlers to prevent server from crashing. @@ -83,11 +91,11 @@ export interface Options extends httpProxy.ServerOptions { * }); * ``` */ - on?: OnProxyEvent; + on?: OnProxyEvent; router?: | { [hostOrPath: string]: httpProxy.ServerOptions['target'] } - | ((req: Request) => httpProxy.ServerOptions['target']) - | ((req: Request) => Promise); + | ((req: TReq) => httpProxy.ServerOptions['target']) + | ((req: TReq) => Promise); /** * Log information from http-proxy-middleware * @example diff --git a/test/e2e/http-proxy-middleware.spec.ts b/test/e2e/http-proxy-middleware.spec.ts index 4ba3637e..cd113f91 100644 --- a/test/e2e/http-proxy-middleware.spec.ts +++ b/test/e2e/http-proxy-middleware.spec.ts @@ -1,7 +1,7 @@ import { createProxyMiddleware, createApp, createAppWithPath, fixRequestBody } from './test-kit'; import * as request from 'supertest'; import { Mockttp, getLocal, CompletedRequest } from 'mockttp'; -import type { Request, Response } from '../../src/types'; +import type * as http from 'http'; import type * as express from 'express'; import * as bodyParser from 'body-parser'; @@ -18,10 +18,10 @@ describe('E2E http-proxy-middleware', () => { describe('pathFilter matching', () => { describe('do not proxy', () => { - const mockReq: Request = { + const mockReq: http.IncomingMessage = { url: '/foo/bar', - } as Request; - const mockRes: Response = {} as Response; + } as http.IncomingMessage; + const mockRes: http.ServerResponse = {} as http.ServerResponse; const mockNext: express.NextFunction = jest.fn(); beforeEach(() => { diff --git a/test/e2e/test-kit.ts b/test/e2e/test-kit.ts index 53a89786..e160f85f 100644 --- a/test/e2e/test-kit.ts +++ b/test/e2e/test-kit.ts @@ -1,9 +1,9 @@ import * as express from 'express'; -import { Express, RequestHandler, ErrorRequestHandler } from 'express'; +import { Express, RequestHandler } from 'express'; export { createProxyMiddleware, responseInterceptor, fixRequestBody } from '../../src/index'; -export function createApp(...middlewares: (RequestHandler | ErrorRequestHandler)[]): Express { +export function createApp(...middlewares): Express { const app = express(); app.use(...middlewares); return app; diff --git a/test/types.spec.ts b/test/types.spec.ts index b0eaf6c5..7b486004 100644 --- a/test/types.spec.ts +++ b/test/types.spec.ts @@ -1,8 +1,9 @@ /* eslint-disable @typescript-eslint/no-empty-function */ +import * as express from 'express'; import * as http from 'http'; -import { createProxyMiddleware as middleware } from '../src'; -import type { Options } from '../src/types'; +import { createProxyMiddleware as middleware, fixRequestBody, responseInterceptor } from '../src'; +import type { Options, RequestHandler } from '../src/types'; describe('http-proxy-middleware TypeScript Types', () => { let options: Options; @@ -126,7 +127,7 @@ describe('http-proxy-middleware TypeScript Types', () => { }); describe('express request and response types', () => { - it('should get TypeScript errors when express specific properties are used', () => { + it('should get TypeScript type errors when express specific properties are used with base types', () => { options = { on: { proxyReq(proxyReq, req, res, options) { @@ -142,5 +143,240 @@ describe('http-proxy-middleware TypeScript Types', () => { expect(options).toBeDefined(); }); + + it('should get contextual types from express server', () => { + const app = express(); + app.use( + middleware({ + router: (req) => req.params, + pathFilter: (pathname, req) => !!req.params, + on: { + error(error, req, res, target) { + req.params; + + // https://www.typescriptlang.org/docs/handbook/2/narrowing.html + if (res instanceof http.ServerResponse) { + res.status(200).send('OK'); + } + }, + proxyReq(proxyReq, req, res, options) { + req.params; + res.status(200).send('OK'); + }, + proxyReqWs(proxyReq, req, socket, options, head) { + req.params; + }, + proxyRes(proxyRes, req, res) { + req.params; + res.status(200).send('OK'); + }, + close(proxyRes, proxySocket, proxyHead) { + proxyRes.params; + }, + start(req, res, target) { + req.params; + res.status(200).send('OK'); + }, + end(req, res, proxyRes) { + req.params; + res.status(200).send('OK'); + proxyRes.params; + }, + econnreset(error, req, res, target) { + req.params; + res.status(200).send('OK'); + }, + }, + }) + ); + + expect(app).toBeDefined(); + }); + + // FIXME: contextual types should work with express path middleware + // it('should get contextual types from express server', () => { + // const app = express(); + // app.use( + // '/', + // middleware({ + // router: (req) => req.params, + // pathFilter: (pathname, req) => !!req.params, + // on: { + // error(error, req, res, target) { + // req.params; + + // // https://www.typescriptlang.org/docs/handbook/2/narrowing.html + // if (res instanceof http.ServerResponse) { + // res.status(200).send('OK'); + // } + // }, + // proxyReq(proxyReq, req, res, options) { + // req.params; + // res.status(200).send('OK'); + // }, + // proxyReqWs(proxyReq, req, socket, options, head) { + // req.params; + // }, + // proxyRes(proxyRes, req, res) { + // req.params; + // res.status(200).send('OK'); + // }, + // close(proxyRes, proxySocket, proxyHead) { + // proxyRes.params; + // }, + // start(req, res, target) { + // req.params; + // res.status(200).send('OK'); + // }, + // end(req, res, proxyRes) { + // req.params; + // res.status(200).send('OK'); + // proxyRes.params; + // }, + // econnreset(error, req, res, target) { + // req.params; + // res.status(200).send('OK'); + // }, + // }, + // }) + // ); + + // expect(app).toBeDefined(); + // }); + + it('should work with explicit generic custom req & res types', () => { + interface MyRequest extends http.IncomingMessage { + myRequestParams: { [key: string]: string }; + } + + interface MyResponse extends http.ServerResponse { + myResponseParams: { [key: string]: string }; + } + + const proxy: RequestHandler = middleware({ + router: (req) => req.myRequestParams, + pathFilter: (pathname, req) => !!req.myRequestParams, + + on: { + error(error, req, res, target) { + req.myRequestParams; + + // https://www.typescriptlang.org/docs/handbook/2/narrowing.html + if (res instanceof http.ServerResponse) { + res.myResponseParams; + } + }, + proxyReq(proxyReq, req, res, options) { + req.myRequestParams; + res.myResponseParams; + }, + proxyReqWs(proxyReq, req, socket, options, head) { + req.myRequestParams; + }, + proxyRes(proxyRes, req, res) { + req.myRequestParams; + res.myResponseParams; + }, + close(proxyRes, proxySocket, proxyHead) { + proxyRes.myRequestParams; + }, + start(req, res, target) { + req.myRequestParams; + res.myResponseParams; + }, + end(req, res, proxyRes) { + req.myRequestParams; + res.myResponseParams; + proxyRes.myRequestParams; + }, + econnreset(error, req, res, target) { + req.myRequestParams; + res.myResponseParams; + }, + }, + }); + + expect(proxy).toBeDefined(); + }); + + it('should work with custom req & res types in responseInterceptor', () => { + interface MyRequest extends http.IncomingMessage { + myRequestParams: { [key: string]: string }; + } + + interface MyResponse extends http.ServerResponse { + myResponseParams: { [key: string]: string }; + } + + const proxy: RequestHandler = middleware({ + target: 'http://www.example.org', + on: { + error: (err: Error & { code?: string }, req, res) => { + err.code; + }, + proxyRes: responseInterceptor(async (buffer, proxyRes, req, res) => { + req.myRequestParams; + res.myResponseParams; + return buffer; + }), + }, + }); + + expect(proxy).toBeDefined(); + }); + + it('should work with express.Request with fixRequestBody', () => { + const proxy: RequestHandler = middleware({ + target: 'http://www.example.org', + on: { + proxyReq: fixRequestBody, + }, + }); + + expect(proxy).toBeDefined(); + }); + + it('should work with generic types in plugins', () => { + const proxy: RequestHandler = middleware({ + target: 'http://www.example.org', + plugins: [ + (proxyServer, options) => { + proxyServer.on('proxyReq', (proxyReq, req, res, options) => { + req.params; + res.status(200).send('OK'); + }); + }, + ], + }); + + expect(proxy).toBeDefined(); + }); + + it('should work with contextual Express types with shipped plugins', () => { + const app = express(); + app.use( + middleware({ + target: 'http://www.example.org', + plugins: [ + (proxyServer, options) => { + // fixRequestBody + proxyServer.on('proxyReq', fixRequestBody); + + // responseInterceptor + proxyServer.on( + 'proxyRes', + responseInterceptor(async (buffer, proxyRes, req, res) => { + req.params; + res.status(200).send('OK'); + return buffer; + }) + ); + }, + ], + }) + ); + + expect(app).toBeDefined(); + }); }); }); diff --git a/test/unit/fix-request-body.spec.ts b/test/unit/fix-request-body.spec.ts index bce9ec0f..fe254318 100644 --- a/test/unit/fix-request-body.spec.ts +++ b/test/unit/fix-request-body.spec.ts @@ -1,17 +1,22 @@ -import { ClientRequest } from 'http'; +import { Socket } from 'net'; +import { ClientRequest, IncomingMessage } from 'http'; import * as querystring from 'querystring'; -import { fixRequestBody } from '../../src/handlers/fix-request-body'; -import type * as express from 'express'; -import type { Request } from '../../src/types'; +import { fixRequestBody, BodyParserLikeRequest } from '../../src/handlers/fix-request-body'; -const fakeProxyRequest = () => { +const fakeProxyRequest = (): ClientRequest => { const proxyRequest = new ClientRequest('http://some-host'); proxyRequest.emit = jest.fn(); return proxyRequest; }; +const createRequestWithBody = (body: unknown): BodyParserLikeRequest => { + const req = new IncomingMessage(new Socket()) as BodyParserLikeRequest; + req.body = body; + return req; +}; + describe('fixRequestBody', () => { it('should not write when body is undefined', () => { const proxyRequest = fakeProxyRequest(); @@ -19,7 +24,7 @@ describe('fixRequestBody', () => { jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); - fixRequestBody(proxyRequest, { body: undefined } as Request); + fixRequestBody(proxyRequest, createRequestWithBody(undefined)); expect(proxyRequest.setHeader).not.toHaveBeenCalled(); expect(proxyRequest.write).not.toHaveBeenCalled(); @@ -32,7 +37,7 @@ describe('fixRequestBody', () => { jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); - fixRequestBody(proxyRequest, { body: {} } as Request); + fixRequestBody(proxyRequest, createRequestWithBody({})); expect(proxyRequest.setHeader).toHaveBeenCalled(); expect(proxyRequest.write).toHaveBeenCalled(); @@ -45,7 +50,7 @@ describe('fixRequestBody', () => { jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); - fixRequestBody(proxyRequest, { body: { someField: 'some value' } } as Request); + fixRequestBody(proxyRequest, createRequestWithBody({ someField: 'some value' })); const expectedBody = JSON.stringify({ someField: 'some value' }); expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length); @@ -59,7 +64,7 @@ describe('fixRequestBody', () => { jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); - fixRequestBody(proxyRequest, { body: { someField: 'some value' } } as Request); + fixRequestBody(proxyRequest, createRequestWithBody({ someField: 'some value' })); const expectedBody = querystring.stringify({ someField: 'some value' }); expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length); @@ -73,7 +78,7 @@ describe('fixRequestBody', () => { jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); - fixRequestBody(proxyRequest, { body: { someField: 'some value' } } as Request); + fixRequestBody(proxyRequest, createRequestWithBody({ someField: 'some value' })); const expectedBody = querystring.stringify({ someField: 'some value' }); expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length); diff --git a/test/unit/path-filter.spec.ts b/test/unit/path-filter.spec.ts index 66721d62..51e40ca1 100644 --- a/test/unit/path-filter.spec.ts +++ b/test/unit/path-filter.spec.ts @@ -1,8 +1,8 @@ -import type { Request } from '../../src/types'; +import type * as http from 'http'; import { matchPathFilter } from '../../src/path-filter'; describe('Path Filter', () => { - const fakeReq = {} as Request; + const fakeReq = {} as http.IncomingMessage; describe('String path matching', () => { let result; diff --git a/yarn.lock b/yarn.lock index bfa1fc84..819fb155 100644 --- a/yarn.lock +++ b/yarn.lock @@ -984,10 +984,10 @@ dependencies: "@types/node" "*" -"@types/http-proxy@^1.17.8": - version "1.17.9" - resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.9.tgz#7f0e7931343761efde1e2bf48c40f02f3f75705a" - integrity sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw== +"@types/http-proxy@^1.17.10": + version "1.17.10" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.10.tgz#e576c8e4a0cc5c6a138819025a88e167ebb38d6c" + integrity sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g== dependencies: "@types/node" "*"