Skip to content

Commit

Permalink
feat(typescript): type improvements (#882)
Browse files Browse the repository at this point in the history
* feat(types): improve types

* refactor(types): remove internal Request and Response type

* feat(types): proxy handlers generic types

* refactor(handlers): use generic types in fixRequestBody and responseInterceptor

* refactor(package): remove optional @types/express

* test(types): use type narrowing

* feat(types): support contextual typing from server
  • Loading branch information
chimurai committed Mar 3, 2023
1 parent fd4e558 commit 0ef0be6
Show file tree
Hide file tree
Showing 23 changed files with 407 additions and 127 deletions.
6 changes: 6 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,11 @@ module.exports = {
'@typescript-eslint/no-var-requires': 'off',
},
},
{
files: ['src/**/*.ts'],
rules: {
'no-restricted-imports': ['error', { paths: ['express'] }],
},
},
],
};
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
11 changes: 4 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<express.Request, express.Response>} */
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);
Expand Down
10 changes: 1 addition & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
2 changes: 1 addition & 1 deletion recipes/servers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<NextApiRequest, NextApiResponse>({
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
pathRewrite: {
Expand Down
2 changes: 1 addition & 1 deletion src/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ERRORS } from './errors';
import { Options } from './types';

export function verifyConfig(options: Options): void {
export function verifyConfig<TReq, TRes>(options: Options<TReq, TRes>): void {
if (!options.target && !options.router) {
throw new Error(ERRORS.ERR_CONFIG_FACTORY_TARGET_MISSING);
}
Expand Down
8 changes: 4 additions & 4 deletions src/get-plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import {
proxyEventsPlugin,
} from './plugins/default';

export function getPlugins(options: Options): Plugin[] {
export function getPlugins<TReq, TRes>(options: Options<TReq, TRes>): Plugin<TReq, TRes>[] {
// 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<TReq, TRes>[] = options.plugins ?? [];
return [...defaultPlugins, ...userPlugins] as unknown as Plugin<TReq, TRes>[];
}
11 changes: 7 additions & 4 deletions src/handlers/fix-request-body.ts
Original file line number Diff line number Diff line change
@@ -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<express.Request>).body;
export function fixRequestBody<TReq = http.IncomingMessage>(
proxyReq: http.ClientRequest,
req: TReq
): void {
const requestBody = (req as unknown as BodyParserLikeRequest).body;

if (!requestBody) {
return;
Expand Down
28 changes: 17 additions & 11 deletions src/handlers/response-interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { getFunctionName } from '../utils/function';

const debug = Debug.extend('response-interceptor');

type Interceptor = (
type Interceptor<TReq = http.IncomingMessage, TRes = http.ServerResponse> = (
buffer: Buffer,
proxyRes: http.IncomingMessage,
req: http.IncomingMessage,
res: http.ServerResponse
proxyRes: TReq,
req: TReq,
res: TRes
) => Promise<Buffer | string>;

/**
Expand All @@ -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<TReq, TRes>) {
return async function proxyResResponseInterceptor(
proxyRes: http.IncomingMessage,
req: http.IncomingMessage,
res: http.ServerResponse
proxyRes: TReq,
req: TReq,
res: TRes
): Promise<void> {
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<TReq>(proxyRes, proxyRes.headers['content-encoding']);

// concat data stream
_proxyRes.on('data', (chunk) => (buffer = Buffer.concat([buffer, chunk])));
Expand Down Expand Up @@ -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<TReq extends http.IncomingMessage = http.IncomingMessage>(
proxyRes: TReq,
contentEncoding: string
): TReq {
let _proxyRes = proxyRes;
let decompress;

Expand Down Expand Up @@ -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;
Expand Down
27 changes: 14 additions & 13 deletions src/http-proxy-middleware.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<TReq, TRes> {
private wsInternalSubscribed = false;
private serverOnCloseSubscribed = false;
private proxyOptions: Options;
private proxy: httpProxy;
private proxyOptions: Options<TReq, TRes>;
private proxy: httpProxy<TReq, TRes>;
private pathRewriter;

constructor(options: Options) {
verifyConfig(options);
constructor(options: Options<TReq, TRes>) {
verifyConfig<TReq, TRes>(options);
this.proxyOptions = options;

debug(`create proxy server`);
Expand Down Expand Up @@ -74,8 +75,8 @@ export class HttpProxyMiddleware {
}
};

private registerPlugins(proxy: httpProxy, options: Options) {
const plugins = getPlugins(options);
private registerPlugins(proxy: httpProxy<TReq, TRes>, options: Options<TReq, TRes>) {
const plugins = getPlugins<TReq, TRes>(options);
plugins.forEach((plugin) => {
debug(`register plugin: "${getFunctionName(plugin)}"`);
plugin(proxy, options);
Expand All @@ -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);
Expand All @@ -103,7 +104,7 @@ export class HttpProxyMiddleware {
/**
* Determine whether request should be proxied.
*/
private shouldProxy = (pathFilter: Filter, req: Request): boolean => {
private shouldProxy = (pathFilter: Filter<TReq>, req: http.IncomingMessage): boolean => {
return matchPathFilter(pathFilter, req.url, req);
};

Expand All @@ -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()}
Expand All @@ -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) {
Expand All @@ -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);

Expand Down
13 changes: 9 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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<TReq, TRes>): RequestHandler<TReq, TRes, TNext> {
const { middleware } = new HttpProxyMiddleware<TReq, TRes>(options);
return middleware as unknown as RequestHandler<TReq, TRes, TNext>;
}

export * from './handlers';
Expand Down
31 changes: 22 additions & 9 deletions src/legacy/create-proxy-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -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<TReq, TRes>;
export function legacyCreateProxyMiddleware<
TReq = http.IncomingMessage,
TRes = http.ServerResponse
>(legacyOptions: LegacyOptions<TReq, TRes>): RequestHandler<TReq, TRes>;
export function legacyCreateProxyMiddleware<
TReq = http.IncomingMessage,
TRes = http.ServerResponse
>(
legacyContext: Filter<TReq>,
legacyOptions: LegacyOptions<TReq, TRes>
): RequestHandler<TReq, TRes>;
export function legacyCreateProxyMiddleware<
TReq = http.IncomingMessage,
TRes = http.ServerResponse
>(legacyContext, legacyOptions?): RequestHandler<TReq, TRes> {
debug('init');

const options = legacyOptionsAdapter(legacyContext, legacyOptions);
const options = legacyOptionsAdapter<TReq, TRes>(legacyContext, legacyOptions);

const proxyMiddleware = createProxyMiddleware(options);
const proxyMiddleware = createProxyMiddleware<TReq, TRes>(options);

// https://github.com/chimurai/http-proxy-middleware/pull/731/files#diff-07e6ad10bda0df091b737caed42767657cd0bd74a01246a1a0b7ab59c0f6e977L118
debug('add marker for patching req.url (old behavior)');
Expand Down
14 changes: 7 additions & 7 deletions src/legacy/options-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TReq, TRes>(
legacyContext: Filter<TReq> | LegacyOptions<TReq, TRes>,
legacyOptions: LegacyOptions<TReq, TRes>
): Options<TReq, TRes> {
let options: LegacyOptions<TReq, TRes>;
let logger: Logger;

// https://github.com/chimurai/http-proxy-middleware/pull/716
Expand All @@ -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<TReq> };
logger = getLegacyLogger(options);

logger.warn(
Expand All @@ -53,7 +53,7 @@ export function legacyOptionsAdapter(
`
);
} else if (legacyContext && !legacyOptions) {
options = { ...(legacyContext as Options) };
options = { ...(legacyContext as LegacyOptions<TReq, TRes>) };
logger = getLegacyLogger(options);
}

Expand Down
5 changes: 3 additions & 2 deletions src/legacy/types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// import * as httpProxy from 'http-proxy';
import type * as http from 'http';
import { Options } from '..';

/**
* @deprecated
*
* Will be removed in a future version.
*/
export interface LegacyOptions extends Options {
export interface LegacyOptions<TReq = http.IncomingMessage, TRes = http.ServerResponse>
extends Options<TReq, TRes> {
/**
* @deprecated
* Use `on.error` instead.
Expand Down

0 comments on commit 0ef0be6

Please sign in to comment.