Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: use node http base types [BREAKING CHANGE] #730

Merged
merged 1 commit into from
Mar 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
2 changes: 1 addition & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@
"fastify",
"globbing",
"gzipped",
"insertanchor",
"lcov",
"Lenna",
"lipsum",
"lorum",
"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