Skip to content

Commit

Permalink
refactor: use node http base types [BREAKING CHANGE]
Browse files Browse the repository at this point in the history
  • Loading branch information
chimurai committed Mar 19, 2022
1 parent 0b30c5d commit f01e972
Show file tree
Hide file tree
Showing 14 changed files with 116 additions and 46 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![dependency Status](https://snyk.io/test/npm/http-proxy-middleware/badge.svg?style=flat-square)](https://snyk.io/test/npm/http-proxy-middleware)
[![npm](https://img.shields.io/npm/v/http-proxy-middleware?color=%23CC3534&style=flat-square)](https://www.npmjs.com/package/http-proxy-middleware)

Node.js proxying made simple. Configure proxy middleware with ease for [connect](https://github.com/senchalabs/connect), [express](https://github.com/strongloop/express), [browser-sync](https://github.com/BrowserSync/browser-sync) and [many more](#compatible-servers).
Node.js proxying made simple. Configure proxy middleware with ease for [connect](https://github.com/senchalabs/connect), [express](https://github.com/expressjs/express), [next.js](https://github.com/vercel/next.js) and [many more](#compatible-servers).

Powered by the popular Nodejitsu [`http-proxy`](https://github.com/nodejitsu/node-http-proxy). [![GitHub stars](https://img.shields.io/github/stars/nodejitsu/node-http-proxy.svg?style=social&label=Star)](https://github.com/nodejitsu/node-http-proxy)

Expand Down Expand Up @@ -534,6 +534,7 @@ View the [recipes](https://github.com/chimurai/http-proxy-middleware/tree/master
- [connect](https://www.npmjs.com/package/connect)
- [express](https://www.npmjs.com/package/express)
- [next.js](https://www.npmjs.com/package/next)
- [fastify](https://www.npmjs.com/package/fastify)
- [browser-sync](https://www.npmjs.com/package/browser-sync)
- [lite-server](https://www.npmjs.com/package/lite-server)
Expand Down
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"middlewares",
"millis",
"mockttp",
"nextjs",
"Nodejitsu",
"ntlm",
"proxied",
Expand Down
2 changes: 2 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
coverageReporters: ['text', 'lcov'],
collectCoverageFrom: ['src/**/*.*'],
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
};
7 changes: 7 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Uncomment the following lines for less noise in test output
*/

// console.info = jest.fn();
// console.log = jest.fn();
// console.error = jest.fn();
37 changes: 33 additions & 4 deletions recipes/servers.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ Overview of `http-proxy-middleware` implementation in different servers.

Missing a server? Feel free to extend this list of examples.

<!-- TOC depthfrom:2 insertanchor:false -->

- [Express](#express)
- [Connect](#connect)
- [Next.js](#nextjs)
- [Browser-Sync](#browser-sync)
- [fastify](#fastify)
- [Polka](#polka)
Expand All @@ -17,8 +16,6 @@ Missing a server? Feel free to extend this list of examples.
- [grunt-browser-sync](#grunt-browser-sync)
- [gulp-webserver](#gulp-webserver)

<!-- /TOC -->

## Express

https://github.com/expressjs/express
Expand Down Expand Up @@ -62,6 +59,38 @@ app.use(apiProxy);
http.createServer(app).listen(3000);
```

## Next.js

https://github.com/vercel/next.js
[![GitHub stars](https://img.shields.io/github/stars/vercel/next.js.svg?style=social&label=Star)](https://github.com/vercel/next.js)
![next.js downloads](https://img.shields.io/npm/dm/next)

Next project: `/pages/api/users.ts`

```typescript
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { createProxyMiddleware } from 'http-proxy-middleware';

const proxyMiddleware = createProxyMiddleware({
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
pathRewrite: {
'^/api/users': '/users',
},
});

export default function handler(req: NextApiRequest, res: NextApiResponse) {
proxyMiddleware(req, res, (result: unknown) => {
if (result instanceof Error) {
throw result;
}
});
}

// curl http://localhost:3000/api/users
```

## Browser-Sync

https://github.com/BrowserSync/browser-sync
Expand Down
5 changes: 2 additions & 3 deletions src/_handlers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type * as express from 'express';
import type { Options } from './types';
import type { Options, Request, Response } from './types';
import type * as httpProxy from 'http-proxy';
import { getInstance } from './logger';
const logger = getInstance();
Expand Down Expand Up @@ -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"
Expand Down
5 changes: 3 additions & 2 deletions src/handlers/fix-request-body.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type * as http from 'http';
import type * as express from 'express';
import type { Request } from '../types';
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 as Request<express.Request>).body;

if (!requestBody) {
return;
Expand Down
21 changes: 10 additions & 11 deletions src/http-proxy-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,28 +35,24 @@ 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);
}
};
}

// 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: RequestHandler = async (req, res, next?) => {
if (this.shouldProxy(this.proxyOptions.pathFilter, req)) {
try {
const activeProxyOptions = await this.prepareProxyRequest(req);
this.proxy.web(req, res, activeProxyOptions);
} catch (err) {
next(err);
next && next(err);
}
} else {
next();
next && next();
}

/**
Expand Down Expand Up @@ -104,7 +100,7 @@ export class HttpProxyMiddleware {
* Determine whether request should be proxied.
*/
private shouldProxy = (pathFilter: Filter, req: Request): boolean => {
const path = req.originalUrl || req.url;
const path = (req as Request<express.Request>).originalUrl || req.url;
return matchPathFilter(pathFilter, path, req);
};

Expand All @@ -119,7 +115,7 @@ export class HttpProxyMiddleware {
private prepareProxyRequest = async (req: Request) => {
// 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.url = (req as Request<express.Request>).originalUrl || req.url;

// store uri before it gets rewritten for logging
const originalPath = req.url;
Expand Down Expand Up @@ -179,7 +175,10 @@ export class HttpProxyMiddleware {
};

private logError = (err, req: Request, res: Response, target?) => {
const hostname = req.headers?.host || req.hostname || req.host; // (websocket) || (node0.10 || node 4/5)
const hostname =
req.headers?.host ||
(req as Request<express.Request>).hostname ||
(req as Request<express.Request>).host; // (websocket) || (node0.10 || node 4/5)
const requestHref = `${hostname}${req.url}`;
const targetHref = `${target?.href}`; // target is undefined when websocket errors

Expand Down
6 changes: 3 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HttpProxyMiddleware } from './http-proxy-middleware';
import { Options } from './types';
import type { Options, RequestHandler } from './types';

export function createProxyMiddleware(options: Options) {
export function createProxyMiddleware(options: Options): RequestHandler {
const { middleware } = new HttpProxyMiddleware(options);
return middleware;
}
Expand All @@ -15,4 +15,4 @@ export function createProxyMiddleware(options: Options) {

export * from './handlers';

export { Filter, Options, RequestHandler } from './types';
export type { Filter, Options, RequestHandler } from './types';
9 changes: 5 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@

/* 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<T = http.IncomingMessage> = T;
export type Response<T = http.ServerResponse> = T;
export type NextFunction<T = (err?: any) => void> = T;

export interface RequestHandler extends express.RequestHandler {
export interface RequestHandler {
(req: Request, res: Response, next?: NextFunction): void | Promise<void>;
upgrade?: (req: Request, socket: net.Socket, head: any) => void;
}

Expand Down
11 changes: 7 additions & 4 deletions test/e2e/http-proxy-middleware.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { createProxyMiddleware, createApp, createAppWithPath, fixRequestBody } from './test-kit';
import * as request from 'supertest';
import { Mockttp, getLocal, CompletedRequest } from 'mockttp';
import { Request, Response } from '../../src/types';
import { NextFunction } from 'express';
import type { Request, Response } from '../../src/types';
import type * as express from 'express';
import * as bodyParser from 'body-parser';

describe('E2E http-proxy-middleware', () => {
Expand All @@ -18,9 +18,12 @@ 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<express.Request> = {
url: '/foo/bar',
originalUrl: '/foo/bar',
} as Request<express.Request>;
const mockRes: Response = {} as Response;
const mockNext: NextFunction = jest.fn();
const mockNext: express.NextFunction = jest.fn();

beforeEach(() => {
const middleware = createProxyMiddleware({
Expand Down
19 changes: 19 additions & 0 deletions test/e2e/http-server.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as http from 'http';
import { createProxyMiddleware } from './test-kit';
import * as request from 'supertest';

describe('http integration', () => {
it('should work with raw node http RequestHandler', async () => {
const handler = createProxyMiddleware({
changeOrigin: true,
logLevel: 'silent',
target: 'http://httpbin.org',
});

const server = http.createServer(handler);
const response = await request(server).get('/get').expect(200);

expect(response.ok).toBe(true);
expect(response.body.url).toBe('http://httpbin.org/get');
});
});
27 changes: 17 additions & 10 deletions test/types.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/* eslint-disable @typescript-eslint/no-empty-function */

import * as http from 'http';
import { createProxyMiddleware as middleware } from '../src';
import { Options } from '../src/types';
import type { Options } from '../src/types';

describe('http-proxy-middleware TypeScript Types', () => {
let options: Options;
Expand All @@ -10,9 +13,19 @@ describe('http-proxy-middleware TypeScript Types', () => {
};
});

it('should create proxy with just options', () => {
const proxy = middleware(options);
expect(proxy).toBeDefined();
describe('createProxyMiddleware()', () => {
it('should create proxy with just options', () => {
const proxy = middleware(options);
expect(proxy).toBeDefined();
});

it('should create proxy and accept base http types (req, res) from native http server', () => {
const proxy = middleware(options);
const server = http.createServer(proxy);

expect(proxy).toBeDefined();
expect(server).toBeDefined();
});
});

describe('HPM Filters', () => {
Expand Down Expand Up @@ -118,39 +131,33 @@ describe('http-proxy-middleware TypeScript Types', () => {

describe('HPM http-proxy events', () => {
it('should have onError type', () => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
options = { onError: (err, req, res) => {} };
expect(options).toBeDefined();
});

it('should have onProxyReq type', () => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
options = { onProxyReq: (proxyReq, req, res) => {} };
expect(options).toBeDefined();
});

it('should have onProxyRes type', () => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
options = { onProxyRes: (proxyRes, req, res) => {} };
expect(options).toBeDefined();
});

it('should have onProxyReqWs type', () => {
options = {
// eslint-disable-next-line @typescript-eslint/no-empty-function
onProxyReqWs: (proxyReq, req, socket, opts, head) => {},
};
expect(options).toBeDefined();
});

it('should have onOpen type', () => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
options = { onOpen: (proxySocket) => {} };
expect(options).toBeDefined();
});

it('should have onClose type', () => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
options = { onClose: (res, socket, head) => {} };
expect(options).toBeDefined();
});
Expand Down
9 changes: 5 additions & 4 deletions test/unit/fix-request-body.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ClientRequest } 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';

const fakeProxyRequest = () => {
Expand All @@ -18,7 +19,7 @@ describe('fixRequestBody', () => {
jest.spyOn(proxyRequest, 'setHeader');
jest.spyOn(proxyRequest, 'write');

fixRequestBody(proxyRequest, { body: undefined } as Request);
fixRequestBody(proxyRequest, { body: undefined } as Request<express.Request>);

expect(proxyRequest.setHeader).not.toHaveBeenCalled();
expect(proxyRequest.write).not.toHaveBeenCalled();
Expand All @@ -31,7 +32,7 @@ describe('fixRequestBody', () => {
jest.spyOn(proxyRequest, 'setHeader');
jest.spyOn(proxyRequest, 'write');

fixRequestBody(proxyRequest, { body: {} } as Request);
fixRequestBody(proxyRequest, { body: {} } as Request<express.Request>);

expect(proxyRequest.setHeader).toHaveBeenCalled();
expect(proxyRequest.write).toHaveBeenCalled();
Expand All @@ -44,7 +45,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 Request<express.Request>);

const expectedBody = JSON.stringify({ someField: 'some value' });
expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length);
Expand All @@ -58,7 +59,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 Request<express.Request>);

const expectedBody = querystring.stringify({ someField: 'some value' });
expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length);
Expand Down

0 comments on commit f01e972

Please sign in to comment.