Repository: chimurai/http-proxy-middleware Branch: master Commit: 7f292f488f6a Files: 133 Total size: 271.4 KB Directory structure: gitextract_8j4z2j65/ ├── .devcontainer/ │ └── devcontainer.json ├── .editorconfig ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug.yml │ │ ├── config.yml │ │ └── feature.yml │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .gitpod.yml ├── .husky/ │ ├── .gitignore │ ├── commit-msg │ └── pre-commit ├── .lintstagedrc ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── .yarnrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.txt ├── LICENSE ├── MIGRATION.md ├── NODE_DEPRECATIONS.md ├── README.md ├── context7.json ├── cspell.json ├── eslint.config.mjs ├── examples/ │ ├── README.md │ ├── browser-sync/ │ │ └── index.js │ ├── connect/ │ │ └── index.js │ ├── express/ │ │ └── index.js │ ├── fastify/ │ │ └── index.js │ ├── http-server/ │ │ └── index.js │ ├── next-app/ │ │ ├── .gitignore │ │ ├── PROXY.md │ │ ├── README.md │ │ ├── app/ │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ ├── page.module.css │ │ │ └── page.tsx │ │ ├── next.config.mjs │ │ ├── package.json │ │ ├── pages/ │ │ │ └── api/ │ │ │ ├── _proxy.ts │ │ │ └── users.ts │ │ └── tsconfig.json │ ├── package.json │ ├── response-interceptor/ │ │ └── index.js │ └── websocket/ │ ├── index.html │ └── index.js ├── jest.config.js ├── jest.setup.js ├── package.json ├── patches/ │ ├── http-proxy+1.18.1.patch │ ├── tr46+0.0.3.patch │ ├── uri-js+4.4.1.patch │ └── whatwg-url+5.0.0.patch ├── recipes/ │ ├── README.md │ ├── async-response.md │ ├── basic.md │ ├── context-matching.md │ ├── corporate-proxy.md │ ├── delay.md │ ├── https.md │ ├── logLevel.md │ ├── logProvider.md │ ├── logger.md │ ├── modify-post.md │ ├── pathFilter.md │ ├── pathRewrite.md │ ├── proxy-events.md │ ├── response-interceptor.md │ ├── router.md │ ├── servers.md │ ├── virtual-hosts.md │ └── websocket.md ├── src/ │ ├── configuration.ts │ ├── debug.ts │ ├── errors.ts │ ├── factory.ts │ ├── get-plugins.ts │ ├── handlers/ │ │ ├── fix-request-body.ts │ │ ├── index.ts │ │ ├── public.ts │ │ └── response-interceptor.ts │ ├── http-proxy-middleware.ts │ ├── index.ts │ ├── legacy/ │ │ ├── create-proxy-middleware.ts │ │ ├── index.ts │ │ ├── options-adapter.ts │ │ ├── public.ts │ │ └── types.ts │ ├── logger.ts │ ├── path-filter.ts │ ├── path-rewriter.ts │ ├── plugins/ │ │ └── default/ │ │ ├── debug-proxy-errors-plugin.ts │ │ ├── error-response-plugin.ts │ │ ├── index.ts │ │ ├── logger-plugin.ts │ │ └── proxy-events.ts │ ├── router.ts │ ├── status-code.ts │ ├── types.ts │ └── utils/ │ ├── function.ts │ ├── logger-plugin.ts │ └── sanitize.ts ├── test/ │ ├── e2e/ │ │ ├── express-error-middleware.spec.ts │ │ ├── express-router.spec.ts │ │ ├── http-proxy-middleware.spec.ts │ │ ├── http-server.spec.ts │ │ ├── path-rewriter.spec.ts │ │ ├── plugins.spec.ts │ │ ├── response-interceptor.spec.ts │ │ ├── router.spec.ts │ │ ├── test-kit.ts │ │ └── websocket.spec.ts │ ├── legacy/ │ │ └── http-proxy-middleware.spec.ts │ ├── types.spec.ts │ └── unit/ │ ├── configuration.spec.ts │ ├── fix-request-body.spec.ts │ ├── get-plugins.spec.ts │ ├── logger.spec.ts │ ├── path-filter.spec.ts │ ├── path-rewriter.spec.ts │ ├── response-interceptor.spec.ts │ ├── router.spec.ts │ ├── status-code.spec.ts │ └── utils/ │ ├── function.spec.ts │ ├── logger-plugin.spec.ts │ └── sanitize.spec.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .devcontainer/devcontainer.json ================================================ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node { "name": "Node.js", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile "image": "mcr.microsoft.com/devcontainers/javascript-node:22", // Features to add to the dev container. More info: https://containers.dev/features. // "features": {}, // Configure tool-specific properties. "customizations": { // Configure properties specific to VS Code. "vscode": { "settings": { "workbench.colorTheme": "Default Dark+", "workbench.iconTheme": "material-icon-theme" }, "extensions": [ "bierner.markdown-preview-github-styles", "dbaeumer.vscode-eslint", "eamodio.gitlens", "EditorConfig.EditorConfig", "esbenp.prettier-vscode", "firsttris.vscode-jest-runner", "pkief.material-icon-theme", "streetsidesoftware.code-spell-checker", "yzhang.markdown-all-in-one" ] } }, // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [3000], // Use 'portsAttributes' to set default properties for specific forwarded ports. // More info: https://containers.dev/implementors/json_reference/#port-attributes // "portsAttributes": { // "3000": { // "label": "Hello Remote World", // "onAutoForward": "notify" // } // }, // Use 'postCreateCommand' to run commands after the container is created. "postCreateCommand": "yarn install" // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // "remoteUser": "root" } ================================================ FILE: .editorconfig ================================================ # http://editorconfig.org root = true [*] charset = utf-8 end_of_line = lf indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false # Matches the exact files [{package.json}] indent_size = 2 ================================================ FILE: .github/ISSUE_TEMPLATE/bug.yml ================================================ name: Bug Report description: 'Create a report to help us improve' body: - type: markdown attributes: value: 'Please note your issue will be closed without comment if do not fill out the issue checklist and provide ALL the requested information.' - type: checkboxes attributes: label: Checks description: Please verify that you've followed these steps. options: - label: I understand project setup issues should be asked on [StackOverflow](https://stackoverflow.com/questions/tagged/http-proxy-middleware) or in [GitHub Discussions](https://github.com/chimurai/http-proxy-middleware/discussions). required: true - label: I updated to latest `http-proxy-middleware`. required: true - type: textarea attributes: label: 'Describe the bug (be clear and concise)' validations: required: true - type: textarea attributes: label: 'Step-by-step reproduction instructions' description: 'If possible, please provide minimal example to demonstrate the issue. Create a minimal Github project or use the CodeSandbox template: https://codesandbox.io/s/http-proxy-middleware-44oc1' value: | 1. ... 2. ... render: shell validations: required: true - type: textarea attributes: label: 'Expected behavior (be clear and concise)' description: Tell us in a clear and concise description of what you expected to happen. validations: required: true - type: textarea attributes: render: shell label: 'How is http-proxy-middleware used in your project?' description: '`yarn why http-proxy-middleware` OR `npm ls http-proxy-middleware` output (mask private folder names with *****)' validations: required: true - type: textarea attributes: render: typescript label: 'What http-proxy-middleware configuration are you using?' description: 'Paste your http-proxy configuration here...' validations: required: true - type: textarea attributes: render: shell label: 'What OS/version and node/version are you seeing the problem?' description: 'Paste the output of `npx envinfo`' placeholder: 'ie. MacOS 11.4 and Node 16.2.0' validations: required: true - type: textarea attributes: label: 'Additional context (optional)' description: 'Please add any addition information that might help the investigation' ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Get help in GitHub Discussions url: https://github.com/chimurai/http-proxy-middleware/discussions about: Have a question? The quickest way to get help is on http-proxy-middleware's GitHub Discussions! ================================================ FILE: .github/ISSUE_TEMPLATE/feature.yml ================================================ name: Feature Request description: 'Suggest an idea for this project' body: - type: textarea attributes: label: "Describe the feature you'd love to see" placeholder: 'A clear and concise description of the feature request' validations: required: true - type: textarea attributes: label: 'Additional context (optional)' placeholder: 'Add any other context about the feature request here.' ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ## Description ## Motivation and Context ## How has this been tested? ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) ## Checklist: - [ ] My change requires a change to the documentation. - [ ] I have updated the documentation accordingly. ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: ['push', 'pull_request'] jobs: dependencies: name: Dependencies runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Use Node.js 22.x uses: actions/setup-node@v4 with: node-version: 22.x - uses: actions/cache@v4 id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) with: path: '**/node_modules' key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - name: yarn install if: steps.yarn-cache.outputs.cache-hit != 'true' # Over here! run: yarn install --frozen-lockfile --ignore-scripts - name: yarn audit run: yarn audit --audit-level high --groups dependencies env: CI: true build: name: Build runs-on: ubuntu-latest needs: [dependencies] steps: - uses: actions/checkout@v5 - name: Use Node.js 22.x uses: actions/setup-node@v4 with: node-version: 22.x - uses: actions/cache@v4 id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) with: path: '**/node_modules' key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - name: yarn install if: steps.yarn-cache.outputs.cache-hit != 'true' # Over here! run: yarn install --frozen-lockfile --ignore-scripts - name: yarn build run: yarn build env: CI: true pkg-pr-new: name: pkg-pr-new if: github.repository == 'chimurai/http-proxy-middleware' runs-on: ubuntu-latest needs: [build] steps: - uses: actions/checkout@v5 - name: Use Node.js 22.x uses: actions/setup-node@v4 with: node-version: 22.x - uses: actions/cache@v4 id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) with: path: '**/node_modules' key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - name: yarn install if: steps.yarn-cache.outputs.cache-hit != 'true' # Over here! run: yarn install --frozen-lockfile --ignore-scripts - name: pkg-pr-new - publish for testing purposes run: npx pkg-pr-new publish --compact --no-template env: CI: true GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} lint: name: Lint runs-on: ubuntu-latest needs: [dependencies] steps: - uses: actions/checkout@v5 - name: Use Node.js 22.x uses: actions/setup-node@v4 with: node-version: 22.x - uses: actions/cache@v4 id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) with: path: '**/node_modules' key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - name: yarn install if: steps.yarn-cache.outputs.cache-hit != 'true' # Over here! run: yarn install --frozen-lockfile --ignore-scripts - name: yarn lint run: yarn lint env: CI: true test: name: Test runs-on: ubuntu-latest needs: [dependencies] strategy: matrix: node-version: [20.x, 22.x, 24.x] steps: - uses: actions/checkout@v5 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - uses: actions/cache@v4 id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) with: path: '**/node_modules' key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - name: yarn install if: steps.yarn-cache.outputs.cache-hit != 'true' # Over here! run: yarn install --frozen-lockfile --ignore-scripts - name: yarn test run: yarn test --ci --reporters="default" --reporters="github-actions" env: CI: true coverage: name: Coverage runs-on: ubuntu-latest needs: [dependencies] steps: - uses: actions/checkout@v5 - name: Use Node.js 22.x uses: actions/setup-node@v4 with: node-version: 22.x - uses: actions/cache@v4 id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) with: path: '**/node_modules' key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - name: yarn install if: steps.yarn-cache.outputs.cache-hit != 'true' # Over here! run: yarn install --frozen-lockfile --ignore-scripts - name: yarn coverage run: yarn coverage - name: Coveralls uses: coverallsapp/github-action@master with: github-token: ${{ secrets.github_token }} spellcheck: name: Spellcheck runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: streetsidesoftware/cspell-action@v6 with: # Define glob patterns to filter the files to be checked. Use a new line between patterns to define multiple patterns. # The default is to check ALL files that were changed in in the pull_request or push. # Note: `ignorePaths` defined in cspell.json still apply. # Example: # files: | # **/*.{ts,js} # !dist/**/*.{ts,js} # # Hidden directories need an explicit .* to be included # .*/**/*.yml # # To not check hidden files, use: # files: "**" # # Default: ALL files files: '**' # The point in the directory tree to start spell checking. # Default: . root: '.' # Notification level to use with inline reporting of spelling errors. # Allowed values are: warning, error, none # Default: warning inline: error # Determines if the action should be failed if any spelling issues are found. # Allowed values are: true, false # Default: true strict: true # Limit the files checked to the ones in the pull request or push. incremental_files_only: false # Path to `cspell.json` config: 'cspell.json' ================================================ FILE: .github/workflows/publish.yml ================================================ name: publish to npmjs on: release: types: [prereleased, released] jobs: build-and-publish: # prevents this action from running on forks if: github.repository == 'chimurai/http-proxy-middleware' runs-on: ubuntu-latest permissions: contents: read id-token: write # Required for OIDC steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: '22.x' registry-url: 'https://registry.npmjs.org' # Ensure npm 11.5.1 or later is installed - name: Update npm run: npm install -g npm@latest - name: Install Dependencies run: yarn install --frozen-lockfile --ignore-scripts - name: Publish to NPM (beta) if: 'github.event.release.prerelease' run: npm publish --access public --tag beta - name: Publish to NPM (stable) if: '!github.event.release.prerelease' run: npm publish --access public ================================================ FILE: .gitignore ================================================ # Created by https://www.gitignore.io/api/node # Edit at https://www.gitignore.io/?templates=node ### Node ### # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env .env.test # parcel-bundler cache (https://parceljs.org/) .cache # next.js build output .next # nuxt.js build output .nuxt # vuepress build output .vuepress/dist # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # End of https://www.gitignore.io/api/node dist tsconfig.tsbuildinfo ================================================ FILE: .gitpod.yml ================================================ # https://gitpod.io/#https://github.com/chimurai/http-proxy-middleware # https://www.gitpod.io/docs/config-gitpod-file tasks: - init: | yarn install yarn build yarn test --maxWorkers=30% vscode: extensions: - bierner.markdown-preview-github-styles - dbaeumer.vscode-eslint - eamodio.gitlens - EditorConfig.EditorConfig - esbenp.prettier-vscode - streetsidesoftware.code-spell-checker - yzhang.markdown-all-in-one ================================================ FILE: .husky/.gitignore ================================================ _ ================================================ FILE: .husky/commit-msg ================================================ yarn commitlint --edit $1 ================================================ FILE: .husky/pre-commit ================================================ yarn lint-staged ================================================ FILE: .lintstagedrc ================================================ { "**/*.{js,ts,md,yml,json,html}": "prettier --write", "**/*.{js,ts}": "eslint --cache --fix" } ================================================ FILE: .npmrc ================================================ package-lock=false provenance=true ================================================ FILE: .prettierignore ================================================ dist coverage ================================================ FILE: .prettierrc ================================================ { "printWidth": 100, "tabWidth": 2, "semi": true, "singleQuote": true, "plugins": ["@trivago/prettier-plugin-sort-imports"], "importOrder": ["^node:.*$", "", "^[./]"], "importOrderSeparation": true, "importOrderSortSpecifiers": true } ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ "bierner.markdown-preview-github-styles", "dbaeumer.vscode-eslint", "eamodio.gitlens", "EditorConfig.EditorConfig", "esbenp.prettier-vscode", "streetsidesoftware.code-spell-checker", "yzhang.markdown-all-in-one" ] } ================================================ FILE: .vscode/settings.json ================================================ { "deno.enable": false, "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "markdown.extension.toc.levels": "2..3" } ================================================ FILE: .yarnrc ================================================ --ignore-scripts true ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## next - fix(types): fix Logger type - fix(error-response-plugin): sanitize input ## [v3.0.5](https://github.com/chimurai/http-proxy-middleware/releases/tag/v3.0.5) - fix(fixRequestBody): check readableLength ([#1096](https://github.com/chimurai/http-proxy-middleware/pull/1096)) ## [v3.0.4](https://github.com/chimurai/http-proxy-middleware/releases/tag/v3.0.4) - fix(fixRequestBody): handle invalid request ([#1092](https://github.com/chimurai/http-proxy-middleware/pull/1092)) - fix(fixRequestBody): prevent multiple .write() calls ([#1089](https://github.com/chimurai/http-proxy-middleware/pull/1089)) - fix(websocket): handle errors in handleUpgrade ([#823](https://github.com/chimurai/http-proxy-middleware/pull/823)) - ci(package): patch http-proxy ([#1084](https://github.com/chimurai/http-proxy-middleware/pull/1084)) - fix(fixRequestBody): support multipart/form-data ([#896](https://github.com/chimurai/http-proxy-middleware/pull/896)) - feat(types): export Plugin type ([#1071](https://github.com/chimurai/http-proxy-middleware/pull/1071)) ## [v3.0.3](https://github.com/chimurai/http-proxy-middleware/releases/tag/v3.0.3) - fix(pathFilter): handle errors ## [v3.0.2](https://github.com/chimurai/http-proxy-middleware/releases/tag/v3.0.2) - refactor(dependency): replace is-plain-obj with is-plain-object ([#1031](https://github.com/chimurai/http-proxy-middleware/pull/1031)) - chore(package): upgrade to eslint v9 ([#1032](https://github.com/chimurai/http-proxy-middleware/pull/1032)) - fix(logger-plugin): handle undefined protocol and hostname ([#1036](https://github.com/chimurai/http-proxy-middleware/pull/1036)) ## [v3.0.1](https://github.com/chimurai/http-proxy-middleware/releases/tag/v3.0.1) - fix(type): fix RequestHandler return type ([#980](https://github.com/chimurai/http-proxy-middleware/pull/980)) - refactor(errors): improve pathFilter error message ([#987](https://github.com/chimurai/http-proxy-middleware/pull/987)) - fix(logger-plugin): fix missing target port ([#989](https://github.com/chimurai/http-proxy-middleware/pull/989)) - ci(package): npm package provenance ([#991](https://github.com/chimurai/http-proxy-middleware/pull/1015)) - fix(logger-plugin): log target port when router option is used ([#1001](https://github.com/chimurai/http-proxy-middleware/pull/1001)) - refactor: fix circular dependencies ([#1010](https://github.com/chimurai/http-proxy-middleware/pull/1010)) - fix(fix-request-body): support '+json' content-type suffix ([#1015](https://github.com/chimurai/http-proxy-middleware/pull/1015)) ## [v3.0.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v3.0.0) This release contains some breaking changes. Please read the V3 discussion or follow the [MIGRATION.md](https://github.com/chimurai/http-proxy-middleware/blob/master/MIGRATION.md) guide. - 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)) - feat(legacyCreateProxyMiddleware): adapter with v2 behavior ([#754](https://github.com/chimurai/http-proxy-middleware/pull/754)) - docs(proxy events): fix new syntax ([#753](https://github.com/chimurai/http-proxy-middleware/pull/753)) - feat(debug): improve troubleshooting ([#752](https://github.com/chimurai/http-proxy-middleware/pull/752)) - test(path-rewriter): improve coverage ([#751](https://github.com/chimurai/http-proxy-middleware/pull/751)) - feat(ejectPlugins): skip registering default plugins ([#750](https://github.com/chimurai/http-proxy-middleware/pull/750)) - refactor: logging [BREAKING CHANGE] ([#749](https://github.com/chimurai/http-proxy-middleware/pull/749)) - refactor(handlers): refactor to plugins [BREAKING CHANGE] ([#745](https://github.com/chimurai/http-proxy-middleware/pull/745)) - feat(plugins): add support for plugins ([#732](https://github.com/chimurai/http-proxy-middleware/pull/732)) - docs: fix v3 documentation - fix: server mounting [BREAKING CHANGE] ([#731](https://github.com/chimurai/http-proxy-middleware/pull/731)) - test(fixRequestBody): fix broken test - refactor: use node http base types [BREAKING CHANGE] ([#730](https://github.com/chimurai/http-proxy-middleware/pull/730)) (special thanks: [@cdaringe](https://github.com/cdaringe) & [@devanshj](https://github.com/devanshj)) - feat(option): refactor context to pathFilter option [BREAKING CHANGE] ([#722](https://github.com/chimurai/http-proxy-middleware/pull/722)) - feat: remove shorthand usage [BREAKING CHANGE] ([#716](https://github.com/chimurai/http-proxy-middleware/pull/716)) ## [v2.0.6](https://github.com/chimurai/http-proxy-middleware/releases/tag/v2.0.6) - fix(proxyReqWs): catch socket errors ([#763](https://github.com/chimurai/http-proxy-middleware/pull/763)) ## [v2.0.5](https://github.com/chimurai/http-proxy-middleware/releases/tag/v2.0.5) - fix(error handler): add default handler to econnreset ([#759](https://github.com/chimurai/http-proxy-middleware/pull/759)) ## [v2.0.4](https://github.com/chimurai/http-proxy-middleware/releases/tag/v2.0.4) - fix(fix-request-body): improve content type check ([#725](https://github.com/chimurai/http-proxy-middleware/pull/725)) ([kevinxh](https://github.com/kevinxh)) ## [v2.0.3](https://github.com/chimurai/http-proxy-middleware/releases/tag/v2.0.3) - feat(package): optional @types/express peer dependency ([#707](https://github.com/chimurai/http-proxy-middleware/pull/707)) ## [v2.0.2](https://github.com/chimurai/http-proxy-middleware/releases/tag/v2.0.2) - chore(deps): update @types/http-proxy to 1.17.8 ([#701](https://github.com/chimurai/http-proxy-middleware/pull/701)) - fix(fixRequestBody): fix request body for empty JSON object requests ([#640](https://github.com/chimurai/http-proxy-middleware/pull/640)) ([mhassan1](https://github.com/mhassan1)) - fix(types): fix type regression ([#700](https://github.com/chimurai/http-proxy-middleware/pull/700)) ## [v2.0.1](https://github.com/chimurai/http-proxy-middleware/releases/tag/v2.0.1) - fix(fixRequestBody): fix type error ([#615](https://github.com/chimurai/http-proxy-middleware/pull/615)) - test(coverage): improve coverage config ([#609](https://github.com/chimurai/http-proxy-middleware/pull/609)) ([leonardobazico](https://github.com/leonardobazico)) - test: add test coverage to fixRequestBody and responseInterceptor ([#608](https://github.com/chimurai/http-proxy-middleware/pull/608)) ([leonardobazico](https://github.com/leonardobazico)) - chore(typescript): extract handlers types ([#603](https://github.com/chimurai/http-proxy-middleware/pull/603)) ([leonardobazico](https://github.com/leonardobazico)) ## [v2.0.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v2.0.0) - chore(package): drop node 10 [BREAKING CHANGE] ([#577](https://github.com/chimurai/http-proxy-middleware/pull/577)) ## [v1.3.1](https://github.com/chimurai/http-proxy-middleware/releases/tag/v1.3.1) - fix(fix-request-body): make sure the content-type exists ([#578](https://github.com/chimurai/http-proxy-middleware/pull/578)) ([oufeng](https://github.com/oufeng)) ## [v1.3.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v1.3.0) - docs(response interceptor): align with nodejs default utf8 ([#567](https://github.com/chimurai/http-proxy-middleware/pull/567)) - feat: try to proxy body even after body-parser middleware ([#492](https://github.com/chimurai/http-proxy-middleware/pull/492)) ([midgleyc](https://github.com/midgleyc)) ## [v1.2.1](https://github.com/chimurai/http-proxy-middleware/releases/tag/v1.2.1) - fix(response interceptor): proxy original response headers ([#563](https://github.com/chimurai/http-proxy-middleware/pull/563)) ## [v1.2.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v1.2.0) - feat(handler): response interceptor ([#520](https://github.com/chimurai/http-proxy-middleware/pull/520)) - fix(log error): handle undefined target when websocket errors ([#527](https://github.com/chimurai/http-proxy-middleware/pull/527)) ## [v1.1.2](https://github.com/chimurai/http-proxy-middleware/releases/tag/v1.1.2) - fix(log error): handle optional target ([#523](https://github.com/chimurai/http-proxy-middleware/pull/523)) ## [v1.1.1](https://github.com/chimurai/http-proxy-middleware/releases/tag/v1.1.1) - fix(error handler): re-throw http-proxy missing target error ([#517](https://github.com/chimurai/http-proxy-middleware/pull/517)) - refactor(dependency): remove `camelcase` - fix(option): optional `target` when `router` is used ([#512](https://github.com/chimurai/http-proxy-middleware/pull/512)) ## [v1.1.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v1.1.0) - fix(errorHandler): fix confusing error message ([#509](https://github.com/chimurai/http-proxy-middleware/pull/509)) - fix(proxy): close proxy when server closes ([#508](https://github.com/chimurai/http-proxy-middleware/pull/508)) - refactor(lodash): remove lodash ([#459](https://github.com/chimurai/http-proxy-middleware/pull/459)) ([#507](https://github.com/chimurai/http-proxy-middleware/pull/507)) ([TrySound](https://github.com/TrySound)) - fix(ETIMEDOUT): return 504 on ETIMEDOUT ([#480](https://github.com/chimurai/http-proxy-middleware/pull/480)) ([aremishevsky](https://github.com/aremishevsky)) ## [v1.0.6](https://github.com/chimurai/http-proxy-middleware/releases/tag/v1.0.6) - chore(deps): lodash 4.17.20 ([#475](https://github.com/chimurai/http-proxy-middleware/pull/475)) ## [v1.0.5](https://github.com/chimurai/http-proxy-middleware/releases/tag/v1.0.6) - chore(deps): lodash 4.17.19 ([#454](https://github.com/chimurai/http-proxy-middleware/pull/454)) ## [v1.0.4](https://github.com/chimurai/http-proxy-middleware/releases/tag/v1.0.4) - chore(deps): http-proxy 1.18.1 ([#442](https://github.com/chimurai/http-proxy-middleware/pull/442)) ## [v1.0.3](https://github.com/chimurai/http-proxy-middleware/releases/tag/v1.0.3) - build(package): exclude build artifact tsconfig.tsbuildinfo ([#415](https://github.com/chimurai/http-proxy-middleware/pull/415)) ## [v1.0.2](https://github.com/chimurai/http-proxy-middleware/releases/tag/v1.0.2) - fix(router): handle rejected promise in custom router ([#410](https://github.com/chimurai/http-proxy-middleware/pull/413)) ([bforbis](https://github.com/bforbis)) ## [v1.0.1](https://github.com/chimurai/http-proxy-middleware/releases/tag/v1.0.1) - fix(typescript): fix proxyRes and router types ([#410](https://github.com/chimurai/http-proxy-middleware/issues/410)) ([dylang](https://github.com/dylang)) ## [v1.0.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v1.0.0) - feat(createProxyMiddleware): explicit import http-proxy-middleware ([BREAKING CHANGE](https://github.com/chimurai/http-proxy-middleware/releases))([#400](https://github.com/chimurai/http-proxy-middleware/issues/400#issuecomment-587162378)) - feat(typescript): export http-proxy-middleware types ([#400](https://github.com/chimurai/http-proxy-middleware/issues/400)) - fix(typescript): ES6 target - TS1192 ([#400](https://github.com/chimurai/http-proxy-middleware/issues/400#issuecomment-587064349)) ## [v0.21.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.21.0) - feat(http-proxy): bump to v1.18.0 - feat: async router ([#379](https://github.com/chimurai/http-proxy-middleware/issues/379)) ([LiranBri](https://github.com/LiranBri)) - feat(typescript): types support ([#369](https://github.com/chimurai/http-proxy-middleware/pull/369)) - feat: async pathRewrite ([#397](https://github.com/chimurai/http-proxy-middleware/pull/397)) ([rsethc](https://github.com/rsethc)) ## [v0.20.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.20.0) - fix(ws): concurrent websocket requests do not get upgraded ([#335](https://github.com/chimurai/http-proxy-middleware/issues/335)) - chore: drop node 6 (BREAKING CHANGE) - chore: update to micromatch@4 ([BREAKING CHANGE](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md#400---2019-03-20)) - chore: update dev dependencies - refactor: migrate to typescript ([#328](https://github.com/chimurai/http-proxy-middleware/pull/328)) - feat(middleware): Promise / async support ([#328](https://github.com/chimurai/http-proxy-middleware/pull/328/files#diff-7890bfeb41abb0fc0ef2670749c84077R50)) - refactor: remove legacy options `proxyHost` and `proxyTable` (BREAKING CHANGE) ## [v0.19.1](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.19.1) - fix(log): handle case when error code is missing ([#303](https://github.com/chimurai/http-proxy-middleware/pull/303)) ## [v0.19.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.19.0) - feat(http-proxy): bump to v1.17.0 ([#261](https://github.com/chimurai/http-proxy-middleware/pull/261)) ## [v0.18.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.18.0) - fix(vulnerability): update micromatch to v3.x ([npm:braces:20180219](https://snyk.io/test/npm/http-proxy-middleware?tab=issues&severity=high&severity=medium&severity=low#npm:braces:20180219)) - test(node): drop node 0.x support ([#212](https://github.com/chimurai/http-proxy-middleware/pull/212)) ## [v0.17.4](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.17.4) - fix(ntlm authentication): fixed bug preventing proxying with ntlm authentication. ([#132](https://github.com/chimurai/http-proxy-middleware/pull/149)) (Thanks: [EladBezalel](https://github.com/EladBezalel), [oshri551](https://github.com/oshri551)) ## [v0.17.3](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.17.3) - fix(onError): improve default proxy error handling. http status codes (504, 502 and 500). ([#132](https://github.com/chimurai/http-proxy-middleware/pull/132)) ([graingert](https://github.com/graingert)) ## [v0.17.2](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.17.2) - feat(logging): improve error message & add link to Node errors page. ([#106](https://github.com/chimurai/http-proxy-middleware/pull/106)) ([cloudmu](https://github.com/cloudmu)) - feat(pathRewrite): path can be empty string. ([#110](https://github.com/chimurai/http-proxy-middleware/pull/110)) ([sunnylqm](https://github.com/sunnylqm)) - bug(websocket): memory leak when option 'ws:true' is used. ([#114](https://github.com/chimurai/http-proxy-middleware/pull/114)) ([julbra](https://github.com/julbra)) - chore(package.json): reduce package size. ([#109](https://github.com/chimurai/http-proxy-middleware/pull/109)) ## [v0.17.1](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.17.1) - fix(Express sub Router): 404 on non-proxy routes ([#94](https://github.com/chimurai/http-proxy-middleware/issues/94)) ## [v0.17.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.17.0) - fix(context matching): Use [RFC 3986 path](https://tools.ietf.org/html/rfc3986#section-3.3) in context matching. (excludes query parameters) ## [v0.16.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.16.0) - deprecated(proxyTable): renamed `proxyTable` to `router`. - feat(router): support for custom `router` function. ## [v0.15.2](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.15.2) - fix(websocket): fixes websocket upgrade. ## [v0.15.1](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.15.1) - feat(pathRewrite): expose `req` object to pathRewrite function. - fix(websocket): fixes websocket upgrade when both config.ws and external .upgrade() are used. ## [v0.15.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.15.0) - feat(pathRewrite): support for custom pathRewrite function. ## [v0.14.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.14.0) - feat(proxy): support proxy creation without context. - fix(connect mounting): use connect's `path` configuration to mount proxy. ## [v0.13.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.13.0) - feat(context): custom context matcher; when simple `path` matching is not sufficient. ## [v0.12.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.12.0) - add option `onProxyReqWs` (subscribe to http-proxy `proxyReqWs` event) - add option `onOpen` (subscribe to http-proxy `open` event) - add option `onClose` (subscribe to http-proxy `close` event) ## [v0.11.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.11.0) - improved logging ## [v0.10.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.10.0) - feat(proxyTable) - added proxyTable support for WebSockets. - fixed(proxyTable) - ensure original path (not rewritten path) is being used when `proxyTable` is used in conjunction with `pathRewrite`. ## [v0.9.1](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.9.1) - fix server crash when socket error not handled correctly. ## [v0.9.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.9.0) - support subscribing to http-proxy `proxyReq` event ([trbngr](https://github.com/trbngr)) - add `logLevel` and `logProvider` support ## [v0.8.2](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.8.2) - fix proxyError handler ([mTazelaar](https://github.com/mTazelaar)) ## [v0.8.1](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.8.1) - fix pathRewrite when `agent` is configured ## [v0.8.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.8.0) - support external websocket upgrade - fix websocket shorthand ## [v0.7.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.7.0) - support shorthand syntax - fix express/connect mounting ## [v0.6.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.6.0) - support proxyTable ## [v0.5.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.5.0) - support subscribing to http-proxy `error` event - support subscribing to http-proxy `proxyRes` event ## [v0.4.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.4.0) - support websocket ## [v0.3.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.3.0) - support wildcard / glob ## [v0.2.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.2.0) - support multiple paths ## [v0.1.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.1.0) - support path rewrite - deprecate proxyHost option ## [v0.0.5](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.0.5) - initial release ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing We love contributors! Your help is welcome to make this project better! Some simple guidelines we'd like you to follow. ## Got Questions or Problems? If you have questions about http-proxy-middle usage; Please check if your question hasn't been already answered on [Stack Overflow](http://stackoverflow.com/search?q=%22http-proxy-middleware%22) or in our [issue archive](https://github.com/chimurai/http-proxy-middleware/issues?utf8=%E2%9C%93&q=is%3Aissue+), [examples](https://github.com/chimurai/http-proxy-middleware/tree/master/examples) and [recipes](https://github.com/chimurai/http-proxy-middleware/tree/master/recipes). Since Nodejitsu's `http-proxy` is providing the actual proxy functionality; You might find your answer in their [documentation](https://github.com/nodejitsu/node-http-proxy), [issue archive](https://github.com/nodejitsu/node-http-proxy/issues?utf8=%E2%9C%93&q=is%3Aissue) or [examples](https://github.com/nodejitsu/node-http-proxy/tree/master/examples). ## Report Issues If you think you've found an issue, please submit it to the [Github issue tracker](https://github.com/chimurai/http-proxy-middleware/issues). "_[It doesn't work](https://goo.gl/GzkkTg)_" is not very useful for anyone. A good issue report should have a well described **problem description** and proxy **configuration**. A great issue report includes a **minimal example**. Properly format your code example for easier reading: [Code and Syntax Highlighting](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet#code-and-syntax-highlighting). The quality of your issue report will determine how quickly and deeply we'll delve into it. Some simple pointers for reporting an issue: #### Problem Description - describe the problem - steps taken (request urls) - found - expected #### Configuration / Setup - http-proxy-middleware version - http-proxy-middleware configuration It might be useful to provide server information in which http-proxy-middleware is used and the target server information to which requests are being proxied. - server + version (express, connect, browser-sync, etc...) - server port number - target server - target server port number ### Minimal example Provide a minimal example to exclude external factors. This will greatly help identifying the issue. Tips on how to create a minimal example: http://stackoverflow.com/help/mcve ## New Feature? Request a new feature by submitting an issue into our [Github issue tracker](https://github.com/chimurai/http-proxy-middleware/issues). PRs are welcome. Please discuss it in our [Github issue tracker](https://github.com/chimurai/http-proxy-middleware/issues) before you start working on it, to avoid wasting your time and effort in case we are already (planning to) working on it. ## Documentation Feel free to send PRs to improve the documentation, [examples](https://github.com/chimurai/http-proxy-middleware/tree/master/examples) and [recipes](https://github.com/chimurai/http-proxy-middleware/tree/master/recipes). ================================================ FILE: CONTRIBUTORS.txt ================================================ aremishevsky Bezalel bforbis cdaringe Chim cloudmu devanshj dylang Elad graingert julbra kevinxh leonardobazico Liran mhassan1 midgleyc mpth oshri oufeng rsethc sunnylqm Tazelaar trbngr ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 Steven Chim Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MIGRATION.md ================================================ # Migration guide - [v3 changes and discussions](#v3-changes-and-discussions) - [v2 to v3 adapter](#v2-to-v3-adapter) - [`legacyCreateProxyMiddleware`](#legacycreateproxymiddleware) - [v3 breaking changes](#v3-breaking-changes) - [Removed `req.url` patching](#removed-requrl-patching) - [`pathRewrite` (potential behavior change)](#pathrewrite-potential-behavior-change) - [Removed "shorthand" usage](#removed-shorthand-usage) - [Removed `context` argument](#removed-context-argument) - [Removed `logProvider` and `logLevel` options](#removed-logprovider-and-loglevel-options) - [Refactored proxy events](#refactored-proxy-events) ## v3 changes and discussions See list of changes in V3: ## v2 to v3 adapter ### `legacyCreateProxyMiddleware` Use the adapter to use v3 with minimal changes to your v2 implementation. 💡 When you use `legacyCreateProxyMiddleware` it will print out console messages in run-time to guide you on how to migrate legacy configurations. NOTE: `legacyCreateProxyMiddleware` will be removed in a future version. ```js // before const { createProxyMiddleware } = require('http-proxy-middleware'); createProxyMiddleware(...); // after const { legacyCreateProxyMiddleware } = require('http-proxy-middleware'); legacyCreateProxyMiddleware(...); ``` ```ts // before import { createProxyMiddleware, Options } from 'http-proxy-middleware'; createProxyMiddleware(...); // after import { legacyCreateProxyMiddleware, LegacyOptions } from 'http-proxy-middleware'; legacyCreateProxyMiddleware(...); ``` ## v3 breaking changes ### Removed `req.url` patching When proxy is mounted on a path, this path should be provided in the target. ```js // before app.use('/user', proxy({ target: 'http://www.example.org' })); // after app.use('/user', proxy({ target: 'http://www.example.org/user' })); ``` ### `pathRewrite` (potential behavior change) Related to removal of [`req.url` patching](#removed-requrl-patching). `pathRewrite` now only rewrites the `path` after the mount point. It was common to rewrite the `basePath` with the `pathRewrite` option: ```js // before app.use( '/user', proxy({ target: 'http://www.example.org', pathRewrite: { '^/user': '/secret' }, }), ); // after app.use('/user', proxy({ target: 'http://www.example.org/secret' })); ``` When proxy is mounted at the root, `pathRewrite` should still work as in v2. ```js // not affected app.use( proxy({ target: 'http://www.example.org', pathRewrite: { '^/user': '/secret' }, }), ); ``` ### Removed "shorthand" usage Specify the `target` option. ```js // before createProxyMiddleware('http://www.example.org'); // after createProxyMiddleware({ target: 'http://www.example.org' }); ``` ### Removed `context` argument The `context` argument has been moved to option: `pathFilter`. Functionality did not change. See [recipes/pathFilter.md](./recipes/pathFilter.md) for more information. ```js // before createProxyMiddleware('/path', { target: 'http://www.example.org' }); // after createProxyMiddleware({ target: 'http://www.example.org', pathFilter: '/path', }); ``` ### Removed `logProvider` and `logLevel` options Use your external logging library to _log_ and control the logging _level_. Only `info`, `warn`, `error` are used internally for compatibility across different loggers. If you use `winston`, make sure to enable interpolation: See [recipes/logger.md](./recipes/logger.md) for more information. ```js // new createProxyMiddleware({ target: 'http://www.example.org', logger: console, }); ``` ### Refactored proxy events See [recipes/proxy-events.md](./recipes/proxy-events.md) for more information. ```js // before createProxyMiddleware({ target: 'http://www.example.org', onError: () => {}, onProxyReq: () => {}, onProxyRes: () => {}, onProxyReqWs: () => {}, onOpen: () => {}, onClose: () => {}, }); // after createProxyMiddleware({ target: 'http://www.example.org', on: { error: () => {}, proxyReq: () => {}, proxyRes: () => {}, proxyReqWs: () => {}, open: () => {}, close: () => {}, }, }); ``` ================================================ FILE: NODE_DEPRECATIONS.md ================================================ # Node deprecation warnings Patch npm dependencies with: - Use `patch-package` () - Or with `pnpm` - - vite example: ## `util._extend` ```shell [DEP0060] DeprecationWarning: The `util._extend` API is deprecated. Please use Object.assign() instead. ``` ## `punycode` ```shell [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead. ``` ================================================ FILE: README.md ================================================ # http-proxy-middleware [![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/chimurai/http-proxy-middleware/ci.yml?branch=master&logo=github-actions&logoColor=white&style=flat-square)](https://github.com/chimurai/http-proxy-middleware/actions/workflows/ci.yml?query=branch%3Amaster) [![Coveralls](https://img.shields.io/coveralls/chimurai/http-proxy-middleware.svg?style=flat-square&logo=coveralls)](https://coveralls.io/r/chimurai/http-proxy-middleware) [![Known Vulnerabilities](https://snyk.io/test/github/chimurai/http-proxy-middleware/badge.svg)](https://snyk.io/test/github/chimurai/http-proxy-middleware) [![npm](https://img.shields.io/npm/v/http-proxy-middleware?color=%23CC3534&style=flat-square&logo=npm)](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/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/http-party/node-http-proxy). [![GitHub stars](https://img.shields.io/github/stars/http-party/node-http-proxy.svg?style=social&label=Star)](https://github.com/http-party/node-http-proxy) ## ⚠️ Note This page is showing documentation for version v3.x.x ([release notes](https://github.com/chimurai/http-proxy-middleware/releases)) See [MIGRATION.md](https://github.com/chimurai/http-proxy-middleware/blob/master/MIGRATION.md) for details on how to migrate from v2.x.x to v3.x.x If you're looking for older documentation. Go to: - - ## TL;DR Proxy `/api` requests to `http://www.example.org` :bulb: **Tip:** Set the option `changeOrigin` to `true` for [name-based virtual hosted sites](http://en.wikipedia.org/wiki/Virtual_hosting#Name-based). ```typescript // typescript import * as express from 'express'; import type { NextFunction, Request, Response } from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; import type { Filter, Options, RequestHandler } from 'http-proxy-middleware'; const app = express(); const proxyMiddleware = createProxyMiddleware({ target: 'http://www.example.org/api', changeOrigin: true, }); app.use('/api', proxyMiddleware); app.listen(3000); // proxy and keep the same base path "/api" // http://127.0.0.1:3000/api/foo/bar -> http://www.example.org/api/foo/bar ``` _All_ `http-proxy` [options](https://github.com/nodejitsu/node-http-proxy#options) can be used, along with some extra `http-proxy-middleware` [options](#options). ## Table of Contents - [Install](#install) - [Basic usage](#basic-usage) - [Express Server Example](#express-server-example) - [app.use(path, proxy)](#appusepath-proxy) - [Options](#options) - [`pathFilter` (string, \[\]string, glob, \[\]glob, function)](#pathfilter-string-string-glob-glob-function) - [`pathRewrite` (object/function)](#pathrewrite-objectfunction) - [`router` (object/function)](#router-objectfunction) - [`plugins` (Array)](#plugins-array) - [`ejectPlugins` (boolean) default: `false`](#ejectplugins-boolean-default-false) - [`logger` (Object)](#logger-object) - [`http-proxy` events](#http-proxy-events) - [`http-proxy` options](#http-proxy-options) - [WebSocket](#websocket) - [External WebSocket upgrade](#external-websocket-upgrade) - [Intercept and manipulate requests](#intercept-and-manipulate-requests) - [Intercept and manipulate responses](#intercept-and-manipulate-responses) - [Node.js 17+: ECONNREFUSED issue with IPv6 and localhost (#705)](#nodejs-17-econnrefused-issue-with-ipv6-and-localhost-705) - [Debugging](#debugging) - [Working examples](#working-examples) - [Recipes](#recipes) - [Compatible servers](#compatible-servers) - [Tests](#tests) - [Changelog](#changelog) - [License](#license) ## Install ```shell npm install --save-dev http-proxy-middleware ``` ## Basic usage Create and configure a proxy middleware with: `createProxyMiddleware(config)`. ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const apiProxy = createProxyMiddleware({ target: 'http://www.example.org', changeOrigin: true, }); // 'apiProxy' is now ready to be used as middleware in a server. ``` - **options.target**: target host to proxy to. _(protocol + host)_ - **options.changeOrigin**: for virtual hosted sites - see full list of [`http-proxy-middleware` configuration options](#options) ## Express Server Example An example with `express` server. ```javascript // include dependencies const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const app = express(); // 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 }); // mount `exampleProxy` in web server app.use('/api', exampleProxy); app.listen(3000); ``` ### app.use(path, proxy) If you want to use the server's `app.use` `path` parameter to match requests. Use `pathFilter` option to further include/exclude requests which you want to proxy. ```javascript app.use( createProxyMiddleware({ target: 'http://www.example.org/api', changeOrigin: true, pathFilter: '/api/proxy-only-this-path', }), ); ``` `app.use` documentation: - express: - connect: - polka: ## Options http-proxy-middleware options: ### `pathFilter` (string, []string, glob, []glob, function) Narrow down which requests should be proxied. The `path` used for filtering is the `request.url` pathname. In Express, this is the `path` relative to the mount-point of the proxy. - **path matching** - `createProxyMiddleware({...})` - matches any path, all requests will be proxied when `pathFilter` is not configured. - `createProxyMiddleware({ pathFilter: '/api', ...})` - matches paths starting with `/api` - **multiple path matching** - `createProxyMiddleware({ pathFilter: ['/api', '/ajax', '/someotherpath'], ...})` - **wildcard path matching** For fine-grained control you can use wildcard matching. Glob pattern matching is done by _micromatch_. Visit [micromatch](https://www.npmjs.com/package/micromatch) or [glob](https://www.npmjs.com/package/glob) for more globbing examples. - `createProxyMiddleware({ pathFilter: '**', ...})` matches any path, all requests will be proxied. - `createProxyMiddleware({ pathFilter: '**/*.html', ...})` matches any path which ends with `.html` - `createProxyMiddleware({ pathFilter: '/*.html', ...})` matches paths directly under path-absolute - `createProxyMiddleware({ pathFilter: '/api/**/*.html', ...})` matches requests ending with `.html` in the path of `/api` - `createProxyMiddleware({ pathFilter: ['/api/**', '/ajax/**'], ...})` combine multiple patterns - `createProxyMiddleware({ pathFilter: ['/api/**', '!**/bad.json'], ...})` exclusion **Note**: In multiple path matching, you cannot use string paths and wildcard paths together. - **custom matching** For full control you can provide a custom function to determine which requests should be proxied or not. ```javascript /** * @return {Boolean} */ const pathFilter = function (path, req) { return path.match('^/api') && req.method === 'GET'; }; const apiProxy = createProxyMiddleware({ target: 'http://www.example.org', pathFilter: pathFilter, }); ``` ### `pathRewrite` (object/function) Rewrite target's url path. Object-keys will be used as _RegExp_ to match paths. ```javascript // rewrite path pathRewrite: {'^/old/api' : '/new/api'} // remove path pathRewrite: {'^/remove/api' : ''} // add base path pathRewrite: {'^/' : '/basepath/'} // custom rewriting pathRewrite: function (path, req) { return path.replace('/api', '/base/api') } // custom rewriting, returning Promise pathRewrite: async function (path, req) { const should_add_something = await httpRequestToDecideSomething(path); if (should_add_something) path += "something"; return path; } ``` ### `router` (object/function) Re-target `option.target` for specific requests. ```javascript // Use `host` and/or `path` to match requests. First match will be used. // The order of the configuration matters. router: { 'integration.localhost:3000' : 'http://127.0.0.1:8001', // host only 'staging.localhost:3000' : 'http://127.0.0.1:8002', // host only 'localhost:3000/api' : 'http://127.0.0.1:8003', // host + path '/rest' : 'http://127.0.0.1:8004' // path only } // Custom router function (string target) router: function(req) { return 'http://127.0.0.1:8004'; } // Custom router function (target object) router: function(req) { return { protocol: 'https:', // The : is required host: '127.0.0.1', port: 8004 }; } // Asynchronous router function which returns promise router: async function(req) { const url = await doSomeIO(); return url; } ``` ### `plugins` (Array) ```js const simpleRequestLogger = (proxyServer, options) => { proxyServer.on('proxyReq', (proxyReq, req, res) => { console.log(`[HPM] [${req.method}] ${req.url}`); // outputs: [HPM] GET /users }); }, const config = { target: `http://example.org`, changeOrigin: true, plugins: [simpleRequestLogger], }; ``` ### `ejectPlugins` (boolean) default: `false` If you're not satisfied with the pre-configured plugins, you can eject them by configuring `ejectPlugins: true`. NOTE: register your own error handlers to prevent server from crashing. ```js // eject default plugins and manually add them back const { debugProxyErrorsPlugin, // subscribe to proxy errors to prevent server from crashing loggerPlugin, // log proxy events to a logger (ie. console) errorResponsePlugin, // return 5xx response on proxy error proxyEventsPlugin, // implements the "on:" option } = require('http-proxy-middleware'); createProxyMiddleware({ target: `http://example.org`, changeOrigin: true, ejectPlugins: true, plugins: [debugProxyErrorsPlugin, loggerPlugin, errorResponsePlugin, proxyEventsPlugin], }); ``` ### `logger` (Object) Configure a logger to output information from http-proxy-middleware: ie. `console`, `winston`, `pino`, `bunyan`, `log4js`, etc... Only `info`, `warn`, `error` are used internally for compatibility across different loggers. If you use `winston`, make sure to enable interpolation: See also logger recipes ([recipes/logger.md](https://github.com/chimurai/http-proxy-middleware/blob/master/recipes/logger.md)) for more details. ```javascript createProxyMiddleware({ logger: console, }); ``` ## `http-proxy` events Subscribe to [http-proxy events](https://github.com/nodejitsu/node-http-proxy#listening-for-proxy-events) with the `on` option: ```js createProxyMiddleware({ target: 'http://www.example.org', on: { proxyReq: (proxyReq, req, res) => { /* handle proxyReq */ }, proxyRes: (proxyRes, req, res) => { /* handle proxyRes */ }, error: (err, req, res) => { /* handle error */ }, }, }); ``` - **option.on.error**: function, subscribe to http-proxy's `error` event for custom error handling. ```javascript function onError(err, req, res, target) { res.writeHead(500, { 'Content-Type': 'text/plain', }); res.end('Something went wrong. And we are reporting a custom error message.'); } ``` - **option.on.proxyRes**: function, subscribe to http-proxy's `proxyRes` event. ```javascript function onProxyRes(proxyRes, req, res) { proxyRes.headers['x-added'] = 'foobar'; // add new header to response delete proxyRes.headers['x-removed']; // remove header from response } ``` - **option.on.proxyReq**: function, subscribe to http-proxy's `proxyReq` event. ```javascript function onProxyReq(proxyReq, req, res) { // add custom header to request proxyReq.setHeader('x-added', 'foobar'); // or log the req } ``` - **option.on.proxyReqWs**: function, subscribe to http-proxy's `proxyReqWs` event. ```javascript function onProxyReqWs(proxyReq, req, socket, options, head) { // add custom header proxyReq.setHeader('X-Special-Proxy-Header', 'foobar'); } ``` - **option.on.open**: function, subscribe to http-proxy's `open` event. ```javascript function onOpen(proxySocket) { // listen for messages coming FROM the target here proxySocket.on('data', hybridParseAndLogMessage); } ``` - **option.on.close**: function, subscribe to http-proxy's `close` event. ```javascript function onClose(res, socket, head) { // view disconnected websocket connections console.log('Client disconnected'); } ``` ## `http-proxy` options The following options are provided by the underlying [http-proxy](https://github.com/nodejitsu/node-http-proxy#options) library. - **option.target**: url string to be parsed with the url module - **option.forward**: url string to be parsed with the url module - **option.agent**: object to be passed to http(s).request (see Node's [https agent](http://nodejs.org/api/https.html#https_class_https_agent) and [http agent](http://nodejs.org/api/http.html#http_class_http_agent) objects) - **option.ssl**: object to be passed to https.createServer() - **option.ws**: true/false: if you want to proxy websockets - **option.xfwd**: true/false, adds x-forward headers - **option.secure**: true/false, if you want to verify the SSL Certs - **option.toProxy**: true/false, passes the absolute URL as the `path` (useful for proxying to proxies) - **option.prependPath**: true/false, Default: true - specify whether you want to prepend the target's path to the proxy path - **option.ignorePath**: true/false, Default: false - specify whether you want to ignore the proxy path of the incoming request (note: you will have to append / manually if required). - **option.localAddress** : Local interface string to bind for outgoing connections - **option.changeOrigin**: true/false, Default: false - changes the origin of the host header to the target URL - **option.preserveHeaderKeyCase**: true/false, Default: false - specify whether you want to keep letter case of response header key - **option.auth** : Basic authentication i.e. 'user:password' to compute an Authorization header. - **option.hostRewrite**: rewrites the location hostname on (301/302/307/308) redirects. - **option.autoRewrite**: rewrites the location host/port on (301/302/307/308) redirects based on requested host/port. Default: false. - **option.protocolRewrite**: rewrites the location protocol on (301/302/307/308) redirects to 'http' or 'https'. Default: null. - **option.cookieDomainRewrite**: rewrites domain of `set-cookie` headers. Possible values: - `false` (default): disable cookie rewriting - String: new domain, for example `cookieDomainRewrite: "new.domain"`. To remove the domain, use `cookieDomainRewrite: ""`. - Object: mapping of domains to new domains, use `"*"` to match all domains. For example keep one domain unchanged, rewrite one domain and remove other domains: ```jsonc cookieDomainRewrite: { "unchanged.domain": "unchanged.domain", "old.domain": "new.domain", "*": "" } ``` - **option.cookiePathRewrite**: rewrites path of `set-cookie` headers. Possible values: - `false` (default): disable cookie rewriting - String: new path, for example `cookiePathRewrite: "/newPath/"`. To remove the path, use `cookiePathRewrite: ""`. To set path to root use `cookiePathRewrite: "/"`. - Object: mapping of paths to new paths, use `"*"` to match all paths. For example, to keep one path unchanged, rewrite one path and remove other paths: ```jsonc cookiePathRewrite: { "/unchanged.path/": "/unchanged.path/", "/old.path/": "/new.path/", "*": "" } ``` - **option.headers**: object, adds [request headers](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields). (Example: `{host:'www.example.org'}`) - **option.proxyTimeout**: timeout (in millis) when proxy receives no response from target - **option.timeout**: timeout (in millis) for incoming requests - **option.followRedirects**: true/false, Default: false - specify whether you want to follow redirects - **option.selfHandleResponse** true/false, if set to true, none of the webOutgoing passes are called and it's your responsibility to appropriately return the response by listening and acting on the `proxyRes` event - **option.buffer**: stream of data to send as the request body. Maybe you have some middleware that consumes the request stream before proxying it on e.g. If you read the body of a request into a field called 'req.rawbody' you could restream this field in the buffer option: ```javascript 'use strict'; const streamify = require('stream-array'); const HttpProxy = require('http-proxy'); const proxy = new HttpProxy(); module.exports = (req, res, next) => { proxy.web( req, res, { target: 'http://127.0.0.1:4003/', buffer: streamify(req.rawBody), }, next, ); }; ``` ## WebSocket ```javascript // verbose api createProxyMiddleware({ pathFilter: '/', target: 'http://echo.websocket.org', ws: true }); ``` ### External WebSocket upgrade In the previous WebSocket examples, http-proxy-middleware relies on a initial http request in order to listen to the http `upgrade` event. If you need to proxy WebSockets without the initial http request, you can subscribe to the server's http `upgrade` event manually. ```javascript const wsProxy = createProxyMiddleware({ target: 'ws://echo.websocket.org', changeOrigin: true }); const app = express(); app.use(wsProxy); const server = app.listen(3000); server.on('upgrade', wsProxy.upgrade); // <-- subscribe to http 'upgrade' ``` ## Intercept and manipulate requests Intercept requests from downstream by defining `onProxyReq` in `createProxyMiddleware`. Currently the only pre-provided request interceptor is `fixRequestBody`, which is used to fix proxied POST requests when `bodyParser` is applied before this middleware. Example: ```javascript const { createProxyMiddleware, fixRequestBody } = require('http-proxy-middleware'); const proxy = createProxyMiddleware({ /** * Fix bodyParser **/ on: { proxyReq: fixRequestBody, }, }); ``` ## Intercept and manipulate responses Intercept responses from upstream with `responseInterceptor`. (Make sure to set `selfHandleResponse: true`) Responses which are compressed with `brotli`, `gzip` and `deflate` will be decompressed automatically. The response will be returned as `buffer` ([docs](https://nodejs.org/api/buffer.html)) which you can manipulate. With `buffer`, response manipulation is not limited to text responses (html/css/js, etc...); image manipulation will be possible too. ([example](https://github.com/chimurai/http-proxy-middleware/blob/master/recipes/response-interceptor.md#manipulate-image-response)) NOTE: `responseInterceptor` disables streaming of target's response. Example: ```javascript const { createProxyMiddleware, responseInterceptor } = require('http-proxy-middleware'); const proxy = createProxyMiddleware({ /** * IMPORTANT: avoid res.end being called automatically **/ selfHandleResponse: true, // res.end() will be called internally by responseInterceptor() /** * Intercept response and replace 'Hello' with 'Goodbye' **/ on: { proxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => { const response = responseBuffer.toString('utf8'); // convert buffer to string return response.replace('Hello', 'Goodbye'); // manipulate response and return the result }), }, }); ``` Check out [interception recipes](https://github.com/chimurai/http-proxy-middleware/blob/master/recipes/response-interceptor.md#readme) for more examples. ## Node.js 17+: ECONNREFUSED issue with IPv6 and localhost ([#705](https://github.com/chimurai/http-proxy-middleware/issues/705)) Node.js 17+ no longer prefers IPv4 over IPv6 for DNS lookups. E.g. It's **not** guaranteed that `localhost` will be resolved to `127.0.0.1` – it might just as well be `::1` (or some other IP address). If your target server only accepts IPv4 connections, trying to proxy to `localhost` will fail if resolved to `::1` (IPv6). Ways to solve it: - Change `target: "http://localhost"` to `target: "http://127.0.0.1"` (IPv4). - Change the target server to (also) accept IPv6 connections. - Add this flag when running `node`: `node index.js --dns-result-order=ipv4first`. (Not recommended.) > Note: There’s a thing called [Happy Eyeballs](https://en.wikipedia.org/wiki/Happy_Eyeballs) which means connecting to both IPv4 and IPv6 in parallel, which Node.js doesn’t have, but explains why for example `curl` can connect. ## Debugging Configure the `DEBUG` environment variable enable debug logging. See [`debug`](https://github.com/debug-js/debug#readme) project for more options. ```shell DEBUG=http-proxy-middleware* node server.js $ http-proxy-middleware proxy created +0ms $ http-proxy-middleware proxying request to target: 'http://www.example.org' +359ms ``` ## Working examples View and play around with [working examples](https://github.com/chimurai/http-proxy-middleware/tree/master/examples). - Browser-Sync ([example source](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/browser-sync/index.js)) - express ([example source](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/express/index.js)) - connect ([example source](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/connect/index.js)) - WebSocket ([example source](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/websocket/index.js)) - Response Manipulation ([example source](https://github.com/chimurai/http-proxy-middleware/blob/master/examples/response-interceptor/index.js)) ## Recipes View the [recipes](https://github.com/chimurai/http-proxy-middleware/tree/master/recipes) for common use cases. ## Compatible servers `http-proxy-middleware` is compatible with the following servers: - [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) - [polka](https://github.com/lukeed/polka) - [grunt-contrib-connect](https://www.npmjs.com/package/grunt-contrib-connect) - [grunt-browser-sync](https://www.npmjs.com/package/grunt-browser-sync) - [gulp-connect](https://www.npmjs.com/package/gulp-connect) - [gulp-webserver](https://www.npmjs.com/package/gulp-webserver) Sample implementations can be found in the [server recipes](https://github.com/chimurai/http-proxy-middleware/tree/master/recipes/servers.md). ## Tests Run the test suite: ```bash # install dependencies $ yarn # linting $ yarn lint $ yarn lint:fix # building (compile typescript to js) $ yarn build # unit tests $ yarn test # code coverage $ yarn cover # check spelling mistakes $ yarn spellcheck ``` ## Changelog - [View changelog](https://github.com/chimurai/http-proxy-middleware/blob/master/CHANGELOG.md) ## License The MIT License (MIT) Copyright (c) 2015-2025 Steven Chim ================================================ FILE: context7.json ================================================ { "$schema": "https://context7.com/schema/context7.json", "projectTitle": "http-proxy-middleware", "description": "Node.js proxy middleware for express-like servers: express, connect, next.js and more.", "previousVersions": [ { "tag": "v2.0.8", "title": "version v2.0.8" }, { "tag": "v1.3.1", "title": "version v1.3.1" } ] } ================================================ FILE: cspell.json ================================================ { "language": "en", "dictionaries": ["node", "npm", "typescript", "contributors"], "ignorePaths": [ "node_modules/**", "coverage/**", "dist/**", "patches/**", "tsconfig.tsbuildinfo", "package.json", "yarn.lock", "devcontainer.json", ".gitpod.yml", "*.tgz" ], "dictionaryDefinitions": [ { "name": "contributors", "path": "CONTRIBUTORS.txt" } ], "ignoreRegExpList": ["[a-z]+path", "\\]\\(#[a-z-]+\\)", "%[\\dA-Z]{2}"], "words": [ "brotli", "camelcase", "codesandbox", "deepcode", "depthfrom", "fastify", "globbing", "gzipped", "jsonplaceholder", "lcov", "Lenna", "lipsum", "lorum", "middlewares", "millis", "mockttp", "nextjs", "Nodejitsu", "ntlm", "pino", "proxied", "proxying", "rawbody", "restream", "snyk", "streamify", "trivago", "tseslint", "typicode", "vhosted", "websockets", "xfwd" ] } ================================================ FILE: eslint.config.mjs ================================================ // @ts-check import eslint from '@eslint/js'; import { defineConfig, globalIgnores } from 'eslint/config'; import globals from 'globals'; import tseslint from 'typescript-eslint'; export default defineConfig( eslint.configs.recommended, tseslint.configs.recommended, globalIgnores(['dist']), { languageOptions: { globals: { ...globals.node, }, }, }, { files: ['**/*.js'], rules: { '@typescript-eslint/no-require-imports': 'off', '@typescript-eslint/no-var-requires': 'off', }, }, { files: ['**/*.ts'], rules: { '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': [ 'error', { vars: 'all', args: 'none', ignoreRestSiblings: false }, ], }, }, { files: ['src/**/*.ts'], rules: { 'no-restricted-imports': ['error', { paths: ['express'] }], }, }, ); ================================================ FILE: examples/README.md ================================================ # Examples View working examples of `http-proxy-middleware` implemented in popular servers. To run and view the [examples](https://github.com/chimurai/http-proxy-middleware/tree/master/examples); Clone the `http-proxy-middleware` repo and install the dependencies: ## Install ```shell # git clone https://github.com/chimurai/http-proxy-middleware.git yarn install:all yarn build ``` ## Run examples Run the example from root folder: ```shell node examples/browser-sync ``` ```shell node examples/connect ``` ```shell node examples/express ``` ```shell node examples/websocket ``` ## Server recipes You can find more server implementations with `http-proxy-middleware` in the [server recipes](https://github.com/chimurai/http-proxy-middleware/tree/master/recipes/servers.md) ================================================ FILE: examples/browser-sync/index.js ================================================ /** * Module dependencies. */ const browserSync = require('browser-sync').create(); const { createProxyMiddleware } = require('../../dist'); // require('http-proxy-middleware'); /** * Configure proxy middleware */ const jsonPlaceholderProxy = createProxyMiddleware({ target: 'http://jsonplaceholder.typicode.com', pathFilter: '/users', changeOrigin: true, // for vhosted sites, changes host header to match to target's host logger: console, }); /** * Add the proxy to browser-sync */ browserSync.init({ server: { baseDir: './', port: 3000, middleware: [jsonPlaceholderProxy], }, startPath: '/users', }); console.log('[DEMO] Server: listening on port 3000'); console.log('[DEMO] Opening: http://localhost:3000/users'); process.on('SIGINT', () => browserSync.exit()); process.on('SIGTERM', () => browserSync.exit()); ================================================ FILE: examples/connect/index.js ================================================ /** * Module dependencies. */ const http = require('http'); const connect = require('connect'); const { createProxyMiddleware } = require('../../dist'); // require('http-proxy-middleware'); /** * Configure proxy middleware */ const jsonPlaceholderProxy = createProxyMiddleware({ target: 'http://jsonplaceholder.typicode.com/users', changeOrigin: true, // for vhosted sites, changes host header to match to target's host }); const app = connect(); /** * Add the proxy to connect */ app.use('/users', jsonPlaceholderProxy); const server = http.createServer(app).listen(3000); console.log('[DEMO] Server: listening on port 3000'); console.log('[DEMO] Opening: http://localhost:3000/users'); require('open')('http://localhost:3000/users'); process.on('SIGINT', () => server.close()); process.on('SIGTERM', () => server.close()); ================================================ FILE: examples/express/index.js ================================================ /** * Module dependencies. */ const express = require('express'); const { createProxyMiddleware } = require('../../dist'); // require('http-proxy-middleware'); /** * Configure proxy middleware */ const jsonPlaceholderProxy = createProxyMiddleware({ target: 'http://jsonplaceholder.typicode.com/users', changeOrigin: true, // for vhosted sites, changes host header to match to target's host logger: console, }); const app = express(); /** * Add the proxy to express */ app.use('/users', jsonPlaceholderProxy); const server = app.listen(3000); console.log('[DEMO] Server: listening on port 3000'); console.log('[DEMO] Opening: http://localhost:3000/users'); require('open')('http://localhost:3000/users'); process.on('SIGINT', () => server.close()); process.on('SIGTERM', () => server.close()); ================================================ FILE: examples/fastify/index.js ================================================ const fastify = require('fastify')({ logger: true }); const { createProxyMiddleware } = require('../../dist'); // require('http-proxy-middleware'); (async () => { await fastify.register(require('@fastify/express')); const proxy = createProxyMiddleware({ target: 'http://jsonplaceholder.typicode.com', changeOrigin: true, }); fastify.use(proxy); fastify.listen({ port: 3000 }, (err, address) => { if (err) throw err; fastify.log.info(`server listening on ${address}`); require('open')(`${address}/users`); }); })(); ================================================ FILE: examples/http-server/index.js ================================================ /** * Module dependencies. */ const http = require('node:http'); const { createProxyMiddleware } = require('../../dist'); // require('http-proxy-middleware'); /** * Configure proxy middleware */ const jsonPlaceholderProxy = createProxyMiddleware({ target: 'http://jsonplaceholder.typicode.com', changeOrigin: true, // for vhosted sites, changes host header to match to target's host logger: console, }); /** * Add the proxy to http-server */ const server = http.createServer(jsonPlaceholderProxy); server.listen(3000); console.log('[DEMO] Server: listening on port 3000'); console.log('[DEMO] Opening: http://localhost:3000/users'); require('open')('http://localhost:3000/users'); process.on('SIGINT', () => server.close()); process.on('SIGTERM', () => server.close()); ================================================ FILE: examples/next-app/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js .yarn/install-state.gz # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env*.local # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts ================================================ FILE: examples/next-app/PROXY.md ================================================ # Next.js + http-proxy-proxy See example `pages/api/users.ts` ```shell yarn dev # visit http://localhost:3000/api/users ``` ================================================ FILE: examples/next-app/README.md ================================================ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). ## Getting Started First, run the development server: ```bash npm run dev # or yarn dev # or pnpm dev # or bun dev ``` Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. ## Learn More To learn more about Next.js, take a look at the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! ## Deploy on Vercel The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. ================================================ FILE: examples/next-app/app/globals.css ================================================ :root { --max-width: 1100px; --border-radius: 12px; --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace; --foreground-rgb: 0, 0, 0; --background-start-rgb: 214, 219, 220; --background-end-rgb: 255, 255, 255; --primary-glow: conic-gradient( from 180deg at 50% 50%, #16abff33 0deg, #0885ff33 55deg, #54d6ff33 120deg, #0071ff33 160deg, transparent 360deg ); --secondary-glow: radial-gradient( rgba(255, 255, 255, 1), rgba(255, 255, 255, 0) ); --tile-start-rgb: 239, 245, 249; --tile-end-rgb: 228, 232, 233; --tile-border: conic-gradient( #00000080, #00000040, #00000030, #00000020, #00000010, #00000010, #00000080 ); --callout-rgb: 238, 240, 241; --callout-border-rgb: 172, 175, 176; --card-rgb: 180, 185, 188; --card-border-rgb: 131, 134, 135; } @media (prefers-color-scheme: dark) { :root { --foreground-rgb: 255, 255, 255; --background-start-rgb: 0, 0, 0; --background-end-rgb: 0, 0, 0; --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); --secondary-glow: linear-gradient( to bottom right, rgba(1, 65, 255, 0), rgba(1, 65, 255, 0), rgba(1, 65, 255, 0.3) ); --tile-start-rgb: 2, 13, 46; --tile-end-rgb: 2, 5, 19; --tile-border: conic-gradient( #ffffff80, #ffffff40, #ffffff30, #ffffff20, #ffffff10, #ffffff10, #ffffff80 ); --callout-rgb: 20, 20, 20; --callout-border-rgb: 108, 108, 108; --card-rgb: 100, 100, 100; --card-border-rgb: 200, 200, 200; } } * { box-sizing: border-box; padding: 0; margin: 0; } html, body { max-width: 100vw; overflow-x: hidden; } body { color: rgb(var(--foreground-rgb)); background: linear-gradient( to bottom, transparent, rgb(var(--background-end-rgb)) ) rgb(var(--background-start-rgb)); } a { color: inherit; text-decoration: none; } @media (prefers-color-scheme: dark) { html { color-scheme: dark; } } ================================================ FILE: examples/next-app/app/layout.tsx ================================================ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "Create Next App", description: "Generated by create next app", }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( {children} ); } ================================================ FILE: examples/next-app/app/page.module.css ================================================ .main { display: flex; flex-direction: column; justify-content: space-between; align-items: center; padding: 6rem; min-height: 100vh; } .description { display: inherit; justify-content: inherit; align-items: inherit; font-size: 0.85rem; max-width: var(--max-width); width: 100%; z-index: 2; font-family: var(--font-mono); } .description a { display: flex; justify-content: center; align-items: center; gap: 0.5rem; } .description p { position: relative; margin: 0; padding: 1rem; background-color: rgba(var(--callout-rgb), 0.5); border: 1px solid rgba(var(--callout-border-rgb), 0.3); border-radius: var(--border-radius); } .code { font-weight: 700; font-family: var(--font-mono); } .grid { display: grid; grid-template-columns: repeat(4, minmax(25%, auto)); max-width: 100%; width: var(--max-width); } .card { padding: 1rem 1.2rem; border-radius: var(--border-radius); background: rgba(var(--card-rgb), 0); border: 1px solid rgba(var(--card-border-rgb), 0); transition: background 200ms, border 200ms; } .card span { display: inline-block; transition: transform 200ms; } .card h2 { font-weight: 600; margin-bottom: 0.7rem; } .card p { margin: 0; opacity: 0.6; font-size: 0.9rem; line-height: 1.5; max-width: 30ch; text-wrap: balance; } .center { display: flex; justify-content: center; align-items: center; position: relative; padding: 4rem 0; } .center::before { background: var(--secondary-glow); border-radius: 50%; width: 480px; height: 360px; margin-left: -400px; } .center::after { background: var(--primary-glow); width: 240px; height: 180px; z-index: -1; } .center::before, .center::after { content: ""; left: 50%; position: absolute; filter: blur(45px); transform: translateZ(0); } .logo { position: relative; } /* Enable hover only on non-touch devices */ @media (hover: hover) and (pointer: fine) { .card:hover { background: rgba(var(--card-rgb), 0.1); border: 1px solid rgba(var(--card-border-rgb), 0.15); } .card:hover span { transform: translateX(4px); } } @media (prefers-reduced-motion) { .card:hover span { transform: none; } } /* Mobile */ @media (max-width: 700px) { .content { padding: 4rem; } .grid { grid-template-columns: 1fr; margin-bottom: 120px; max-width: 320px; text-align: center; } .card { padding: 1rem 2.5rem; } .card h2 { margin-bottom: 0.5rem; } .center { padding: 8rem 0 6rem; } .center::before { transform: none; height: 300px; } .description { font-size: 0.8rem; } .description a { padding: 1rem; } .description p, .description div { display: flex; justify-content: center; position: fixed; width: 100%; } .description p { align-items: center; inset: 0 0 auto; padding: 2rem 1rem 1.4rem; border-radius: 0; border: none; border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); background: linear-gradient( to bottom, rgba(var(--background-start-rgb), 1), rgba(var(--callout-rgb), 0.5) ); background-clip: padding-box; backdrop-filter: blur(24px); } .description div { align-items: flex-end; pointer-events: none; inset: auto 0 0; padding: 2rem; height: 200px; background: linear-gradient( to bottom, transparent 0%, rgb(var(--background-end-rgb)) 40% ); z-index: 1; } } /* Tablet and Smaller Desktop */ @media (min-width: 701px) and (max-width: 1120px) { .grid { grid-template-columns: repeat(2, 50%); } } @media (prefers-color-scheme: dark) { .vercelLogo { filter: invert(1); } .logo { filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); } } @keyframes rotate { from { transform: rotate(360deg); } to { transform: rotate(0deg); } } ================================================ FILE: examples/next-app/app/page.tsx ================================================ import Image from "next/image"; import styles from "./page.module.css"; export default function Home() { return (

Get started by editing  app/page.tsx

Next.js Logo
); } ================================================ FILE: examples/next-app/next.config.mjs ================================================ /** @type {import('next').NextConfig} */ const nextConfig = {}; export default nextConfig; ================================================ FILE: examples/next-app/package.json ================================================ { "name": "next-app", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" }, "dependencies": { "react": "^18", "react-dom": "^18", "next": "14.2.35" }, "devDependencies": { "typescript": "^5", "@types/node": "^22", "@types/react": "^18", "@types/react-dom": "^18" } } ================================================ FILE: examples/next-app/pages/api/_proxy.ts ================================================ import type { NextApiRequest, NextApiResponse } from 'next'; import { createProxyMiddleware } from '../../../../dist'; // Singleton // prevent a new proxy being created for every request export const proxyMiddleware = createProxyMiddleware({ target: 'http://jsonplaceholder.typicode.com', changeOrigin: true, pathRewrite: { '^/api/users': '/users', }, logger: console, }); ================================================ FILE: examples/next-app/pages/api/users.ts ================================================ import type { NextApiRequest, NextApiResponse, PageConfig } from 'next'; import { proxyMiddleware } from './_proxy'; // https://nextjs.org/docs/pages/building-your-application/routing/api-routes export default async function handler(req: NextApiRequest, res: NextApiResponse) { return proxyMiddleware(req, res, (result: unknown) => { if (result instanceof Error) { throw result; } }); } export const config: PageConfig = { api: { externalResolver: true, // Uncomment to fix stalled POST requests // https://github.com/chimurai/http-proxy-middleware/issues/795#issuecomment-1314464432 // bodyParser: false, }, }; ================================================ FILE: examples/next-app/tsconfig.json ================================================ { "compilerOptions": { "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "plugins": [ { "name": "next" } ], "paths": { "@/*": ["./*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } ================================================ FILE: examples/package.json ================================================ { "name": "examples", "version": "1.0.0", "private": "true", "description": "View working examples of `http-proxy-middleware` implemented in popular servers.", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "chimurai", "license": "MIT", "devDependencies": { "@fastify/express": "4.0.4", "browser-sync": "3.0.4", "connect": "3.7.0", "express": "5.2.1", "fastify": "5.7.4" } } ================================================ FILE: examples/response-interceptor/index.js ================================================ // file deepcode ignore DisablePoweredBy: example code // file deepcode ignore UseCsurfForExpress: example code /** * Module dependencies. */ const express = require('express'); const { createProxyMiddleware, responseInterceptor } = require('../../dist'); // require('http-proxy-middleware'); // test with double-byte characters // cSpell:ignore Kroket, ส้มตำไทย, चिकन const favoriteFoods = [ { country: 'NL', food: 'Kroket', }, { country: 'HK', food: '叉燒包', }, { country: 'US', food: 'Hamburger', }, { country: 'TH', food: 'ส้มตำไทย', }, { country: 'IN', food: 'बटर चिकन', }, ]; /** * Configure proxy middleware */ const jsonPlaceholderProxy = createProxyMiddleware({ target: 'http://jsonplaceholder.typicode.com', router: { '/users': 'http://jsonplaceholder.typicode.com', '/brotli': 'http://httpbin.org', '/gzip': 'http://httpbin.org', '/deflate': 'http://httpbin.org', }, changeOrigin: true, // for vhosted sites, changes host header to match to target's host selfHandleResponse: true, // manually call res.end(); IMPORTANT: res.end() is called internally by responseInterceptor() on: { proxyRes: responseInterceptor(async (buffer, proxyRes, req, res) => { // log original response // console.log(`[DEBUG] original response:\n${buffer.toString('utf8')}`); console.log('change response content-type'); res.setHeader('content-type', 'application/json; charset=utf-8'); console.log('change response status code'); res.statusCode = 418; console.log('return a complete different response'); return JSON.stringify(favoriteFoods); }), }, logger: console, }); const app = express(); /** * Add the proxy to express */ app.use(jsonPlaceholderProxy); const server = app.listen(3000); console.log('[DEMO] Server: listening on port 3000'); console.log('[DEMO] Open: http://localhost:3000/users'); console.log('[DEMO] Open: http://localhost:3000/brotli'); console.log('[DEMO] Open: http://localhost:3000/gzip'); console.log('[DEMO] Open: http://localhost:3000/deflate'); require('open')('http://localhost:3000/users'); process.on('SIGINT', () => server.close()); process.on('SIGTERM', () => server.close()); ================================================ FILE: examples/websocket/index.html ================================================ http-proxy-middleware - WebSocket example

WebSocket demo

Proxy ws://localhost:3000ws://ws.ifelse.io

================================================ FILE: examples/websocket/index.js ================================================ /** * Module dependencies. */ const express = require('express'); const { createProxyMiddleware } = require('../../dist'); // require('http-proxy-middleware'); /** * Configure proxy middleware */ const wsProxy = createProxyMiddleware({ target: 'http://ws.ifelse.io', // pathRewrite: { // '^/websocket' : '/socket', // rewrite path. // '^/removepath' : '' // remove path. // }, changeOrigin: true, // for vhosted sites, changes host header to match to target's host ws: true, // enable websocket proxy logger: console, }); const app = express(); app.use('/', express.static(__dirname)); // demo page app.use(wsProxy); // add the proxy to express const server = app.listen(3000); server.on('upgrade', wsProxy.upgrade); // optional: upgrade externally console.log('[DEMO] Server: listening on port 3000'); console.log('[DEMO] Opening: http://localhost:3000'); require('open')('http://localhost:3000'); /** * Example: * Open http://localhost:3000 in WebSocket compatible browser. * In browser console: * 1. `const socket = new WebSocket('ws://localhost:3000');` // create new WebSocket * 2. `socket.onmessage = function (msg) {console.log(msg)};` // listen to socket messages * 3. `socket.send('hello world');` // send message * > {data: "hello world"} // server should echo back your message. **/ ================================================ FILE: jest.config.js ================================================ /** @type {import('jest').Config} */ module.exports = { preset: 'ts-jest', testEnvironment: 'node', coverageReporters: ['text', 'lcov'], collectCoverageFrom: ['src/**/*.*'], setupFilesAfterEnv: ['/jest.setup.js'], }; ================================================ FILE: jest.setup.js ================================================ jest.retryTimes(3); /** * Uncomment the following lines for less noise in test output */ // console.info = jest.fn(); // console.log = jest.fn(); // console.error = jest.fn(); ================================================ FILE: package.json ================================================ { "name": "http-proxy-middleware", "type": "commonjs", "version": "3.0.5", "description": "The one-liner node.js proxy middleware for connect, express, next.js and more", "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ "dist" ], "scripts": { "clean": "rm -rf dist coverage tsconfig.tsbuildinfo .eslintcache", "install:all": "yarn && (cd examples && yarn)", "lint": "yarn prettier && yarn eslint", "lint:fix": "yarn prettier:fix && yarn eslint:fix", "eslint": "eslint '{src,test,examples}/**/*.{js,ts}' --cache", "eslint:fix": "yarn eslint --fix", "prettier": "prettier --list-different \"**/*.{js,ts,md,yml,json,html}\"", "prettier:fix": "prettier --write \"**/*.{js,ts,md,yml,json,html}\"", "build": "tsc --build", "test": "jest", "coverage": "jest --coverage", "prepare": "husky && patch-package", "prepack": "yarn clean && yarn test && yarn build", "spellcheck": "npx --yes cspell --show-context --show-suggestions '**/*.*'" }, "publishConfig": { "provenance": true }, "repository": { "type": "git", "url": "git+https://github.com/chimurai/http-proxy-middleware.git" }, "keywords": [ "reverse", "proxy", "middleware", "http", "https", "connect", "express", "fastify", "polka", "next.js", "browser-sync", "gulp", "grunt-contrib-connect", "websocket", "ws", "cors" ], "author": "Steven Chim", "license": "MIT", "bugs": { "url": "https://github.com/chimurai/http-proxy-middleware/issues" }, "homepage": "https://github.com/chimurai/http-proxy-middleware#readme", "devDependencies": { "@commitlint/cli": "20.4.2", "@commitlint/config-conventional": "20.4.2", "@eslint/js": "10.0.1", "@trivago/prettier-plugin-sort-imports": "6.0.2", "@types/debug": "4.1.12", "@types/eslint": "9.6.1", "@types/express": "5.0.6", "@types/is-glob": "4.0.4", "@types/jest": "30.0.0", "@types/micromatch": "4.0.10", "@types/node": "24.10.2", "@types/supertest": "7.2.0", "@types/ws": "8.18.1", "body-parser": "2.2.2", "eslint": "10.0.2", "express": "5.2.1", "get-port": "5.1.1", "globals": "17.3.0", "husky": "9.1.7", "jest": "30.2.0", "lint-staged": "16.3.0", "mockttp": "4.2.1", "open": "8.4.2", "patch-package": "8.0.1", "pkg-pr-new": "0.0.65", "prettier": "3.8.1", "supertest": "7.2.2", "ts-jest": "29.4.6", "typescript": "5.9.3", "typescript-eslint": "8.56.1", "ws": "8.19.0" }, "dependencies": { "@types/http-proxy": "^1.17.15", "debug": "^4.3.6", "http-proxy": "^1.18.1", "is-glob": "^4.0.3", "is-plain-object": "^5.0.0", "micromatch": "^4.0.8" }, "resolutions": { "patch-package/**/tmp": "^0.2.4" }, "engines": { "node": "^14.18.0 || ^16.10.0 || >=18.0.0" }, "commitlint": { "extends": [ "@commitlint/config-conventional" ] } } ================================================ FILE: patches/http-proxy+1.18.1.patch ================================================ diff --git a/node_modules/http-proxy/lib/http-proxy/common.js b/node_modules/http-proxy/lib/http-proxy/common.js index 6513e81..d01d8db 100644 --- a/node_modules/http-proxy/lib/http-proxy/common.js +++ b/node_modules/http-proxy/lib/http-proxy/common.js @@ -1,6 +1,6 @@ var common = exports, url = require('url'), - extend = require('util')._extend, + extend = Object.assign, required = require('requires-port'); var upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i, diff --git a/node_modules/http-proxy/lib/http-proxy/index.js b/node_modules/http-proxy/lib/http-proxy/index.js index 977a4b3..739409c 100644 --- a/node_modules/http-proxy/lib/http-proxy/index.js +++ b/node_modules/http-proxy/lib/http-proxy/index.js @@ -1,5 +1,5 @@ var httpProxy = module.exports, - extend = require('util')._extend, + extend = Object.assign, parse_url = require('url').parse, EE3 = require('eventemitter3'), http = require('http'), ================================================ FILE: patches/tr46+0.0.3.patch ================================================ diff --git a/node_modules/tr46/index.js b/node_modules/tr46/index.js index 9ce12ca..7c3b5d7 100644 --- a/node_modules/tr46/index.js +++ b/node_modules/tr46/index.js @@ -1,6 +1,6 @@ "use strict"; -var punycode = require("punycode"); +var punycode = require("punycode/"); var mappingTable = require("./lib/mappingTable.json"); var PROCESSING_OPTIONS = { ================================================ FILE: patches/uri-js+4.4.1.patch ================================================ diff --git a/node_modules/uri-js/dist/esnext/schemes/mailto.js b/node_modules/uri-js/dist/esnext/schemes/mailto.js index 2553713..df0ecfd 100755 --- a/node_modules/uri-js/dist/esnext/schemes/mailto.js +++ b/node_modules/uri-js/dist/esnext/schemes/mailto.js @@ -1,5 +1,5 @@ import { pctEncChar, pctDecChars, unescapeComponent } from "../uri"; -import punycode from "punycode"; +import punycode from "punycode/"; import { merge, subexp, toUpperCase, toArray } from "../util"; const O = {}; const isIRI = true; diff --git a/node_modules/uri-js/dist/esnext/uri.js b/node_modules/uri-js/dist/esnext/uri.js index 659ce26..1806aa5 100755 --- a/node_modules/uri-js/dist/esnext/uri.js +++ b/node_modules/uri-js/dist/esnext/uri.js @@ -34,7 +34,7 @@ */ import URI_PROTOCOL from "./regexps-uri"; import IRI_PROTOCOL from "./regexps-iri"; -import punycode from "punycode"; +import punycode from "punycode/"; import { toUpperCase, typeOf, assign } from "./util"; export const SCHEMES = {}; export function pctEncChar(chr) { ================================================ FILE: patches/whatwg-url+5.0.0.patch ================================================ diff --git a/node_modules/whatwg-url/lib/url-state-machine.js b/node_modules/whatwg-url/lib/url-state-machine.js index c25dbc2..8c2d955 100644 --- a/node_modules/whatwg-url/lib/url-state-machine.js +++ b/node_modules/whatwg-url/lib/url-state-machine.js @@ -1,5 +1,5 @@ "use strict"; -const punycode = require("punycode"); +const punycode = require("punycode/"); const tr46 = require("tr46"); const specialSchemes = { ================================================ FILE: recipes/README.md ================================================ # Recipes Common usages of `http-proxy-middleware`. ================================================ FILE: recipes/async-response.md ================================================ # Async proxied response Sometimes we need the ability to modify the response headers of the response of the proxied backend before sending it. For achieving it just make sure you have selfHandleResponse to true and add a pipe in the proxyRes: ```javascript const myProxy = createProxyMiddleware({ target: 'http://www.example.com/api', changeOrigin: true, selfHandleResponse: true, on: { proxyReq: (proxyReq, req, res) => { // before proxyReq.setHeader('mpth-1', 'da'); }, proxyRes: async (proxyRes, req, res) => { const da = await new Promise((resolve, reject) => { setTimeout(() => { resolve({ wei: 'wei' }); }, 200); }); // add your dynamic header res.setHeader('mpth-2', da.wei); // now pipe the response proxyRes.pipe(res); }, }, }); app.use('/api', myProxy); ``` There are also cases where you need to modify the request header async, we can achieve this by applying middleware in front of the proxy. Like: ```javascript const entryMiddleware = async (req, res, next) => { const foo = await new Promise((resolve, reject) => { setTimeout(() => { resolve({ da: 'da' }); }, 200); }); req.locals = { da: foo.da, }; next(); }; const myProxy = createProxyMiddleware({ target: 'http://www.example.com/api', changeOrigin: true, selfHandleResponse: true, on: { proxyReq: (proxyReq, req, res) => { // before // get something async from entry middleware before the proxy kicks in console.log('proxyReq:', req.locals.da); proxyReq.setHeader('mpth-1', req.locals.da); }, proxyRes: async (proxyRes, req, res) => { const da = await new Promise((resolve, reject) => { setTimeout(() => { resolve({ wei: 'wei' }); }, 200); }); // end: res.setHeader('mpth-2', da.wei); proxyRes.pipe(res); }, }, }); app.use('/api', entryMiddleware, myProxy); ``` _working sample available at: [codesandbox.io/s/holy-resonance-yz552](https://codesandbox.io/s/holy-resonance-yz552?file=/src/index.js) Server Control Panel: restart server, see logging_ ================================================ FILE: recipes/basic.md ================================================ # Basic usage This example will create a basic proxy middleware. ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const apiProxy = createProxyMiddleware({ target: 'http://localhost:3000', changeOrigin: true, }); ``` ## Alternative configuration ```javascript app.use('/api', createProxyMiddleware({ target: 'http://localhost:3000/api', changeOrigin: true })); ``` ```javascript app.use( createProxyMiddleware({ target: 'http://localhost:3000', changeOrigin: true, pathFilter: '/api', }), ); ``` ================================================ FILE: recipes/context-matching.md ================================================ # [BREAKING CHANGE] This functionality is removed in v3. The old "context matching" function has been moved to the [pathFilter](pathFilter.md) configuration property. TL;DR ```js // v2 createProxyMiddleware('/api', { target: 'http://localhost:3000', }); // v3 createProxyMiddleware({ target: 'http://localhost:3000', pathFilter: '/api', }); ``` ================================================ FILE: recipes/corporate-proxy.md ================================================ # Corporate Proxy Support This example will create a basic proxy middleware with corporate proxy support. Provide a custom `http.agent` with [https-proxy-agent](https://github.com/TooTallNate/node-https-proxy-agent) to connect to the corporate proxy server. ```javascript const HttpsProxyAgent = require('https-proxy-agent'); const { createProxyMiddleware } = require('http-proxy-middleware'); // corporate proxy to connect to const proxyServer = process.env.HTTPS_PROXY || process.env.HTTP_PROXY; const options = { target: 'http://localhost:3000', agent: new HttpsProxyAgent(proxyServer), }; const apiProxy = createProxyMiddleware(options); ``` ================================================ FILE: recipes/delay.md ================================================ # Delay proxied request/response Sometimes we need the ability to delay request to backend server or response from it back to client to test long execution time of particular url or all of them. With DevTool's [network throttling](https://developers.google.com/web/tools/chrome-devtools/profile/network-performance/network-conditions?hl=en) we can test slowdown of all request, not separately. But we can handle each request individually via our proxy, and add delay to its execution time. Let's assume that we want slow down the access to backend's `/api/get-me-something` resource. Delay request time by 2 seconds and increase response time by 5 seconds. For achieving it just put additional route handler to your app before proxy handler: ```javascript const myProxy = createProxyMiddleware({ target: 'http://www.example.com', changeOrigin: true, }); const proxyDelay = function (req, res, next) { if (req.originalUrl === '/api/get-me-something') { // Delay request by 2 seconds setTimeout(next, 2000); // Delay response completion by 5 seconds const endOriginal = res.end; res.end = function (...args) { setTimeout(function () { endOriginal.apply(res, args); }, 5000); }; } else { next(); } }; app.use('/api', proxyDelay, myProxy); ``` And you will see result in devtools similar to this: ![http-proxy-delay](https://cloud.githubusercontent.com/assets/576077/15839924/49ebe256-2bfb-11e6-8591-ef0101670885.png) ================================================ FILE: recipes/https.md ================================================ # HTTPS How to proxy requests to various types of HTTPS servers. All options are provided by [http-proxy](https://github.com/nodejitsu/node-http-proxy). ## Basic proxy to an HTTPS server ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const apiProxy = createProxyMiddleware({ target: 'https://example.org', changeOrigin: true, }); ``` ## Proxy to an HTTPS server using a PKCS12 client certificate ```javascript const fs = require('fs'); const { createProxyMiddleware } = require('http-proxy-middleware'); const apiProxy = createProxyMiddleware({ target: { protocol: 'https:', host: 'example.org', port: 443, pfx: fs.readFileSync('path/to/certificate.p12'), passphrase: 'password', }, changeOrigin: true, }); ``` ================================================ FILE: recipes/logLevel.md ================================================ # [BREAKING CHANGE] This functionality is removed in v3. See [logger.md](logger.md) for logging in v3. ================================================ FILE: recipes/logProvider.md ================================================ # [BREAKING CHANGE] This functionality is removed in v3. See [logger.md](logger.md) for logging in v3. ================================================ FILE: recipes/logger.md ================================================ # Logger Configure a logger to output information from http-proxy-middleware: ie. `console`, `winston`, `pino`, `bunyan`, `log4js`, etc... ## `console` ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const proxy = createProxyMiddleware({ target: 'http://localhost:3000', logger: console, }); ``` ## `winston` ![GitHub Repo stars](https://img.shields.io/github/stars/winstonjs/winston?style=social) ![winston downloads](https://img.shields.io/npm/dm/winston) ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const winston = require('winston'); const { format, transports } = require('winston'); // Enable interpolation in log messages // https://github.com/winstonjs/winston#string-interpolation const logger = winston.createLogger({ format: format.combine(format.splat(), format.simple()), transports: [new transports.Console()], }); const proxy = createProxyMiddleware({ target: 'http://localhost:3000', logger, }); ``` ## `pino` ![GitHub Repo stars](https://img.shields.io/github/stars/pinojs/pino?style=social) ![winston downloads](https://img.shields.io/npm/dm/pino) ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const pino = require('pino'); const logger = pino(); const proxy = createProxyMiddleware({ target: 'http://localhost:3000', logger, }); ``` ## `log4js` ![GitHub Repo stars](https://img.shields.io/github/stars/log4js-node/log4js-node?style=social) ![winston downloads](https://img.shields.io/npm/dm/log4js) ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const log4js = require('log4js'); const logger = log4js.getLogger(); logger.level = 'debug'; const proxy = createProxyMiddleware({ target: 'http://localhost:3000', logger, }); ``` ## `bunyan` ![GitHub Repo stars](https://img.shields.io/github/stars/trentm/node-bunyan?style=social) ![winston downloads](https://img.shields.io/npm/dm/bunyan) ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const bunyan = require('bunyan'); const logger = bunyan.createLogger({ name: 'my-app', }); const proxy = createProxyMiddleware({ target: 'http://localhost:3000', logger, }); ``` ================================================ FILE: recipes/modify-post.md ================================================ ## Modify Post Parameters: The code example below illustrates how to modify POST body data prior to forwarding to the proxy target. Key to this example is the _"OnProxyReq"_ event handler that creates a new POST body that can be manipulated to format the POST data as required. For example: inject new POST parameters that should only be visible server side. This example uses the _"body-parser"_ module in the main app to create a req.body object with the decoded POST parameters. Side note - the code below will allow _"http-proxy-middleware"_ to work with _"body-parser"_. Since this only modifies the request body stream the original POST body parameters remain in tact, so any POST data changes will not be sent back in the response to the client. ## Example: ```js 'use strict'; const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const router = express.Router(); const proxy_filter = function (path, req) { return path.match('^/docs') && (req.method === 'GET' || req.method === 'POST'); }; const proxy_options = { target: 'http://localhost:8080', pathRewrite: { '^/docs': '/java/rep/server1', // Host path & target path conversion }, on: { error(err, req, res) { res.writeHead(500, { 'Content-Type': 'text/plain', }); res.end('Something went wrong. And we are reporting a custom error message.' + err); }, proxyReq(proxyReq, req, res) { if (req.method == 'POST' && req.body) { // Add req.body logic here if needed.... // .... // Remove body-parser body object from the request if (req.body) delete req.body; // Make any needed POST parameter changes let body = new Object(); body.filename = 'reports/statistics/summary_2016.pdf'; body.routeId = 's003b012d002'; body.authId = 'bac02c1d-258a-4177-9da6-862580154960'; // URI encode JSON object body = Object.keys(body) .map(function (key) { return encodeURIComponent(key) + '=' + encodeURIComponent(body[key]); }) .join('&'); // Update header proxyReq.setHeader('content-type', 'application/x-www-form-urlencoded'); proxyReq.setHeader('content-length', body.length); // Write out body changes to the proxyReq stream proxyReq.write(body); proxyReq.end(); } }, }, }; // Proxy configuration const proxy = createProxyMiddleware(proxy_filter, proxy_options); /* GET home page. */ router.get('/', function (req, res, next) { res.render('index', { title: 'Node.js Express Proxy Test' }); }); router.all('/docs', proxy); module.exports = router; ``` ================================================ FILE: recipes/pathFilter.md ================================================ # Path Filter Narrow down which requests should be proxied. The `path` used for filtering is the `request.url` pathname. In Express, this is the `path` relative to the mount-point of the proxy. `pathFilter` is optional and is useful in cases where you are not able to use the regular [middleware mounting](http://expressjs.com/en/4x/api.html#app.use). `http-proxy-middleware` offers several ways to do this: - [Path](#path) - [Multi Path](#multi-path) - [Wildcard](#wildcard) - [Multi Wildcard](#multi-wildcard) - [Wildcard / Exclusion](#wildcard--exclusion) - [Custom filtering](#custom-filtering) ## Path This will match paths starting with `/api` ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const apiProxy = createProxyMiddleware({ target: 'http://localhost:3000', pathFilter: '/api', }); // `/api/foo/bar` -> `http://localhost:3000/api/foo/bar` ``` ## Multi Path This will match paths starting with `/api` or `/rest` ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const apiProxy = createProxyMiddleware({ target: 'http://localhost:3000', pathFilter: ['/api', '/rest'], }); // `/api/foo/bar` -> `http://localhost:3000/api/foo/bar` // `/rest/lorum/ipsum` -> `http://localhost:3000/rest/lorum/ipsum` ``` ## Wildcard This will match paths starting with `/api/` and should also end with `.json` ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const apiProxy = createProxyMiddleware({ target: 'http://localhost:3000', pathFilter: '/api/**/*.json', }); ``` ## Multi Wildcard Multiple wildcards can be used. ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const apiProxy = createProxyMiddleware({ target: 'http://localhost:3000', pathFilter: ['/api/**/*.json', '/rest/**'], }); ``` ## Wildcard / Exclusion This example will create a proxy with globs. ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const apiProxy = createProxyMiddleware({ target: 'http://localhost:3000', pathFilter: ['foo/*.js', '!bar.js'], }); ``` ## Custom filtering Write your custom `pathFilter` function to have full control on the matching behavior. The request `pathname` and `req` object are provided to determine which requests should be proxied or not. ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const pathFilter = function (pathname, req) { return pathname.match('^/api') && req.method === 'GET'; }; const apiProxy = createProxyMiddleware({ pathFilter: pathFilter, target: 'http://localhost:3000', }); ``` ================================================ FILE: recipes/pathRewrite.md ================================================ # pathRewrite Modify request paths before requests are send to the target. - [rewrite paths](#rewrite-paths) - [remove paths](#remove-paths) - [add paths](#add-paths) - [custom rewrite function](#custom-rewrite-function) ## rewrite paths Rewrite paths ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const options = { target: 'http://localhost:3000', pathRewrite: { '^/api/old-path': '/api/new-path', // rewrite path }, }; const apiProxy = createProxyMiddleware(options); // `/api/old-path/foo/bar` -> `http://localhost:3000/api/new-path/foo/bar` ``` ## remove paths Remove base path ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const options = { target: 'http://localhost:3000', pathRewrite: { '^/api/': '/', // remove base path }, }; const apiProxy = createProxyMiddleware(options); // `/api/lorum/ipsum` -> `http://localhost:3000/lorum/ipsum` ``` ## add paths Add base path ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const options = { target: 'http://localhost:3000', pathRewrite: { '^/': '/extra/', // add base path }, }; const apiProxy = createProxyMiddleware(options); // `/api/lorum/ipsum` -> `http://localhost:3000/extra/api/lorum/ipsum` ``` ## custom rewrite function Implement you own path rewrite logic. The unmodified path will be used, when rewrite function returns `undefined` ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const rewriteFn = function (path, req) { return path.replace('/api/foo', '/api/bar'); }; const options = { target: 'http://localhost:3000', pathRewrite: rewriteFn, }; const apiProxy = createProxyMiddleware(options); // `/api/foo/lorum/ipsum` -> `http://localhost:3000/api/bar/lorum/ipsum` ``` ================================================ FILE: recipes/proxy-events.md ================================================ # Proxy Events Subscribe to `http-proxy` events: `error`, `proxyReq`, `proxyReqWs`, `proxyRes`, `open`, `close`, `start`, `end`, `econnreset`. ## on.error Subscribe to http-proxy's [error event](https://www.npmjs.com/package/http-proxy#listening-for-proxy-events). ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const onError = function (err, req, res) { console.log('Something went wrong.'); console.log('And we are reporting a custom error message.'); }; const options = { target: 'http://localhost:3000', on: { 'error', onError } }; const apiProxy = createProxyMiddleware(options); ``` ## on.proxyReq Subscribe to http-proxy's [proxyReq event](https://www.npmjs.com/package/http-proxy#listening-for-proxy-events). ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const onProxyReq = function (proxyReq, req, res) { // add new header to request proxyReq.setHeader('x-added', 'foobar'); }; const options = { target: 'http://localhost:3000', on: { proxyReq: onProxyReq }, }; const apiProxy = createProxyMiddleware(options); ``` ## on.proxyReqWs Subscribe to http-proxy's [proxyReqWs event](https://www.npmjs.com/package/http-proxy#listening-for-proxy-events). ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const onProxyReqWs = function (proxyReq, req, socket, options, head) { // add custom header proxyReq.setHeader('X-Special-Proxy-Header', 'foobar'); }; const options = { target: 'http://localhost:3000', on: { proxyReqWs: onProxyReqWs }, }; const apiProxy = createProxyMiddleware(options); ``` ## on.proxyRes Subscribe to http-proxy's [proxyRes event](https://www.npmjs.com/package/http-proxy#listening-for-proxy-events). ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const onProxyRes = function (proxyRes, req, res) { // add new header to response proxyRes.headers['x-added'] = 'foobar'; // remove header from response delete proxyRes.headers['x-removed']; }; const options = { target: 'http://localhost:3000', on: { proxyRes: onProxyRes }, }; const apiProxy = createProxyMiddleware(options); ``` ================================================ FILE: recipes/response-interceptor.md ================================================ # Response Interceptor Intercept responses from upstream with `responseInterceptor`. (Make sure to set `selfHandleResponse: true`) Responses which are compressed with `brotli`, `gzip` and `deflate` will be decompressed automatically. Response will be made available as [`buffer`](https://nodejs.org/api/buffer.html) which you can manipulate. ## Replace text and change http status code ```js const { createProxyMiddleware, responseInterceptor } = require('http-proxy-middleware'); const proxy = createProxyMiddleware({ target: 'http://www.example.com', changeOrigin: true, // for vhosted sites /** * IMPORTANT: avoid res.end being called automatically **/ selfHandleResponse: true, // res.end() will be called internally by responseInterceptor() /** * Intercept response and replace 'Hello' with 'Teapot' with 418 http response status code **/ on: { proxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => { res.statusCode = 418; // set different response status code const response = responseBuffer.toString('utf8'); return response.replaceAll('Example', 'Teapot'); }), }, }); ``` ## Log request and response ```javascript const proxy = createProxyMiddleware({ target: 'http://www.example.com', changeOrigin: true, // for vhosted sites selfHandleResponse: true, // res.end() will be called internally by responseInterceptor() on: { proxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => { // log original request and proxied request info const exchange = `[DEBUG] ${req.method} ${req.path} -> ${proxyRes.req.protocol}//${proxyRes.req.host}${proxyRes.req.path} [${proxyRes.statusCode}]`; console.log(exchange); // [DEBUG] GET / -> http://www.example.com [200] // log complete response const response = responseBuffer.toString('utf8'); console.log(response); // log response body return responseBuffer; }), }, }); ``` ## Manipulate JSON responses (application/json) ```javascript const proxy = createProxyMiddleware({ target: 'http://jsonplaceholder.typicode.com', changeOrigin: true, // for vhosted sites selfHandleResponse: true, // res.end() will be called internally by responseInterceptor() on: { proxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => { // detect json responses if (proxyRes.headers['content-type'] === 'application/json') { let data = JSON.parse(responseBuffer.toString('utf8')); // manipulate JSON data here data = Object.assign({}, data, { extra: 'foo bar' }); // return manipulated JSON return JSON.stringify(data); } // return other content-types as-is return responseBuffer; }), }, }); ``` ## Manipulate image response Example [Lenna](https://en.wikipedia.org/wiki/Lenna) image: Proxy and manipulate image (flip, sepia, pixelate). [![Image of Lenna](../.github/docs/response-interceptor-lenna.png)](https://codesandbox.io/s/trusting-engelbart-03rjl) Check [source code](https://codesandbox.io/s/trusting-engelbart-03rjl) on codesandbox. Some working examples on /[relative wikimedia image path]: - Lenna - ([manipulated](https://03rjl.sse.codesandbox.io/wikipedia/en/7/7d/Lenna_%28test_image%29.png)) ([original](https://upload.wikimedia.org/wikipedia/en/7/7d/Lenna_%28test_image%29.png)). - Starry Night - ([manipulated](https://03rjl.sse.codesandbox.io/wikipedia/commons/thumb/e/ea/Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg/1024px-Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg)) ([original](https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg/1024px-Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg)). - Mona Lisa - ([manipulated](https://03rjl.sse.codesandbox.io/wikipedia/commons/thumb/e/ec/Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg/800px-Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg)) ([original](https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg/800px-Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg)). _You can just use any relative image path from and use the relative image path on to see the manipulated image._ ```javascript const Jimp = require('jimp'); // use jimp library for image manipulation const proxy = createProxyMiddleware({ target: 'https://upload.wikimedia.org', changeOrigin: true, // for vhosted sites selfHandleResponse: true, // res.end() will be called internally by responseInterceptor() on: { proxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => { const imageTypes = ['image/png', 'image/jpg', 'image/jpeg', 'image/gif']; // detect image responses if (imageTypes.includes(proxyRes.headers['content-type'])) { try { const image = await Jimp.read(responseBuffer); image.flip(true, false).sepia().pixelate(5); return image.getBufferAsync(Jimp.AUTO); } catch (err) { console.log('image processing error: ', err); return responseBuffer; } } return responseBuffer; // return other content-types as-is }), }, }); // http://localhost:3000/wikipedia/en/7/7d/Lenna\_%28test_image%29.png ``` ## Manipulate response headers ```js const { createProxyMiddleware, responseInterceptor } = require('http-proxy-middleware'); const proxy = createProxyMiddleware({ target: 'http://www.example.com', changeOrigin: true, // for vhosted sites /** * IMPORTANT: avoid res.end being called automatically **/ selfHandleResponse: true, // res.end() will be called internally by responseInterceptor() /** * Intercept response and remove the **/ on: { proxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => { res.removeHeader('content-security-policy'); // Remove the Content Security Policy header res.setHeader('HPM-Header', 'Intercepted by HPM'); // Set a new header and value return responseBuffer; }), }, }); ``` ================================================ FILE: recipes/router.md ================================================ # router Allows you to route to a different `target` by using a table of a custom function. - [Custom router function](#custom-router-function) - [Proxy Table](#proxy-table) - [Example](#example) ## Custom router function Write your own router to dynamically route to a different `target`. The `req` object is provided to retrieve contextual data. ```javascript const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const customRouter = function (req) { return 'http://www.example.org'; // protocol + host }; const options = { target: 'http://localhost:8000', router: customRouter, }; const myProxy = createProxyMiddleware(options); const app = express(); app.use(myProxy); // add the proxy to express app.listen(3000); ``` ## Proxy Table Use a Proxy Table to proxy requests to a different `target` based on: - Host [HTTP header](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields). - Request path - Host HTTP header + path ```javascript const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const proxyTable = { 'integration.localhost:3000': 'http://localhost:8001', // host only 'staging.localhost:3000': 'http://localhost:8002', // host only 'localhost:3000/api': 'http://localhost:8003', // host + path '/rest': 'http://localhost:8004', // path only }; const options = { target: 'http://localhost:8000', router: proxyTable, }; const myProxy = createProxyMiddleware(options); const app = express(); app.use(myProxy); // add the proxy to express app.listen(3000); ``` ### Example In the example above; all requests will be proxied to `http://localhost:8000`. When request's `Host HTTP header` and/or `path` match a configuration in the proxyTable, they will be send to matching target. ```text http://localhost:3000/lorum/ipsum -> http://localhost:8000/lorum/ipsum http://integration.localhost:3000/lorum/ipsum -> http://localhost:8001/lorum/ipsum http://staging.localhost:3000/rest/foo/bar -> http://localhost:8002/rest/foo/bar http://localhost:3000/api/houses/123 -> http://localhost:8003/api/houses/123 http://localhost:3000/rest/books/123 -> http://localhost:8004/rest/books/123 ``` ================================================ FILE: recipes/servers.md ================================================ # Servers Overview of `http-proxy-middleware` implementation in different servers. Missing a server? Feel free to extend this list of examples. - [http.createServer](#httpcreateserver) - [Express](#express) - [Connect](#connect) - [Next.js](#nextjs) - [fastify](#fastify) - [Browser-Sync](#browser-sync) - [Polka](#polka) - [lite-server](#lite-server) - [grunt-contrib-connect](#grunt-contrib-connect) - [gulp-connect](#gulp-connect) - [grunt-browser-sync](#grunt-browser-sync) - [gulp-webserver](#gulp-webserver) ## http.createServer Vanilla http server implementation with [`http.createServer`](https://nodejs.org/docs/latest/api/http.html#httpcreateserveroptions-requestlistener) ```javascript const http = require('node:http'); const { createProxyMiddleware } = require('http-proxy-middleware'); /** * Configure proxy middleware */ const apiProxy = createProxyMiddleware({ target: 'http://www.example.com', changeOrigin: true, // for vhosted sites, changes host header to match to target's host }); const server = http.createServer(apiProxy); server.listen(3000); ``` ## Express https://github.com/expressjs/express [![GitHub stars](https://img.shields.io/github/stars/expressjs/express.svg?style=social&label=Star)](https://github.com/expressjs/express) ![express downloads](https://img.shields.io/npm/dm/express) ```javascript const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const apiProxy = createProxyMiddleware({ target: 'http://www.example.org/api', changeOrigin: true, // for vhosted sites }); const app = express(); app.use('/api', apiProxy); app.listen(3000); ``` ## Connect https://github.com/senchalabs/connect [![GitHub stars](https://img.shields.io/github/stars/senchalabs/connect.svg?style=social&label=Star)](https://github.com/senchalabs/connect) ![connect downloads](https://img.shields.io/npm/dm/connect) ```javascript const http = require('http'); const connect = require('connect'); const { createProxyMiddleware } = require('http-proxy-middleware'); const apiProxy = createProxyMiddleware({ target: 'http://www.example.org/api', changeOrigin: true, // for vhosted sites }); const app = connect(); app.use('/api', 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) See working Next.js example in [/examples/next-app/pages/api/users.ts](https://github.com/chimurai/http-proxy-middleware/blob/master/examples/next-app/pages/api/users.ts) ```typescript // Next project: `/pages/api/users.proxy.ts` import { createProxyMiddleware } from 'http-proxy-middleware'; // singleton export const proxyMiddleware = createProxyMiddleware({ target: 'http://jsonplaceholder.typicode.com', changeOrigin: true, pathRewrite: { '^/api/users': '/users', }, }); ``` ```typescript // Next project: `/pages/api/users.ts` // Next.js API route support: https://nextjs.org/docs/api-routes/introduction import type { NextApiRequest, NextApiResponse } from 'next'; import { proxyMiddleware } from './users.proxy'; export default function handler(req: NextApiRequest, res: NextApiResponse) { proxyMiddleware(req, res, (result: unknown) => { if (result instanceof Error) { throw result; } }); } export const config = { api: { externalResolver: true, // Uncomment to fix stalled POST requests // https://github.com/chimurai/http-proxy-middleware/issues/795#issuecomment-1314464432 // bodyParser: false, }, }; // curl http://localhost:3000/api/users ``` ## fastify [![GitHub stars](https://img.shields.io/github/stars/fastify/fastify.svg?style=social&label=Star)](https://github.com/fastify/fastify) ![fastify downloads](https://img.shields.io/npm/dm/fastify) See working example in [/examples/fastify/index.js](https://github.com/chimurai/http-proxy-middleware/blob/master/examples/fastify/index.js) ```javascript const fastify = require('fastify')({ logger: true }); const { createProxyMiddleware } = require('http-proxy-middleware'); (async () => { await fastify.register(require('@fastify/express')); const proxy = createProxyMiddleware({ target: 'http://jsonplaceholder.typicode.com', changeOrigin: true, }); fastify.use(proxy); fastify.listen({ port: 3000 }, (err, address) => { if (err) throw err; fastify.log.info(`server listening on ${address}`); }); })(); // curl http://localhost:3000/users ``` ## Browser-Sync https://github.com/BrowserSync/browser-sync [![GitHub stars](https://img.shields.io/github/stars/BrowserSync/browser-sync.svg?style=social&label=Star)](https://github.com/BrowserSync/browser-sync) ![browser-sync downloads](https://img.shields.io/npm/dm/browser-sync) ```javascript const browserSync = require('browser-sync').create(); const { createProxyMiddleware } = require('http-proxy-middleware'); const apiProxy = createProxyMiddleware({ target: 'http://www.example.org', changeOrigin: true, // for vhosted sites pathFilter: '/api', }); browserSync.init({ server: { baseDir: './', port: 3000, middleware: [apiProxy], }, startPath: '/api', }); ``` ## Polka https://github.com/lukeed/polka [![GitHub stars](https://img.shields.io/github/stars/lukeed/polka.svg?style=social&label=Star)](https://github.com/lukeed/polka) ![polka downloads](https://img.shields.io/npm/dm/polka) ```javascript const polka = require('polka'); const { createProxyMiddleware } = require('http-proxy-middleware'); const app = polka(); app.use( createProxyMiddleware({ target: 'http://www.example.org', changeOrigin: true, }), ); app.listen(3000); ``` ## lite-server https://github.com/johnpapa/lite-server [![GitHub stars](https://img.shields.io/github/stars/johnpapa/lite-server.svg?style=social&label=Star)](https://github.com/johnpapa/lite-server) ![lite-server downloads](https://img.shields.io/npm/dm/lite-server) File: `bs-config.js` ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const apiProxy = createProxyMiddleware({ target: 'http://www.example.org', changeOrigin: true, // for vhosted sites pathFilter: '/api', }); module.exports = { server: { // Start from key `10` in order to NOT overwrite the default 2 middleware provided // by `lite-server` or any future ones that might be added. // Reference: https://github.com/johnpapa/lite-server/blob/master/lib/config-defaults.js#L16 middleware: { 10: apiProxy, }, }, }; ``` ## grunt-contrib-connect https://github.com/gruntjs/grunt-contrib-connect [![GitHub stars](https://img.shields.io/github/stars/gruntjs/grunt-contrib-connect.svg?style=social&label=Star)](https://github.com/gruntjs/grunt-contrib-connect) ![grunt-contrib-connect downloads](https://img.shields.io/npm/dm/grunt-contrib-connect) As an `Array`: ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const apiProxy = createProxyMiddleware({ target: 'http://www.example.org', changeOrigin: true, // for vhosted sites pathFilter: '/api', }); grunt.initConfig({ connect: { server: { options: { middleware: [apiProxy], }, }, }, }); ``` As a `function`: ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const apiProxy = createProxyMiddleware({ target: 'http://www.example.org', changeOrigin: true, // for vhosted sites pathFilter: '/api', }); grunt.initConfig({ connect: { server: { options: { middleware: function (connect, options, middlewares) { // inject a custom middleware into the array of default middlewares middlewares.unshift(apiProxy); return middlewares; }, }, }, }, }); ``` ## gulp-connect https://github.com/avevlad/gulp-connect [![GitHub stars](https://img.shields.io/github/stars/avevlad/gulp-connect.svg?style=social&label=Star)](https://github.com/avevlad/gulp-connect) ![gulp-connect downloads](https://img.shields.io/npm/dm/gulp-connect) ```javascript const gulp = require('gulp'); const connect = require('gulp-connect'); const { createProxyMiddleware } = require('http-proxy-middleware'); gulp.task('connect', function () { connect.server({ root: ['./app'], middleware: function (connect, opt) { const apiProxy = createProxyMiddleware({ target: 'http://www.example.org', changeOrigin: true, // for vhosted sites pathFilter: '/api', }); return [apiProxy]; }, }); }); gulp.task('default', ['connect']); ``` ## grunt-browser-sync https://github.com/BrowserSync/grunt-browser-sync [![GitHub stars](https://img.shields.io/github/stars/BrowserSync/grunt-browser-sync.svg?style=social&label=Star)](https://github.com/BrowserSync/grunt-browser-sync) ![grunt-browser-sync downloads](https://img.shields.io/npm/dm/grunt-browser-sync) ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const apiProxy = createProxyMiddleware({ target: 'http://www.example.org', changeOrigin: true, // for vhosted sites pathFilter: '/api', }); grunt.initConfig({ // BrowserSync Task browserSync: { default_options: { options: { files: ['css/*.css', '*.html'], port: 9000, server: { baseDir: ['app'], middleware: apiProxy, }, }, }, }, }); ``` ## gulp-webserver https://github.com/schickling/gulp-webserver [![GitHub stars](https://img.shields.io/github/stars/schickling/gulp-webserver.svg?style=social&label=Star)](https://github.com/schickling/gulp-webserver) ![gulp-webserver downloads](https://img.shields.io/npm/dm/gulp-webserver) ```javascript const gulp = require('gulp'); const webserver = require('gulp-webserver'); const { createProxyMiddleware } = require('http-proxy-middleware'); gulp.task('webserver', function () { const apiProxy = createProxyMiddleware({ target: 'http://www.example.org', changeOrigin: true, // for vhosted sites pathFilter: '/api', }); gulp.src('app').pipe( webserver({ livereload: true, directoryListing: true, open: true, middleware: [apiProxy], }), ); }); gulp.task('default', ['webserver']); ``` ================================================ FILE: recipes/virtual-hosts.md ================================================ # Name-based Virtual Hosts This example will create a basic proxy middleware for [virtual hosted sites](https://en.wikipedia.org/wiki/Virtual_hosting#Name-based). When `changeOrigin` is set to `true`; Host [HTTP header](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields) will be set to match target's host. The `changeOrigin` option is provided by [http-proxy](https://github.com/nodejitsu/node-http-proxy). ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const options = { target: 'http://localhost:3000', changeOrigin: true, }; const apiProxy = createProxyMiddleware(options); ``` ================================================ FILE: recipes/websocket.md ================================================ # WebSocket This example will create a proxy middleware with websocket support. ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const socketProxy = createProxyMiddleware({ target: 'http://localhost:3000', pathFilter: '/socket', ws: true, }); ``` ## WebSocket - Path Rewrite This example will create a proxy middleware with websocket support and pathRewrite. ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const options = { target: 'http://localhost:3000', ws: true, pathFilter: '/socket', pathRewrite: { '^/socket': '', }, }; const socketProxy = createProxyMiddleware(options); ``` ## WebSocket - Server update subscription This example will create a proxy middleware with websocket support. Subscribe to server's upgrade event. ```javascript const { createProxyMiddleware } = require('http-proxy-middleware'); const socketProxy = createProxyMiddleware({ target: 'http://localhost:3000', pathFilter: '/socket', ws: true, }); server.on('upgrade', socketProxy.upgrade); // <-- subscribe to http 'upgrade' ``` ================================================ FILE: src/configuration.ts ================================================ import { ERRORS } from './errors'; import { Options } from './types'; export function verifyConfig(options: Options): void { if (!options.target && !options.router) { throw new Error(ERRORS.ERR_CONFIG_FACTORY_TARGET_MISSING); } } ================================================ FILE: src/debug.ts ================================================ import * as createDebug from 'debug'; /** * Debug instance with the given namespace: http-proxy-middleware */ export const Debug = createDebug('http-proxy-middleware'); ================================================ FILE: src/errors.ts ================================================ export enum ERRORS { ERR_CONFIG_FACTORY_TARGET_MISSING = '[HPM] Missing "target" option. Example: {target: "http://www.example.org"}', ERR_CONTEXT_MATCHER_GENERIC = '[HPM] Invalid pathFilter. Expecting something like: "/api" or ["/api", "/ajax"]', ERR_CONTEXT_MATCHER_INVALID_ARRAY = '[HPM] Invalid pathFilter. Plain paths (e.g. "/api") can not be mixed with globs (e.g. "/api/**"). Expecting something like: ["/api", "/ajax"] or ["/api/**", "!**.html"].', ERR_PATH_REWRITER_CONFIG = '[HPM] Invalid pathRewrite config. Expecting object with pathRewrite config or a rewrite function', } ================================================ FILE: src/factory.ts ================================================ import type * as http from 'node:http'; import { HttpProxyMiddleware } from './http-proxy-middleware'; import type { NextFunction, Options, RequestHandler } from './types'; 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; } ================================================ FILE: src/get-plugins.ts ================================================ import { debugProxyErrorsPlugin, errorResponsePlugin, loggerPlugin, proxyEventsPlugin, } from './plugins/default'; import type { Options, Plugin } from './types'; 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 = options.ejectPlugins ? [] // no default plugins when ejecting : [debugProxyErrorsPlugin, proxyEventsPlugin, loggerPlugin, ...maybeErrorResponsePlugin]; const userPlugins: Plugin[] = options.plugins ?? []; return [...defaultPlugins, ...userPlugins] as unknown as Plugin[]; } ================================================ FILE: src/handlers/fix-request-body.ts ================================================ import type * as http from 'node:http'; import * as querystring from 'node:querystring'; export type BodyParserLikeRequest = http.IncomingMessage & { body?: any }; /** * Fix proxied body if bodyParser is involved. */ export function fixRequestBody( proxyReq: http.ClientRequest, req: TReq, ): void { // skip fixRequestBody() when req.readableLength not 0 (bodyParser failure) if (req.readableLength !== 0) { return; } const requestBody = req.body; if (!requestBody) { return; } const contentType = proxyReq.getHeader('Content-Type') as string; if (!contentType) { return; } const writeBody = (bodyData: string) => { proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData)); proxyReq.write(bodyData); }; // Use if-elseif to prevent multiple writeBody/setHeader calls: // Error: "Cannot set headers after they are sent to the client" if (contentType.includes('application/json') || contentType.includes('+json')) { writeBody(JSON.stringify(requestBody)); } else if (contentType.includes('application/x-www-form-urlencoded')) { writeBody(querystring.stringify(requestBody)); } else if (contentType.includes('multipart/form-data')) { writeBody(handlerFormDataBodyData(contentType, requestBody)); } else if (contentType.includes('text/plain')) { writeBody(requestBody); } } /** * format FormData data * @param contentType * @param data * @returns */ function handlerFormDataBodyData(contentType: string, data: any) { const boundary = contentType.replace(/^.*boundary=(.*)$/, '$1'); let str = ''; for (const [key, value] of Object.entries(data)) { str += `--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${value}\r\n`; } return str; } ================================================ FILE: src/handlers/index.ts ================================================ export * from './public'; ================================================ FILE: src/handlers/public.ts ================================================ export { responseInterceptor } from './response-interceptor'; export { fixRequestBody } from './fix-request-body'; ================================================ FILE: src/handlers/response-interceptor.ts ================================================ import type * as http from 'node:http'; import * as zlib from 'node:zlib'; import { Debug } from '../debug'; import { getFunctionName } from '../utils/function'; const debug = Debug.extend('response-interceptor'); type Interceptor = ( buffer: Buffer, proxyRes: TReq, req: TReq, res: TRes, ) => Promise; /** * Intercept responses from upstream. * Automatically decompress (deflate, gzip, brotli). * Give developer the opportunity to modify intercepted Buffer and http.ServerResponse * * NOTE: must set options.selfHandleResponse=true (prevent automatic call of res.end()) */ export function responseInterceptor< TReq extends http.IncomingMessage = http.IncomingMessage, TRes extends http.ServerResponse = http.ServerResponse, >(interceptor: Interceptor) { return async function proxyResResponseInterceptor( 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']); // concat data stream _proxyRes.on('data', (chunk) => (buffer = Buffer.concat([buffer, chunk]))); _proxyRes.on('end', async () => { // copy original headers copyHeaders(proxyRes, res); // call interceptor with intercepted response (buffer) debug('call interceptor function: %s', getFunctionName(interceptor)); const interceptedBuffer = Buffer.from(await interceptor(buffer, originalProxyRes, req, res)); // set correct content-length (with double byte character support) debug('set content-length: %s', Buffer.byteLength(interceptedBuffer, 'utf8')); res.setHeader('content-length', Buffer.byteLength(interceptedBuffer, 'utf8')); debug('write intercepted response'); res.write(interceptedBuffer); res.end(); }); _proxyRes.on('error', (error) => { res.end(`Error fetching proxied request: ${error.message}`); }); }; } /** * Streaming decompression of proxy response * source: https://github.com/apache/superset/blob/9773aba522e957ed9423045ca153219638a85d2f/superset-frontend/webpack.proxy-config.js#L116 */ function decompress( proxyRes: TReq, contentEncoding?: string, ): TReq | zlib.Gunzip | zlib.Inflate | zlib.BrotliDecompress { let _proxyRes: TReq | zlib.Gunzip | zlib.Inflate | zlib.BrotliDecompress = proxyRes; let decompress; switch (contentEncoding) { case 'gzip': decompress = zlib.createGunzip(); break; case 'br': decompress = zlib.createBrotliDecompress(); break; case 'deflate': decompress = zlib.createInflate(); break; default: break; } if (decompress) { debug(`decompress proxy response with 'content-encoding': %s`, contentEncoding); _proxyRes.pipe(decompress); _proxyRes = decompress; } return _proxyRes; } /** * Copy original headers * https://github.com/apache/superset/blob/9773aba522e957ed9423045ca153219638a85d2f/superset-frontend/webpack.proxy-config.js#L78 */ function copyHeaders(originalResponse, response): void { debug('copy original response headers'); response.statusCode = originalResponse.statusCode; response.statusMessage = originalResponse.statusMessage; if (response.setHeader) { let keys = Object.keys(originalResponse.headers); // ignore chunked, brotli, gzip, deflate headers keys = keys.filter((key) => !['content-encoding', 'transfer-encoding'].includes(key)); keys.forEach((key) => { let value = originalResponse.headers[key]; if (key === 'set-cookie') { // remove cookie domain value = Array.isArray(value) ? value : [value]; value = value.map((x) => x.replace(/Domain=[^;]+?/i, '')); } response.setHeader(key, value); }); } else { response.headers = originalResponse.headers; } } ================================================ FILE: src/http-proxy-middleware.ts ================================================ import type * as http from 'node:http'; import type * as https from 'node:https'; import type * as net from 'node:net'; import * as httpProxy from 'http-proxy'; import { verifyConfig } from './configuration'; import { Debug as debug } from './debug'; import { getPlugins } from './get-plugins'; import { getLogger } from './logger'; import { matchPathFilter } from './path-filter'; import * as PathRewriter from './path-rewriter'; import * as Router from './router'; import type { Filter, Logger, Options, RequestHandler } from './types'; import { getFunctionName } from './utils/function'; export class HttpProxyMiddleware { private wsInternalSubscribed = false; private serverOnCloseSubscribed = false; private proxyOptions: Options; private proxy: httpProxy; private pathRewriter; private logger: Logger; constructor(options: Options) { verifyConfig(options); this.proxyOptions = options; this.logger = getLogger(options as unknown as Options); debug(`create proxy server`); this.proxy = httpProxy.createProxyServer({}); this.registerPlugins(this.proxy, this.proxyOptions); this.pathRewriter = PathRewriter.createPathRewriter(this.proxyOptions.pathRewrite); // returns undefined when "pathRewrite" is not provided // https://github.com/chimurai/http-proxy-middleware/issues/19 // expose function to upgrade externally 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, res, next?) => { if (this.shouldProxy(this.proxyOptions.pathFilter, req)) { try { const activeProxyOptions = await this.prepareProxyRequest(req); debug(`proxy request to target: %O`, activeProxyOptions.target); this.proxy.web(req, res, activeProxyOptions); } catch (err) { next?.(err); } } else { next?.(); } /** * Get the server object to subscribe to server events; * 'upgrade' for websocket and 'close' for graceful shutdown * * NOTE: * req.socket: node >= 13 * req.connection: node < 13 (Remove this when node 12/13 support is dropped) */ const server: https.Server = ((req.socket ?? req.connection) as any)?.server; if (server && !this.serverOnCloseSubscribed) { server.on('close', () => { debug('server close signal received: closing proxy server'); this.proxy.close(); }); this.serverOnCloseSubscribed = true; } if (this.proxyOptions.ws === true) { // use initial request to access the server object to subscribe to http upgrade event this.catchUpgradeRequest(server); } }) as RequestHandler; private registerPlugins(proxy: httpProxy, options: Options) { const plugins = getPlugins(options); plugins.forEach((plugin) => { debug(`register plugin: "${getFunctionName(plugin)}"`); plugin(proxy, options); }); } private catchUpgradeRequest = (server: https.Server) => { if (!this.wsInternalSubscribed) { debug('subscribing to server upgrade event'); server.on('upgrade', this.handleUpgrade); // prevent duplicate upgrade handling; // in case external upgrade is also configured this.wsInternalSubscribed = true; } }; private handleUpgrade = async (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => { try { if (this.shouldProxy(this.proxyOptions.pathFilter, req)) { const activeProxyOptions = await this.prepareProxyRequest(req); this.proxy.ws(req, socket, head, activeProxyOptions); debug('server upgrade event received. Proxying WebSocket'); } } catch (err) { // This error does not include the URL as the fourth argument as we won't // have the URL if `this.prepareProxyRequest` throws an error. this.proxy.emit('error', err, req, socket); } }; /** * Determine whether request should be proxied. */ private shouldProxy = ( pathFilter: Filter | undefined, req: http.IncomingMessage, ): boolean => { try { return matchPathFilter(pathFilter, req.url, req); } catch (err) { debug('Error: matchPathFilter() called with request url: ', `"${req.url}"`); this.logger.error(err); return false; } }; /** * Apply option.router and option.pathRewrite * Order matters: * Router uses original path for routing; * NOT the modified path, after it has been rewritten by pathRewrite * @param {Object} req * @return {Object} proxy options */ 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()} * FIXME: remove this patch in future release */ if ((this.middleware as unknown as any).__LEGACY_HTTP_PROXY_MIDDLEWARE__) { req.url = (req as unknown as any).originalUrl || req.url; } const newProxyOptions = Object.assign({}, this.proxyOptions); // Apply in order: // 1. option.router // 2. option.pathRewrite await this.applyRouter(req, newProxyOptions); await this.applyPathRewrite(req, this.pathRewriter); return newProxyOptions; }; // Modify option.target when router present. private applyRouter = async (req: http.IncomingMessage, options: Options) => { let newTarget; if (options.router) { newTarget = await Router.getTarget(req, options); if (newTarget) { debug('router new target: "%s"', newTarget); options.target = newTarget; } } }; // rewrite path private applyPathRewrite = async (req: http.IncomingMessage, pathRewriter) => { if (pathRewriter) { const path = await pathRewriter(req.url, req); if (typeof path === 'string') { debug('pathRewrite new path: %s', req.url); req.url = path; } else { debug('pathRewrite: no rewritten path found: %s', req.url); } } }; } ================================================ FILE: src/index.ts ================================================ export * from './factory'; export * from './handlers'; export type { Plugin, Filter, Options, RequestHandler } from './types'; /** * Default plugins */ export * from './plugins/default'; /** * Legacy exports */ export * from './legacy'; ================================================ FILE: src/legacy/create-proxy-middleware.ts ================================================ import type * as http from 'node:http'; import { Debug } from '../debug'; import { createProxyMiddleware } from '../factory'; import { Filter, RequestHandler } from '../types'; import { legacyOptionsAdapter } from './options-adapter'; import { LegacyOptions } from './types'; const debug = Debug.extend('legacy-create-proxy-middleware'); /** * @deprecated * This function is deprecated and will be removed in a future version. * * Use {@link createProxyMiddleware} instead. */ 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 proxyMiddleware = createProxyMiddleware(options); // https://github.com/chimurai/http-proxy-middleware/pull/731/files#diff-07e6ad10bda0df091b737caed42767657cd0bd74a01246a1a0b7ab59c0f6e977L118 debug('add marker for patching req.url (old behavior)'); (proxyMiddleware as any).__LEGACY_HTTP_PROXY_MIDDLEWARE__ = true; return proxyMiddleware; } ================================================ FILE: src/legacy/index.ts ================================================ export * from './public'; ================================================ FILE: src/legacy/options-adapter.ts ================================================ import * as url from 'node:url'; import { Debug } from '../debug'; import { getLogger } from '../logger'; import { Filter, Options } from '../types'; import { Logger } from '../types'; import { LegacyOptions } from './types'; const debug = Debug.extend('legacy-options-adapter'); // https://github.com/chimurai/http-proxy-middleware/blob/7341704d0aa9d1606dfd37ebfdffddd34c894784/src/_handlers.ts#L20-L27 const proxyEventMap = { onError: 'error', onProxyReq: 'proxyReq', onProxyRes: 'proxyRes', onProxyReqWs: 'proxyReqWs', onOpen: 'open', onClose: 'close', }; /** * Convert {@link LegacyOptions legacy Options} to new {@link Options} */ export function legacyOptionsAdapter( legacyContext: Filter | LegacyOptions, legacyOptions: LegacyOptions, ): Options { let options: LegacyOptions = {}; let logger: Logger; // https://github.com/chimurai/http-proxy-middleware/pull/716 if (typeof legacyContext === 'string' && !!url.parse(legacyContext).host) { throw new Error( `Shorthand syntax is removed from legacyCreateProxyMiddleware(). Please use "legacyCreateProxyMiddleware({ target: 'http://www.example.org' })" instead. More details: https://github.com/chimurai/http-proxy-middleware/blob/master/MIGRATION.md#removed-shorthand-usage `, ); } // detect old "context" argument and convert to "options.pathFilter" // 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 }; logger = getLegacyLogger(options); logger.warn( `[http-proxy-middleware] Legacy "context" argument is deprecated. Migrate your "context" to "options.pathFilter": const options = { pathFilter: '${legacyContext}', } More details: https://github.com/chimurai/http-proxy-middleware/blob/master/MIGRATION.md#removed-context-argument `, ); } else if (legacyContext && !legacyOptions) { options = { ...(legacyContext as LegacyOptions) }; logger = getLegacyLogger(options); } else { logger = getLegacyLogger({}) as never; } // map old event names to new event names // https://github.com/chimurai/http-proxy-middleware/pull/745/files#diff-c54113cf61ec99691748a3890bfbeb00e10efb3f0a76f03a0fd9ec49072e410aL48-L53 Object.entries(proxyEventMap).forEach(([legacyEventName, proxyEventName]) => { if (options[legacyEventName]) { options.on = { ...options.on }; options.on[proxyEventName] = options[legacyEventName]; debug('map legacy event "%s" to "on.%s"', legacyEventName, proxyEventName); logger.warn( `[http-proxy-middleware] Legacy "${legacyEventName}" is deprecated. Migrate to "options.on.${proxyEventName}": const options = { on: { ${proxyEventName}: () => {}, }, } More details: https://github.com/chimurai/http-proxy-middleware/blob/master/MIGRATION.md#refactored-proxy-events `, ); } }); // map old logProvider to new logger // https://github.com/chimurai/http-proxy-middleware/pull/749 const logProvider = options.logProvider && options.logProvider(); const logLevel = options.logLevel; debug('legacy logLevel', logLevel); debug('legacy logProvider: %O', logProvider); if (typeof logLevel === 'string' && logLevel !== 'silent') { debug('map "logProvider" to "logger"'); logger.warn( `[http-proxy-middleware] Legacy "logLevel" and "logProvider" are deprecated. Migrate to "options.logger": const options = { logger: console, } More details: https://github.com/chimurai/http-proxy-middleware/blob/master/MIGRATION.md#removed-logprovider-and-loglevel-options `, ); } return options; } function getLegacyLogger(options): Logger { const legacyLogger = options.logProvider && options.logProvider(); if (legacyLogger) { options.logger = legacyLogger; } return getLogger(options); } ================================================ FILE: src/legacy/public.ts ================================================ export { legacyCreateProxyMiddleware } from './create-proxy-middleware'; export { LegacyOptions } from './types'; ================================================ FILE: src/legacy/types.ts ================================================ import type * as http from 'node:http'; import { Options } from '../types'; /** * @deprecated * * Will be removed in a future version. */ export interface LegacyOptions< TReq = http.IncomingMessage, TRes = http.ServerResponse, > extends Options { /** * @deprecated * Use `on.error` instead. * * @example * ```js * { * on: { * error: () => {} * } * ``` */ onError?: (...args: any[]) => void; //httpProxy.ErrorCallback; /** * @deprecated * Use `on.proxyRes` instead. * * @example * ```js * { * on: { * proxyRes: () => {} * } * ``` */ onProxyRes?: (...args: any[]) => void; //httpProxy.ProxyResCallback; /** * @deprecated * Use `on.proxyReq` instead. * * @example * ```js * { * on: { * proxyReq: () => {} * } * ``` */ onProxyReq?: (...args: any[]) => void; //httpProxy.ProxyReqCallback; /** * @deprecated * Use `on.proxyReqWs` instead. * * @example * ```js * { * on: { * proxyReqWs: () => {} * } * ``` */ onProxyReqWs?: (...args: any[]) => void; //httpProxy.ProxyReqWsCallback; /** * @deprecated * Use `on.open` instead. * * @example * ```js * { * on: { * open: () => {} * } * ``` */ onOpen?: (...args: any[]) => void; //httpProxy.OpenCallback; /** * @deprecated * Use `on.close` instead. * * @example * ```js * { * on: { * close: () => {} * } * ``` */ onClose?: (...args: any[]) => void; //httpProxy.CloseCallback; /** * @deprecated * Use `logger` instead. * * @example * ```js * { * logger: console * } * ``` */ logProvider?: any; /** * @deprecated * Use `logger` instead. * * @example * ```js * { * logger: console * } * ``` */ logLevel?: any; } ================================================ FILE: src/logger.ts ================================================ import { Logger, Options } from './types'; /** * Compatibility matrix * | Library | log | info | warn | error | \ | |----------|:------|:-------|:------|:--------|:------------------| | console | ✅ | ✅ | ✅ | ✅ | ✅ (%s %o %O) | | bunyan | ❌ | ✅ | ✅ | ✅ | ✅ (%s %o %O) | | pino | ❌ | ✅ | ✅ | ✅ | ✅ (%s %o %O) | | winston | ❌ | ✅ | ✅ | ✅ | ✅ (%s %o %O)^1 | | log4js | ❌ | ✅ | ✅ | ✅ | ✅ (%s %o %O) | * * ^1: https://github.com/winstonjs/winston#string-interpolation */ const noopLogger: Logger = { info: () => {}, warn: () => {}, error: () => {}, }; export function getLogger(options: Options): Logger { return (options.logger as Logger) || noopLogger; } ================================================ FILE: src/path-filter.ts ================================================ import type * as http from 'node:http'; import * as url from 'node:url'; import * as isGlob from 'is-glob'; import * as micromatch from 'micromatch'; import { ERRORS } from './errors'; import type { Filter } from './types'; export function matchPathFilter( pathFilter: Filter = '/', uri: string | undefined, req: http.IncomingMessage, ): boolean { // single path if (isStringPath(pathFilter as string)) { return matchSingleStringPath(pathFilter as string, uri); } // single glob path if (isGlobPath(pathFilter as string)) { return matchSingleGlobPath(pathFilter as unknown as string[], uri); } // multi path if (Array.isArray(pathFilter)) { if (pathFilter.every(isStringPath)) { return matchMultiPath(pathFilter, uri); } if (pathFilter.every(isGlobPath)) { return matchMultiGlobPath(pathFilter as string[], uri); } throw new Error(ERRORS.ERR_CONTEXT_MATCHER_INVALID_ARRAY); } // custom matching if (typeof pathFilter === 'function') { const pathname = getUrlPathName(uri) as string; return pathFilter(pathname, req as TReq); } throw new Error(ERRORS.ERR_CONTEXT_MATCHER_GENERIC); } /** * @param {String} pathFilter '/api' * @param {String} uri 'http://example.org/api/b/c/d.html' * @return {Boolean} */ function matchSingleStringPath(pathFilter: string, uri?: string) { const pathname = getUrlPathName(uri); return pathname?.indexOf(pathFilter) === 0; } function matchSingleGlobPath(pattern: string | string[], uri?: string) { const pathname = getUrlPathName(uri) as string; const matches = micromatch([pathname], pattern); return matches && matches.length > 0; } function matchMultiGlobPath(patternList: string | string[], uri?: string) { return matchSingleGlobPath(patternList, uri); } /** * @param {String} pathFilterList ['/api', '/ajax'] * @param {String} uri 'http://example.org/api/b/c/d.html' * @return {Boolean} */ function matchMultiPath(pathFilterList: string[], uri?: string) { let isMultiPath = false; for (const context of pathFilterList) { if (matchSingleStringPath(context, uri)) { isMultiPath = true; break; } } return isMultiPath; } /** * Parses URI and returns RFC 3986 path * * @param {String} uri from req.url * @return {String} RFC 3986 path */ function getUrlPathName(uri?: string) { return uri && url.parse(uri).pathname; } function isStringPath(pathFilter: string) { return typeof pathFilter === 'string' && !isGlob(pathFilter); } function isGlobPath(pathFilter: string) { return isGlob(pathFilter); } ================================================ FILE: src/path-rewriter.ts ================================================ import { isPlainObject } from 'is-plain-object'; import { Debug } from './debug'; import { ERRORS } from './errors'; const debug = Debug.extend('path-rewriter'); type RewriteRule = { regex: RegExp; value: string }; /** * Create rewrite function, to cache parsed rewrite rules. * * @param {Object} rewriteConfig * @return {Function} Function to rewrite paths; This function should accept `path` (request.url) as parameter */ export function createPathRewriter(rewriteConfig) { let rulesCache: RewriteRule[]; if (!isValidRewriteConfig(rewriteConfig)) { return; } if (typeof rewriteConfig === 'function') { const customRewriteFn = rewriteConfig; return customRewriteFn; } else { rulesCache = parsePathRewriteRules(rewriteConfig); return rewritePath; } function rewritePath(path) { let result = path; for (const rule of rulesCache) { if (rule.regex.test(path)) { result = result.replace(rule.regex, rule.value); debug('rewriting path from "%s" to "%s"', path, result); break; } } return result; } } function isValidRewriteConfig(rewriteConfig) { if (typeof rewriteConfig === 'function') { return true; } else if (isPlainObject(rewriteConfig)) { return Object.keys(rewriteConfig).length !== 0; } else if (rewriteConfig === undefined || rewriteConfig === null) { return false; } else { throw new Error(ERRORS.ERR_PATH_REWRITER_CONFIG); } } function parsePathRewriteRules(rewriteConfig: Record) { const rules: RewriteRule[] = []; if (isPlainObject(rewriteConfig)) { for (const [key, value] of Object.entries(rewriteConfig)) { rules.push({ regex: new RegExp(key), value: value, }); debug('rewrite rule created: "%s" ~> "%s"', key, value); } } return rules; } ================================================ FILE: src/plugins/default/debug-proxy-errors-plugin.ts ================================================ import { Debug } from '../../debug'; import { Plugin } from '../../types'; const debug = Debug.extend('debug-proxy-errors-plugin'); /** * Subscribe to {@link https://www.npmjs.com/package/http-proxy#listening-for-proxy-events http-proxy 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 => { /** * http-proxy doesn't handle any errors by default (https://github.com/http-party/node-http-proxy#listening-for-proxy-events) * Prevent server from crashing when http-proxy errors (uncaught errors) */ proxyServer.on('error', (error, req, res, target) => { debug(`http-proxy error event: \n%O`, error); }); proxyServer.on('proxyReq', (proxyReq, req, socket) => { socket.on('error', (error) => { debug('Socket error in proxyReq event: \n%O', error); }); }); /** * Fix SSE close events * @link https://github.com/chimurai/http-proxy-middleware/issues/678 * @link https://github.com/http-party/node-http-proxy/issues/1520#issue-877626125 */ proxyServer.on('proxyRes', (proxyRes, req, res) => { res.on('close', () => { if (!res.writableEnded) { debug('Destroying proxyRes in proxyRes close event'); proxyRes.destroy(); } }); }); /** * Fix crash when target server restarts * https://github.com/chimurai/http-proxy-middleware/issues/476#issuecomment-746329030 * https://github.com/webpack/webpack-dev-server/issues/1642#issuecomment-790602225 */ proxyServer.on('proxyReqWs', (proxyReq, req, socket) => { socket.on('error', (error) => { debug('Socket error in proxyReqWs event: \n%O', error); }); }); proxyServer.on('open', (proxySocket) => { proxySocket.on('error', (error) => { debug('Socket error in open event: \n%O', error); }); }); proxyServer.on('close', (req, socket, head) => { socket.on('error', (error) => { debug('Socket error in close event: \n%O', error); }); }); // https://github.com/webpack/webpack-dev-server/issues/1642#issuecomment-1103136590 proxyServer.on('econnreset', (error, req, res, target) => { debug(`http-proxy econnreset event: \n%O`, error); }); }; ================================================ FILE: src/plugins/default/error-response-plugin.ts ================================================ import type * as http from 'node:http'; import type { Socket } from 'node:net'; import { getStatusCode } from '../../status-code'; import { Plugin } from '../../types'; import { sanitize } from '../../utils/sanitize'; function isResponseLike(obj: any): obj is http.ServerResponse { return obj && typeof obj.writeHead === 'function'; } function isSocketLike(obj: any): obj is Socket { return obj && typeof obj.write === 'function' && !('writeHead' in obj); } export const errorResponsePlugin: Plugin = (proxyServer, options) => { proxyServer.on('error', (err, req, res, target?) => { // Re-throw error. Not recoverable since req & res are empty. if (!req && !res) { throw err; // "Error: Must provide a proper URL as target" } if (isResponseLike(res)) { if (!res.headersSent) { const statusCode = getStatusCode((err as unknown as any).code); res.writeHead(statusCode); } const host = req.headers && req.headers.host; res.end(`Error occurred while trying to proxy: ${sanitize(host)}${sanitize(req.url)}`); } else if (isSocketLike(res)) { res.destroy(); } }); }; ================================================ FILE: src/plugins/default/index.ts ================================================ export * from './debug-proxy-errors-plugin'; export * from './error-response-plugin'; export * from './logger-plugin'; export * from './proxy-events'; ================================================ FILE: src/plugins/default/logger-plugin.ts ================================================ import type { IncomingMessage } from 'node:http'; import { URL } from 'node:url'; import { getLogger } from '../../logger'; import { Plugin } from '../../types'; import { getPort } from '../../utils/logger-plugin'; type ExpressRequest = { /** Express req.baseUrl */ baseUrl?: string; }; type BrowserSyncRequest = { /** BrowserSync req.originalUrl */ originalUrl?: string; }; /** Request Types from different server libs */ type FrameworkRequest = IncomingMessage & ExpressRequest & BrowserSyncRequest; export const loggerPlugin: Plugin = (proxyServer, options) => { const logger = getLogger(options); proxyServer.on('error', (err, req, res, target?) => { const hostname = req?.headers?.host; const requestHref = `${hostname}${req?.url}`; const targetHref = `${(target as unknown as any)?.href}`; // target is undefined when websocket errors const errorMessage = '[HPM] Error occurred while proxying request %s to %s [%s] (%s)'; const errReference = 'https://nodejs.org/api/errors.html#errors_common_system_errors'; // link to Node Common Systems Errors page logger.error(errorMessage, requestHref, targetHref, (err as any).code || err, errReference); }); /** * Log request and response * @example * ```shell * [HPM] GET /users/ -> http://jsonplaceholder.typicode.com/users/ [304] * ``` */ proxyServer.on('proxyRes', (proxyRes: any, req: FrameworkRequest, res) => { // BrowserSync uses req.originalUrl // Next.js doesn't have req.baseUrl const originalUrl = req.originalUrl ?? `${req.baseUrl || ''}${req.url}`; // construct targetUrl let target: URL; try { const port = getPort(proxyRes.req?.agent?.sockets); const obj = { protocol: proxyRes.req.protocol, host: proxyRes.req.host, pathname: proxyRes.req.path, } as URL; target = new URL(`${obj.protocol}//${obj.host}${obj.pathname}`); if (port) { target.port = port; } // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (err) { // nock issue (https://github.com/chimurai/http-proxy-middleware/issues/1035) // fallback to old implementation (less correct - without port) target = new URL(options.target as URL); target.pathname = proxyRes.req.path; } const targetUrl = target.toString(); const exchange = `[HPM] ${req.method} ${originalUrl} -> ${targetUrl} [${proxyRes.statusCode}]`; logger.info(exchange); }); /** * When client opens WebSocket connection */ proxyServer.on('open', (socket) => { logger.info('[HPM] Client connected: %o', socket.address()); }); /** * When client closes WebSocket connection */ proxyServer.on('close', (req, proxySocket, proxyHead) => { logger.info('[HPM] Client disconnected: %o', proxySocket.address()); }); }; ================================================ FILE: src/plugins/default/proxy-events.ts ================================================ import { Debug } from '../../debug'; import { Plugin } from '../../types'; import { getFunctionName } from '../../utils/function'; const debug = Debug.extend('proxy-events-plugin'); /** * Implements option.on object to subscribe to http-proxy events. * * @example * ```js * createProxyMiddleware({ * on: { * error: (error, req, res, target) => {}, * proxyReq: (proxyReq, req, res, options) => {}, * proxyReqWs: (proxyReq, req, socket, options) => {}, * proxyRes: (proxyRes, req, res) => {}, * open: (proxySocket) => {}, * close: (proxyRes, proxySocket, proxyHead) => {}, * start: (req, res, target) => {}, * end: (req, res, proxyRes) => {}, * econnreset: (error, req, res, target) => {}, * } * }); * ``` */ export const proxyEventsPlugin: Plugin = (proxyServer, options) => { Object.entries(options.on || {}).forEach(([eventName, handler]) => { debug(`register event handler: "${eventName}" -> "${getFunctionName(handler)}"`); proxyServer.on(eventName, handler as (...args: unknown[]) => void); }); }; ================================================ FILE: src/router.ts ================================================ import { isPlainObject } from 'is-plain-object'; import { Debug } from './debug'; const debug = Debug.extend('router'); export async function getTarget(req, config) { let newTarget; const router = config.router; if (isPlainObject(router)) { newTarget = getTargetFromProxyTable(req, router); } else if (typeof router === 'function') { newTarget = await router(req); } return newTarget; } function getTargetFromProxyTable(req, table) { let result; const host = req.headers.host; const path = req.url; const hostAndPath = host + path; for (const [key, value] of Object.entries(table)) { if (containsPath(key)) { if (hostAndPath.indexOf(key) > -1) { // match 'localhost:3000/api' result = value; debug('match: "%s" -> "%s"', key, result); break; } } else { if (key === host) { // match 'localhost:3000' result = value; debug('match: "%s" -> "%s"', host, result); break; } } } return result; } function containsPath(v) { return v.indexOf('/') > -1; } ================================================ FILE: src/status-code.ts ================================================ export function getStatusCode(errorCode: string): number { let statusCode: number; if (/HPE_INVALID/.test(errorCode)) { statusCode = 502; } else { switch (errorCode) { case 'ECONNRESET': case 'ENOTFOUND': case 'ECONNREFUSED': case 'ETIMEDOUT': statusCode = 504; break; default: statusCode = 500; break; } } return statusCode; } ================================================ FILE: src/types.ts ================================================ /** * Based on definition by DefinitelyTyped: * https://github.com/DefinitelyTyped/DefinitelyTyped/blob/6f529c6c67a447190f86bfbf894d1061e41e07b7/types/http-proxy-middleware/index.d.ts */ import type * as http from 'node:http'; import type * as net from 'node:net'; import type * as httpProxy from 'http-proxy'; export type NextFunction void> = T; export interface RequestHandler< TReq = http.IncomingMessage, TRes = http.ServerResponse, TNext = NextFunction, > { (req: TReq, res: TRes, next?: TNext): Promise; upgrade: (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => void; } export type Filter = | string | string[] | ((pathname: string, req: TReq) => boolean); export interface Plugin { (proxyServer: httpProxy, options: Options): void; } 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; } export type Logger = Pick; 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. * @link https://github.com/chimurai/http-proxy-middleware/blob/master/recipes/pathFilter.md * @since v3.0.0 */ pathFilter?: Filter; /** * Modify request paths before requests are send to the target. * @example * ```js * createProxyMiddleware({ * pathRewrite: { * '^/api/old-path': '/api/new-path', // rewrite path * } * }); * ``` * @link https://github.com/chimurai/http-proxy-middleware/blob/master/recipes/pathRewrite.md */ pathRewrite?: | { [regexp: string]: string } | ((path: string, req: TReq) => string | undefined) | ((path: string, req: TReq) => Promise); /** * Access the internal http-proxy server instance to customize behavior * * @example * ```js * createProxyMiddleware({ * plugins: [(proxyServer, options) => { * proxyServer.on('error', (error, req, res) => { * console.error(error); * }); * }] * }); * ``` * @link https://github.com/chimurai/http-proxy-middleware#plugins-array * @since v3.0.0 */ plugins?: Plugin[]; /** * Eject pre-configured plugins. * NOTE: register your own error handlers to prevent server from crashing. * * @link https://github.com/chimurai/http-proxy-middleware#ejectplugins-boolean-default-false * @since v3.0.0 */ ejectPlugins?: boolean; /** * Listen to http-proxy events * @see {@link OnProxyEvent} for available events * @example * ```js * createProxyMiddleware({ * on: { * error: (error, req, res, target) => { * console.error(error); * } * } * }); * ``` * @link https://github.com/chimurai/http-proxy-middleware/blob/master/recipes/proxy-events.md * @since v3.0.0 */ on?: OnProxyEvent; /** * Dynamically set the {@link Options.target `options.target`}. * @example * ```js * createProxyMiddleware({ * router: async (req) => { * return 'http://127:0.0.1:3000'; * } * }); * ``` * @link https://github.com/chimurai/http-proxy-middleware/blob/master/recipes/router.md */ router?: | { [hostOrPath: string]: httpProxy.ServerOptions['target'] } | ((req: TReq) => httpProxy.ServerOptions['target']) | ((req: TReq) => Promise); /** * Log information from http-proxy-middleware * @example * ```js * createProxyMiddleware({ * logger: console * }); * ``` * @link https://github.com/chimurai/http-proxy-middleware/blob/master/recipes/logger.md * @since v3.0.0 */ logger?: Logger; } ================================================ FILE: src/utils/function.ts ================================================ /* eslint-disable @typescript-eslint/no-unsafe-function-type */ export function getFunctionName(fn: Function): string { return fn.name || '[anonymous Function]'; } ================================================ FILE: src/utils/logger-plugin.ts ================================================ import type { Agent } from 'node:http'; export type Sockets = Pick; /** * Get port from target * Using proxyRes.req.agent.sockets to determine the target port */ export function getPort(sockets?: Sockets): string | undefined { return Object.keys(sockets || {})?.[0]?.split(':')[1]; } ================================================ FILE: src/utils/sanitize.ts ================================================ export function sanitize(input: string | undefined): string { return input?.replace(/[<>]/g, (i) => encodeURIComponent(i)) ?? ''; } ================================================ FILE: test/e2e/express-error-middleware.spec.ts ================================================ import * as request from 'supertest'; import { createApp, createProxyMiddleware } from './test-kit'; describe('express error middleware', () => { it('should propagate error to express', async () => { let httpProxyError: Error | undefined; const proxyMiddleware = createProxyMiddleware({ changeOrigin: true, router: (req) => undefined, // Trigger "Error: Must provide a proper URL as target" }); const errorMiddleware = (err, req, res, next) => { httpProxyError = err; res.status(504).send('Something broke!'); }; const app = createApp(proxyMiddleware, errorMiddleware); const response = await request(app).get('/get').expect(504); expect(httpProxyError?.message).toBe('Must provide a proper URL as target'); expect(response.text).toBe('Something broke!'); }); }); ================================================ FILE: test/e2e/express-router.spec.ts ================================================ import * as express from 'express'; import * as request from 'supertest'; import { Options } from '../../src/index'; import { createProxyMiddleware } from './test-kit'; describe('Usage in Express', () => { let app: express.Express; let agent: request.Agent; beforeEach(() => { app = express(); }); // https://github.com/chimurai/http-proxy-middleware/issues/94 describe('Express Sub Route', () => { beforeEach(() => { // sub route config const sub = express.Router(); function filter(pathname, req) { const urlFilter = new RegExp('^/sub/api'); const match = urlFilter.test(pathname); return match; } /** * Mount proxy without 'path' in sub route */ const proxyConfig: Options = { changeOrigin: true, target: 'http://jsonplaceholder.typicode.com', pathFilter: filter, }; sub.use(createProxyMiddleware(proxyConfig)); sub.get('/hello', jsonMiddleware({ content: 'foobar' })); // configure sub route on /sub junction app.use('/sub', sub); // start server agent = request(app); }); it('should still return a response when route does not match proxyConfig', async () => { const response = await agent.get('/sub/hello'); expect(response.body).toEqual({ content: 'foobar' }); }); }); function jsonMiddleware(data) { return (req, res) => { res.json(data); }; } }); ================================================ FILE: test/e2e/http-proxy-middleware.spec.ts ================================================ import type * as http from 'node:http'; import * as bodyParser from 'body-parser'; import type * as express from 'express'; import { CompletedRequest, Mockttp, getLocal } from 'mockttp'; import * as request from 'supertest'; import type { Logger } from '../../src/types'; import { createApp, createAppWithPath, createProxyMiddleware, fixRequestBody } from './test-kit'; describe('E2E http-proxy-middleware', () => { describe('http-proxy-middleware creation', () => { it('should create a middleware', () => { const middleware = createProxyMiddleware({ target: `http://localhost:8000`, pathFilter: '/api', }); expect(typeof middleware).toBe('function'); }); }); describe('pathFilter matching', () => { describe('do not proxy', () => { const mockReq: http.IncomingMessage = { url: '/foo/bar', } as http.IncomingMessage; const mockRes: http.ServerResponse = {} as http.ServerResponse; const mockNext: express.NextFunction = jest.fn(); beforeEach(() => { const middleware = createProxyMiddleware({ target: `http://localhost:8000`, pathFilter: '/api', }); middleware(mockReq, mockRes, mockNext); }); it('should not proxy requests when request url does not match pathFilter', () => { expect(mockNext).toHaveBeenCalled(); }); }); }); describe('http-proxy-middleware in actual server', () => { let mockTargetServer: Mockttp; let agent: request.Agent; beforeEach(async () => { mockTargetServer = getLocal(); await mockTargetServer.start(); }); afterEach(async () => { await mockTargetServer.stop(); }); describe('basic setup, requests to target', () => { beforeEach(() => { agent = request( createApp( createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, pathFilter: '/api', }), ), ); }); it('should have response body: "HELLO WEB"', async () => { await mockTargetServer.forGet('/api').thenReply(200, 'HELLO WEB'); const response = await agent.get(`/api`).expect(200); expect(response.text).toBe('HELLO WEB'); }); it('should have proxied the uri-path and uri-query, but not the uri-hash', async () => { await mockTargetServer .forGet('/api/b/c/dp') .withExactQuery('?q=1&r=[2,3]') .thenReply(200, 'OK'); const response = await request(`http://localhost:${mockTargetServer.port}`) .get(`/api/b/c/dp?q=1&r=[2,3]#s`) .expect(200); expect(response.text).toBe('OK'); }); }); describe('basic setup with configured body-parser', () => { it('should proxy request body from form', async () => { agent = request( createApp( bodyParser.urlencoded({ extended: false }), createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, pathFilter: '/api', on: { proxyReq: fixRequestBody, }, }), ), ); await mockTargetServer.forPost('/api').thenCallback(async (req) => { expect(await req.body.getText()).toBe('foo=bar&bar=baz'); return { statusCode: 200 }; }); await agent.post('/api').send('foo=bar').send('bar=baz').expect(200); }); it('should proxy request body from json', async () => { agent = request( createApp( bodyParser.json(), createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, pathFilter: '/api', on: { proxyReq: fixRequestBody, }, }), ), ); await mockTargetServer.forPost('/api').thenCallback(async (req) => { expect(await req.body.getJson()).toEqual({ foo: 'bar', bar: 'baz', doubleByte: '文' }); return { statusCode: 200 }; }); await agent.post('/api').send({ foo: 'bar', bar: 'baz', doubleByte: '文' }).expect(200); }); }); describe('custom pathFilter matcher/filter', () => { it('should have response body: "HELLO WEB"', async () => { const filter = (path, req) => { return true; }; agent = request( createApp( createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, pathFilter: filter, }), ), ); await mockTargetServer.forGet('/api/b/c/d').thenReply(200, 'HELLO WEB'); const response = await agent.get(`/api/b/c/d`).expect(200); expect(response.text).toBe('HELLO WEB'); }); it('should not proxy when filter returns false', async () => { const filter = (path, req) => { return false; }; agent = request( createApp( createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, pathFilter: filter, }), ), ); await mockTargetServer.forGet('/api/b/c/d').thenReply(200, 'HELLO WEB'); const response = await agent.get(`/api/b/c/d`).expect(404); expect(response.status).toBe(404); }); it('should not proxy when filter throws Error', async () => { const myError = new Error('MY_ERROR'); const filter = (path, req) => { throw myError; }; const logger: Logger = { info: jest.fn(), warn: jest.fn(), error: jest.fn(), }; agent = request( createApp( createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, pathFilter: filter, logger: logger, }), ), ); await mockTargetServer.forGet('/api/b/c/d').thenReply(200, 'HELLO WEB'); const response = await agent.get(`/api/b/c/d`).expect(404); expect(response.status).toBe(404); expect(logger.error).toHaveBeenCalledWith(myError); }); }); describe('multi path', () => { beforeEach(() => { agent = request( createApp( createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, pathFilter: ['/api', '/ajax'], }), ), ); }); it('should proxy to path /api', async () => { await mockTargetServer.forGet(/\/api\/.+/).thenReply(200, 'HELLO /API'); const response = await agent.get(`/api/b/c/d`).expect(200); expect(response.text).toBe('HELLO /API'); }); it('should proxy to path /ajax', async () => { await mockTargetServer.forGet(/\/ajax\/.+/).thenReply(200, 'HELLO /AJAX'); const response = await agent.get(`/ajax/b/c/d`).expect(200); expect(response.text).toBe('HELLO /AJAX'); }); it('should not proxy with no matching path', async () => { const response = await agent.get(`/lorum/ipsum`).expect(404); expect(response.status).toBe(404); }); }); describe('wildcard path matching', () => { beforeEach(() => { agent = request( createApp( createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, pathFilter: '/api/**', }), ), ); }); it('should proxy to path', async () => { await mockTargetServer.forGet(/\/api\/.+/).thenReply(200, 'HELLO /api'); const response = await agent.get(`/api/b/c/d`).expect(200); expect(response.text).toBe('HELLO /api'); }); }); describe('multi glob wildcard path matching', () => { beforeEach(() => { agent = request( createApp( createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, pathFilter: ['**/*.html', '!**.json'], }), ), ); }); it('should proxy to paths ending with *.html', async () => { await mockTargetServer.forGet(/.+html$/).thenReply(200, 'HELLO .html'); const response = await agent.get(`/api/some/endpoint/index.html`).expect(200); expect(response.text).toBe('HELLO .html'); }); it('should not proxy to paths ending with *.json', async () => { await mockTargetServer.forGet(/.+json$/).thenReply(200, 'HELLO .html'); const response = await agent.get(`/api/some/endpoint/data.json`).expect(404); expect(response.status).toBe(404); }); }); describe('option.headers - additional request headers', () => { beforeEach(() => { agent = request( createApp( createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, pathFilter: '/api', headers: { host: 'foobar.dev' }, }), ), ); }); it('should send request header "host" to target server', async () => { let completedRequest: CompletedRequest | undefined; await mockTargetServer.forGet().thenCallback((req) => { completedRequest = req; return { statusCode: 200, body: 'OK' }; }); const response = await agent.get(`/api/some/endpoint/index.html`).expect(200); expect(response.text).toBe('OK'); expect(completedRequest?.headers.host).toBe('foobar.dev'); }); }); describe('default httpProxy on error handling', () => { beforeEach(() => { agent = request( createApp( createProxyMiddleware({ target: `http://localhost:666`, // unreachable host on port:666 }), ), ); }); it('should handle errors when host is not reachable', async () => { const response = await agent.get(`/api/some/endpoint`).expect(504); expect(response.status).toBe(504); }); }); describe('option.on.error - custom error handler', () => { beforeEach(() => { agent = request( createApp( createProxyMiddleware({ target: `http://localhost:666`, // unreachable host on port:666 on: { error(err, req, res) { if (err) { (res as express.Response).writeHead(418); // different error code res.end("I'm a teapot"); // no response body } }, }, }), ), ); }); it('should respond with custom http status code', async () => { const response = await agent.get(`/api/some/endpoint`).expect(418); expect(response.status).toBe(418); }); it('should respond with custom status message', async () => { const response = await agent.get(`/api/some/endpoint`).expect(418); expect(response.text).toBe("I'm a teapot"); }); }); describe('option.onProxyRes', () => { beforeEach(() => { agent = request( createApp( createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, pathFilter: '/api', on: { proxyRes: (proxyRes, req, res) => { // tslint:disable-next-line: no-string-literal proxyRes['headers']['x-added'] = 'foobar'; // add custom header to response // tslint:disable-next-line: no-string-literal delete proxyRes['headers']['x-removed']; }, }, }), ), ); }); it('should add `x-added` as custom header to response"', async () => { await mockTargetServer.forGet().thenReply(200, 'HELLO .html'); const response = await agent.get(`/api/some/endpoint/index.html`).expect(200); expect(response.header['x-added']).toBe('foobar'); }); it('should remove `x-removed` field from response header"', async () => { await mockTargetServer.forGet().thenCallback((req) => { return { statusCode: 200, headers: { 'x-removed': 'this should be removed', }, }; }); const response = await agent.get(`/api/some/endpoint/index.html`).expect(200); expect(response.header['x-removed']).toBeUndefined(); }); }); describe('option.onProxyReq', () => { beforeEach(() => { agent = request( createApp( createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, pathFilter: '/api', on: { proxyReq: (proxyReq, req, res) => { proxyReq.setHeader('x-added', 'added-from-hpm'); // add custom header to request }, }, }), ), ); }); it('should add `x-added` as custom header to request"', async () => { let completedRequest: CompletedRequest | undefined; await mockTargetServer.forGet().thenCallback((req) => { completedRequest = req; return { statusCode: 200 }; }); await agent.get(`/api/foo/bar`).expect(200); expect(completedRequest?.headers['x-added']).toBe('added-from-hpm'); }); }); describe('option.pathRewrite', () => { beforeEach(() => { agent = request( createApp( createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, pathRewrite: { '^/api': '/rest', '^/remove': '', }, }), ), ); }); it('should have rewritten path from "/api/foo/bar" to "/rest/foo/bar"', async () => { await mockTargetServer.forGet('/rest/foo/bar').thenReply(200, 'HELLO /rest/foo/bar'); const response = await agent.get(`/api/foo/bar`).expect(200); expect(response.text).toBe('HELLO /rest/foo/bar'); }); it('should have removed path from "/remove/api/lipsum" to "/api/lipsum"', async () => { await mockTargetServer.forGet('/api/lipsum').thenReply(200, 'HELLO /api/lipsum'); const response = await agent.get(`/remove/api/lipsum`).expect(200); expect(response.text).toBe('HELLO /api/lipsum'); }); }); describe('express with path + proxy', () => { beforeEach(() => { agent = request( createAppWithPath( '/api', createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}/api` }), ), ); }); it('should proxy to target with the baseUrl', async () => { await mockTargetServer.forGet('/api/foo/bar').thenReply(200, 'HELLO /api/foo/bar'); const response = await agent.get(`/api/foo/bar`).expect(200); expect(response.text).toBe('HELLO /api/foo/bar'); }); }); describe('option.logger', () => { let logMessages: string[]; let customLogger: Logger; beforeEach(() => { logMessages = []; customLogger = { info: (message: string) => logMessages.push(message), warn: (message: string) => logMessages.push(message), error: (message: string) => logMessages.push(message), }; }); it('should have logged messages', async () => { agent = request( createApp( createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, pathFilter: '/api', logger: customLogger, }), ), ); await mockTargetServer.forGet('/api/foo/bar').thenReply(200); await agent.get(`/api/foo/bar`).expect(200); expect(logMessages).not.toBeUndefined(); expect(logMessages.length).toBe(1); expect(logMessages.at(0)).toBe( `[HPM] GET /api/foo/bar -> http://localhost:${mockTargetServer.port}/api/foo/bar [200]`, ); }); it('should have logged messages when router used', async () => { agent = request( createApp( createProxyMiddleware({ router: () => `http://localhost:${mockTargetServer.port}`, pathFilter: '/api', logger: customLogger, }), ), ); await mockTargetServer.forGet('/api/foo/bar').thenReply(200); await agent.get(`/api/foo/bar`).expect(200); expect(logMessages).not.toBeUndefined(); expect(logMessages.length).toBe(1); expect(logMessages.at(0)).toBe( `[HPM] GET /api/foo/bar -> http://localhost:${mockTargetServer.port}/api/foo/bar [200]`, ); }); }); }); }); ================================================ FILE: test/e2e/http-server.spec.ts ================================================ import * as http from 'node:http'; import * as request from 'supertest'; import { createProxyMiddleware } from './test-kit'; describe('http integration', () => { it('should work with raw node http RequestHandler', async () => { const handler = createProxyMiddleware({ changeOrigin: true, 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'); }); }); ================================================ FILE: test/e2e/path-rewriter.spec.ts ================================================ import { Mockttp, getLocal } from 'mockttp'; import * as request from 'supertest'; import { createApp, createProxyMiddleware } from './test-kit'; describe('E2E pathRewrite', () => { let mockTargetServer: Mockttp; beforeEach(async () => { mockTargetServer = getLocal(); await mockTargetServer.start(); }); afterEach(async () => { await mockTargetServer.stop(); }); describe('Rewrite paths with rules table', () => { it('should remove "/foobar" from path', async () => { mockTargetServer .forGet('/api/lorum/ipsum') .thenReply(200, '/API RESPONSE AFTER PATH REWRITE'); const agent = request( createApp( createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, pathRewrite: { '^/foobar/api/': '/api/', }, }), ), ); const response = await agent.get('/foobar/api/lorum/ipsum').expect(200); expect(response.text).toBe('/API RESPONSE AFTER PATH REWRITE'); }); }); describe('Rewrite paths with function', () => { it('should remove "/foobar" from path', async () => { mockTargetServer .forGet('/api/lorum/ipsum') .thenReply(200, '/API RESPONSE AFTER PATH REWRITE FUNCTION'); const agent = request( createApp( createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, pathRewrite(path, req) { return path.replace('/foobar', ''); }, }), ), ); const response = await agent.get('/foobar/api/lorum/ipsum').expect(200); expect(response.text).toBe('/API RESPONSE AFTER PATH REWRITE FUNCTION'); }); }); describe('Rewrite paths with function which return undefined', () => { it('should proxy with requested path', async () => { mockTargetServer .forGet('/api/lorum/ipsum') .thenReply(200, '/API RESPONSE AFTER PATH REWRITE FUNCTION'); const agent = request( createApp( createProxyMiddleware({ target: `http://localhost:${mockTargetServer.port}`, pathRewrite(path, req) { return undefined; }, }), ), ); const response = await agent.get('/api/lorum/ipsum').expect(200); expect(response.text).toBe('/API RESPONSE AFTER PATH REWRITE FUNCTION'); }); }); }); ================================================ FILE: test/e2e/plugins.spec.ts ================================================ import { Mockttp, getLocal } from 'mockttp'; import * as request from 'supertest'; import type { Options, Plugin } from '../../src/types'; import { createApp, createProxyMiddleware } from './test-kit'; describe('E2E Plugins', () => { let mockTargetServer: Mockttp; beforeEach(async () => { mockTargetServer = getLocal(); await mockTargetServer.start(); }); afterEach(async () => { await mockTargetServer.stop(); }); it('should register a plugin and access the http-proxy object', async () => { let proxyReqUrl: string | undefined; let responseStatusCode: number | undefined; mockTargetServer.forGet('/users/1').thenReply(200, '{"userName":"John"}'); const simplePlugin: Plugin = (proxy) => { proxy.on('proxyReq', (proxyReq, req, res, options) => (proxyReqUrl = req.url)); proxy.on('proxyRes', (proxyRes, req, res) => (responseStatusCode = proxyRes.statusCode)); }; const config: Options = { target: `http://localhost:${mockTargetServer.port}`, plugins: [simplePlugin], // register a plugin }; const proxyMiddleware = createProxyMiddleware(config); const app = createApp(proxyMiddleware); const agent = request(app); const response = await agent.get('/users/1').expect(200); expect(proxyReqUrl).toBe('/users/1'); expect(response.text).toBe('{"userName":"John"}'); expect(responseStatusCode).toBe(200); }); }); ================================================ FILE: test/e2e/response-interceptor.spec.ts ================================================ import * as request from 'supertest'; import { createProxyMiddleware, responseInterceptor } from '../../src'; import { createApp } from './test-kit'; describe('responseInterceptor()', () => { let agent: request.Agent; describe('intercept responses', () => { beforeEach(() => { agent = request( createApp( createProxyMiddleware({ target: `http://httpbin.org`, changeOrigin: true, // for vhosted sites, changes host header to match to target's host selfHandleResponse: true, on: { proxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => { res.setHeader('content-type', 'application/json; charset=utf-8'); return JSON.stringify({ foo: 'bar', favorite: '叉燒包' }); }), }, }), ), ); }); it('should return totally different response from http://httpbin.org/json', async () => { const response = await agent.get(`/json`).expect(200); expect(response.body.foo).toEqual('bar'); }); it('should return totally different response from http://httpbin.org/image', async () => { const response = await agent .get(`/image`) .expect('Content-Type', 'application/json; charset=utf-8') .expect(200); expect(response.body.foo).toEqual('bar'); }); it('should support double bytes characters http://httpbin.org/json', async () => { const response = await agent.get(`/json`).expect(200); expect(response.body.favorite).toEqual('叉燒包'); }); }); describe('intercept responses with original headers', () => { beforeEach(() => { agent = request( createApp( createProxyMiddleware({ target: `http://httpbin.org`, changeOrigin: true, // for vhosted sites, changes host header to match to target's host selfHandleResponse: true, on: { proxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => { return responseBuffer; }), }, }), ), ); }); it('should proxy and return original headers from http://httpbin.org/cookies/set/cookie/monster', async () => { return agent .get(`/cookies/set/cookie/monster`) .expect('Access-Control-Allow-Origin', '*') .expect('Date', /.+/) .expect('set-cookie', /.*cookie=monster.*/) .expect(302); }); }); describe('intercept compressed responses', () => { beforeEach(() => { agent = request( createApp( createProxyMiddleware({ target: `http://httpbin.org`, changeOrigin: true, // for vhosted sites, changes host header to match to target's host selfHandleResponse: true, on: { proxyRes: responseInterceptor(async (buffer) => buffer), }, }), ), ); }); it('should return decompressed brotli response http://httpbin.org/brotli', async () => { const response = await agent.get(`/brotli`).expect(200); expect(response.body.brotli).toBe(true); }); it('should return decompressed gzipped response from http://httpbin.org/gzip', async () => { const response = await agent.get(`/gzip`).expect(200); expect(response.body.gzipped).toBe(true); }); it('should return decompressed deflated response from http://httpbin.org/deflate', async () => { const response = await agent.get(`/deflate`).expect(200); expect(response.body.deflated).toBe(true); }); }); }); ================================================ FILE: test/e2e/router.spec.ts ================================================ import { ErrorRequestHandler } from 'express'; import * as getPort from 'get-port'; import { Mockttp, generateCACertificate, getLocal } from 'mockttp'; import * as request from 'supertest'; import { createApp, createAppWithPath, createProxyMiddleware } from './test-kit'; const untrustedCACert = generateCACertificate({ bits: 2048 }); process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; describe('E2E router', () => { let targetServerA: Mockttp; let targetServerB: Mockttp; let targetServerC: Mockttp; let targetPortA: number; let targetPortB: number; let targetPortC: number; beforeEach(async () => { targetServerA = getLocal({ https: await untrustedCACert }); targetServerB = getLocal({ https: await untrustedCACert }); targetServerC = getLocal({ https: await untrustedCACert }); targetPortA = await getPort(); targetPortB = await getPort(); targetPortC = await getPort(); await targetServerA.forAnyRequest().thenPassThrough({ ignoreHostHttpsErrors: ['localhost'] }); await targetServerB.forAnyRequest().thenPassThrough({ ignoreHostHttpsErrors: ['localhost'] }); await targetServerC.forAnyRequest().thenPassThrough({ ignoreHostHttpsErrors: ['localhost'] }); await targetServerA .forAnyRequest() .thenCallback(({ protocol }) => ({ body: protocol === 'https' ? 'A' : 'NOT HTTPS A' })); await targetServerB .forAnyRequest() .thenCallback(({ protocol }) => ({ body: protocol === 'https' ? 'B' : 'NOT HTTPS B' })); await targetServerC .forAnyRequest() .thenCallback(({ protocol }) => ({ body: protocol === 'https' ? 'C' : 'NOT HTTPS C' })); await targetServerA.start(targetPortA); await targetServerB.start(targetPortB); await targetServerC.start(targetPortC); }); afterEach(async () => { await targetServerA.stop(); await targetServerB.stop(); await targetServerC.stop(); }); describe('router with req', () => { it('should work with a string', async () => { const app = createApp( createProxyMiddleware({ target: `https://localhost:${targetPortA}`, secure: false, changeOrigin: true, router(req) { return `https://localhost:${targetPortC}`; }, }), ); const agent = request(app); const response = await agent.get('/api').expect(200); expect(response.text).toBe('C'); }); it('should work with an object', async () => { const app = createApp( createProxyMiddleware({ target: `https://localhost:${targetPortA}`, secure: false, changeOrigin: true, router(req) { return { host: 'localhost', port: targetPortC, protocol: 'https:' }; }, }), ); const agent = request(app); const response = await agent.get('/api').expect(200); expect(response.text).toBe('C'); }); it('should work with an async callback', async () => { const app = createApp( createProxyMiddleware({ target: `https://localhost:${targetPortA}`, secure: false, changeOrigin: true, router: async (req) => { return new Promise((resolve) => resolve({ host: 'localhost', port: targetPortC, protocol: 'https:' }), ); }, }), ); const agent = request(app); const response = await agent.get('/api').expect(200); expect(response.text).toBe('C'); }); it('should handle promise rejection in router', async () => { const app = createApp( createProxyMiddleware({ target: `https://localhost:${targetPortA}`, secure: false, changeOrigin: true, router: async (req) => { throw new Error('An error thrown in the router'); }, }), ); const errorHandler: ErrorRequestHandler = (err: Error, req, res, next) => { res.status(502).send(err.message); }; app.use(errorHandler); const agent = request(app); const response = await agent.get('/api').expect(502); expect(response.text).toBe('An error thrown in the router'); }); it('missing a : will cause it to use http', async () => { const app = createApp( createProxyMiddleware({ target: `https://localhost:${targetPortA}`, secure: false, changeOrigin: true, router: async (req) => { return new Promise((resolve) => resolve({ host: 'localhost', port: targetPortC, protocol: 'https' }), ); }, }), ); const agent = request(app); const response = await agent.get('/api').expect(200); expect(response.text).toBe('NOT HTTPS C'); }); }); describe('router with proxyTable', () => { let agent; beforeEach(() => { const app = createAppWithPath( '/', createProxyMiddleware({ target: `https://localhost:${targetPortA}`, secure: false, changeOrigin: true, router: { 'alpha.localhost:6000': `https://localhost:${targetPortA}`, 'beta.localhost:6000': `https://localhost:${targetPortB}`, 'localhost:6000/api': `https://localhost:${targetPortC}`, }, }), ); agent = request(app); }); it('should proxy to option.target', async () => { const response = await agent.get('/api').expect(200); expect(response.text).toBe('A'); }); it('should proxy when host is "alpha.localhost"', async () => { const response = await agent.get('/api').set('host', 'alpha.localhost:6000').expect(200); expect(response.text).toBe('A'); }); it('should proxy when host is "beta.localhost"', async () => { const response = await agent.get('/api').set('host', 'beta.localhost:6000').expect(200); expect(response.text).toBe('B'); }); it('should proxy with host & path config: "localhost:6000/api"', async () => { const response = await agent.get('/api').set('host', 'localhost:6000').expect(200); expect(response.text).toBe('C'); }); }); }); ================================================ FILE: test/e2e/test-kit.ts ================================================ import * as express from 'express'; import type { Express, RequestHandler } from 'express'; export { createProxyMiddleware, responseInterceptor, fixRequestBody } from '../../src/index'; export function createApp(...middlewares): Express { const app = express(); app.use(...middlewares); return app; } export function createAppWithPath(path: string | string[], middleware: RequestHandler): Express { const app = express(); app.use(path, middleware); return app; } ================================================ FILE: test/e2e/websocket.spec.ts ================================================ import * as http from 'node:http'; import * as getPort from 'get-port'; import { WebSocket, WebSocketServer } from 'ws'; import type { RequestHandler } from '../../src/types'; import { createApp, createProxyMiddleware } from './test-kit'; /******************************************************************** * - Not possible to use `supertest` to test WebSockets * - Make sure to use different port for each test to avoid flakiness ********************************************************************/ describe('E2E WebSocket proxy', () => { let proxyServer: http.Server; let ws: WebSocket; let wss: WebSocketServer; let proxyMiddleware: RequestHandler; let WS_SERVER_PORT: number; let SERVER_PORT: number; beforeEach(async () => { WS_SERVER_PORT = await getPort(); SERVER_PORT = await getPort(); wss = new WebSocketServer({ port: WS_SERVER_PORT }); wss.on('connection', (websocket) => { websocket.on('message', (data, isBinary) => { const message = isBinary ? data : data.toString(); websocket.send(message); // echo received message }); }); }); beforeEach(() => { proxyMiddleware = createProxyMiddleware({ target: `http://localhost:${WS_SERVER_PORT}`, ws: true, pathRewrite: { '^/socket': '' }, }); }); afterEach(async () => { return Promise.all([ new Promise((resolve) => proxyServer.close(resolve)), new Promise((resolve) => wss.close(resolve)), new Promise((resolve) => resolve(ws.close())), ]); }); describe('option.ws', () => { beforeEach(async () => { proxyServer = createApp(proxyMiddleware).listen(SERVER_PORT); // quick & dirty Promise version of http.get (don't care about correctness) const get = async (uri) => new Promise((resolve, reject) => http.get(uri, resolve)); // need to make a normal http request, so http-proxy-middleware can catch the upgrade request await get(`http://localhost:${SERVER_PORT}/`); // do a second http request to make sure only 1 listener subscribes to upgrade request await get(`http://localhost:${SERVER_PORT}/`); return new Promise((resolve) => { ws = new WebSocket(`ws://localhost:${SERVER_PORT}/socket`); ws.on('open', resolve); }); }); it('should proxy to path', (done) => { ws.on('message', (data, isBinary) => { const message = isBinary ? data : data.toString(); expect(message).toBe('foobar'); done(); }); ws.send('foobar'); }); }); describe('option.ws with external server "upgrade"', () => { beforeEach((done) => { proxyServer = createApp(proxyMiddleware).listen(SERVER_PORT); proxyServer.on('upgrade', proxyMiddleware.upgrade); ws = new WebSocket(`ws://localhost:${SERVER_PORT}/socket`); ws.on('open', done); }); it('should proxy to path', (done) => { ws.on('message', (data, isBinary) => { const message = isBinary ? data : data.toString(); expect(message).toBe('foobar'); done(); }); ws.send('foobar'); }); }); describe('with router and pathRewrite', () => { beforeEach(() => { const proxyMiddleware = createProxyMiddleware({ // cSpell:ignore notworkinghost target: 'ws://notworkinghost:6789', router: { '/socket': `ws://localhost:${WS_SERVER_PORT}` }, pathRewrite: { '^/socket': '' }, }); proxyServer = createApp(proxyMiddleware).listen(SERVER_PORT); proxyServer.on('upgrade', proxyMiddleware.upgrade); }); beforeEach((done) => { ws = new WebSocket(`ws://localhost:${SERVER_PORT}/socket`); ws.on('open', done); }); it('should proxy to path', (done) => { ws.on('message', (data, isBinary) => { const message = isBinary ? data : data.toString(); expect(message).toBe('foobar'); done(); }); ws.send('foobar'); }); }); describe('with error in router', () => { beforeEach(() => { const proxyMiddleware = createProxyMiddleware({ // cSpell:ignore notworkinghost target: `http://notworkinghost:6789`, router: async () => { throw new Error('error'); }, }); proxyServer = createApp(proxyMiddleware).listen(SERVER_PORT); proxyServer.on('upgrade', proxyMiddleware.upgrade); }); it('should handle error', (done) => { ws = new WebSocket(`ws://localhost:${SERVER_PORT}/socket`); ws.on('error', (err) => { expect(err).toBeTruthy(); done(); }); }); }); }); ================================================ FILE: test/legacy/http-proxy-middleware.spec.ts ================================================ import { Mockttp, getLocal } from 'mockttp'; import * as request from 'supertest'; import { LegacyOptions, legacyCreateProxyMiddleware } from '../../src'; import { createApp, createAppWithPath } from '../e2e/test-kit'; describe('legacyCreateProxyMiddleware()', () => { const mockServer: Mockttp = getLocal(); beforeEach(() => mockServer.start()); afterEach(() => mockServer.stop()); it('should throw when short hand is used', () => { expect(() => legacyCreateProxyMiddleware(`http://localhost:${mockServer.port}`)) .toThrowErrorMatchingInlineSnapshot(` "Shorthand syntax is removed from legacyCreateProxyMiddleware(). Please use "legacyCreateProxyMiddleware({ target: 'http://www.example.org' })" instead. More details: https://github.com/chimurai/http-proxy-middleware/blob/master/MIGRATION.md#removed-shorthand-usage " `); }); it('should expose external websocket upgrade handler', () => { const proxyMiddleware = legacyCreateProxyMiddleware({ target: `http://localhost:${mockServer.port}`, }); expect(proxyMiddleware.upgrade).toBeDefined(); }); it('should proxy to /users', async () => { mockServer.forGet('/users').thenReply(200, 'legacy OK'); const proxyMiddleware = legacyCreateProxyMiddleware({ target: `http://localhost:${mockServer.port}`, }); const app = createApp(proxyMiddleware); const response = await request(app).get('/users').expect(200); expect(response.text).toBe('legacy OK'); }); it('should proxy with old context option', async () => { mockServer.forGet('/admin/users').thenReply(200, 'legacy OK'); const proxyMiddleware = legacyCreateProxyMiddleware('/admin', { target: `http://localhost:${mockServer.port}`, }); const app = createApp(proxyMiddleware); const response = await request(app).get('/admin/users').expect(200); expect(response.text).toBe('legacy OK'); }); it('should proxy to /users with legacy patched req.url behavior', async () => { mockServer.forGet('/users').thenReply(200, 'legacy OK'); const proxyMiddleware = legacyCreateProxyMiddleware({ target: `http://localhost:${mockServer.port}`, }); const app = createAppWithPath('/users', proxyMiddleware); const response = await request(app).get('/users').expect(200); expect(response.text).toBe('legacy OK'); }); it('should fail to proxy to /users with legacy patched req.url behavior', async () => { mockServer.forGet('/users').thenReply(200, 'legacy OK'); const proxyMiddleware = legacyCreateProxyMiddleware({ target: `http://localhost:${mockServer.port}/users`, }); const app = createAppWithPath('/users', proxyMiddleware); await request(app).get('/users').expect(503); }); it('should respond with legacy onError handler and log error', async () => { mockServer.forGet('/users').thenCloseConnection(); const mockLogger = { info: jest.fn(), warn: jest.fn(), error: jest.fn(), }; const legacyOptions: LegacyOptions = { target: `http://localhost:${mockServer.port}`, logLevel: 'error', logProvider: () => mockLogger, onError(err, req, res) { res.status(500).send('my legacy error'); }, }; const proxyMiddleware = legacyCreateProxyMiddleware(legacyOptions); const app = createAppWithPath('/users', proxyMiddleware); const response = await request(app).get('/users').expect(500); expect(response.text).toBe('my legacy error'); expect(mockLogger.error).toHaveBeenCalledTimes(1); expect(mockLogger.warn).toHaveBeenCalledTimes(2); }); }); ================================================ FILE: test/types.spec.ts ================================================ /* eslint-disable @typescript-eslint/no-unused-expressions */ import * as http from 'node:http'; import * as express from 'express'; import { fixRequestBody, createProxyMiddleware as middleware, responseInterceptor } from '../src'; import type { Options, RequestHandler } from '../src/types'; describe('http-proxy-middleware TypeScript Types', () => { let options: Options; beforeEach(() => { options = { target: 'http://example.org', }; }); 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', () => { it('should create proxy with path filter', () => { const proxy = middleware({ ...options, pathFilter: '/api' }); expect(proxy).toBeDefined(); }); it('should create proxy with glob filter', () => { const proxy = middleware({ ...options, pathFilter: ['/path/**'] }); expect(proxy).toBeDefined(); }); it('should create proxy with custom filter', () => { const proxy = middleware({ ...options, pathFilter: (path, req) => true }); expect(proxy).toBeDefined(); }); it('should create proxy with manual websocket upgrade function', () => { const proxy = middleware({ ...options, pathFilter: (path, req) => true }); expect(proxy.upgrade).toBeDefined(); }); }); describe('http-proxy options', () => { it('should extend from http-proxy options', () => { options = { target: 'http://example', ws: true, }; expect(options).toBeDefined(); }); }); describe('http-proxy-middleware options', () => { describe('pathRewrite', () => { it('should have pathRewrite Type with table', () => { options = { pathRewrite: { '^/from': '/to' } }; expect(options).toBeDefined(); }); it('should have pathRewrite Type with function', () => { options = { pathRewrite: (path, req) => '/path' }; expect(options).toBeDefined(); }); it('should have pathRewrite Type with async function', () => { options = { pathRewrite: async (path, req) => '/path' }; expect(options).toBeDefined(); }); }); describe('router', () => { it('should have router Type with table', () => { options = { router: { '^/from': '/to' } }; expect(options).toBeDefined(); }); it('should have router Type with function', () => { options = { router: (path) => '/path' }; expect(options).toBeDefined(); }); it('should have router Type with async function', () => { options = { router: async (path) => '/path' }; expect(options).toBeDefined(); }); }); describe('logger', () => { it('should have logger option', () => { options = { logger: console }; expect(options).toBeDefined(); }); it('should allow custom logger option', () => { const customLogger = { info: () => {}, warn: () => {}, error: () => {}, }; options = { logger: customLogger }; expect(options).toBeDefined(); }); it('should fail when custom logger has missing log function', () => { const customLogger = { info: () => {}, // warn: () => {}, error: () => {}, }; // @ts-expect-error explanation: should error when customLogger has a missing log function options = { logger: customLogger }; expect(options).toBeDefined(); }); it('should fail when invalid logger is provided', () => { // @ts-expect-error explanation: should error when invalid logger is provided options = { logger: 500 }; expect(options).toBeDefined(); }); }); describe('on', () => { it('should have on events', () => { options = { on: { error: (error, req, res, target) => {}, proxyReq: (proxyReq, req, res, options) => {}, proxyReqWs: (proxyReq, req, socket, options) => {}, proxyRes: (proxyRes, req, res) => {}, open: (proxySocket) => {}, close: (proxyRes, proxySocket, proxyHead) => {}, start: (req, res, target) => {}, end: (req, res, proxyRes) => {}, econnreset: (error, req, res, target) => {}, // @ts-expect-error explanation: should error when unknown event is passed unknownEventName: () => {}, }, }; expect(options).toBeDefined(); }); }); }); describe('express request and response types', () => { it('should get TypeScript type errors when express specific properties are used with base types', () => { options = { on: { proxyReq(proxyReq, req, res, options) { // @ts-expect-error explanation: should error when express properties are used req.params; }, proxyRes(proxyRes, req, res) { // @ts-expect-error explanation: should error when express properties are used res.status(200).send('OK'); }, }, }; 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; // @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; // 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 get contextual types from express server', () => { const app = express(); app.use( '/', // FIXME: contextual types should work with express path middleware (without providing explicit types) 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; // @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; 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; // @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; 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'); // @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; return buffer; }), ); }, ], }), ); expect(app).toBeDefined(); }); }); }); ================================================ FILE: test/unit/configuration.spec.ts ================================================ import { verifyConfig } from '../../src/configuration'; describe('configFactory', () => { describe('verifyConfig()', () => { describe('missing option.target', () => { let fn; beforeEach(() => { fn = () => { verifyConfig({ pathFilter: '/api' }); }; }); it('should throw an error when target and router option are missing', () => { expect(fn).toThrow(Error); }); }); describe('optional option.target when option.router is used', () => { let fn; beforeEach(() => { fn = () => { verifyConfig({ pathFilter: '/api', router: (req) => 'http://www.example.com', }); }; }); it('should not throw an error when target option is missing when router is used', () => { expect(fn).not.toThrow(Error); }); }); }); }); ================================================ FILE: test/unit/fix-request-body.spec.ts ================================================ import { ClientRequest, IncomingMessage, ServerResponse } from 'node:http'; import { Socket } from 'node:net'; import * as querystring from 'node:querystring'; import { BodyParserLikeRequest, fixRequestBody } from '../../src/handlers/fix-request-body'; const fakeProxyRequest = (): ClientRequest => { const proxyRequest = new ClientRequest('http://some-host'); proxyRequest.emit = jest.fn(); return proxyRequest; }; const fakeProxyResponse = (): ServerResponse => { const res = new ServerResponse(new IncomingMessage(new Socket())); return res; }; const createRequestWithBody = (body: unknown): BodyParserLikeRequest => { const req = new IncomingMessage(new Socket()) as BodyParserLikeRequest; req.url = '/test_path'; req.body = body; return req; }; const handlerFormDataBodyData = (contentType: string, data: { [key: string]: any }) => { const boundary = contentType.replace(/^.*boundary=(.*)$/, '$1'); let str = ''; for (const [key, value] of Object.entries(data)) { str += `--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${value}\r\n`; } return str; }; describe('fixRequestBody', () => { it('should not write when body is undefined', () => { const proxyRequest = fakeProxyRequest(); jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); fixRequestBody(proxyRequest, createRequestWithBody(undefined)); expect(proxyRequest.setHeader).not.toHaveBeenCalled(); expect(proxyRequest.write).not.toHaveBeenCalled(); }); it('should write when body is an empty JSON object', () => { const proxyRequest = fakeProxyRequest(); proxyRequest.setHeader('content-type', 'application/json; charset=utf-8'); jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); fixRequestBody(proxyRequest, createRequestWithBody({})); expect(proxyRequest.setHeader).toHaveBeenCalled(); expect(proxyRequest.write).toHaveBeenCalled(); }); it('should write when body is not empty and Content-Type is text/plain', () => { const proxyRequest = fakeProxyRequest(); proxyRequest.setHeader('content-type', 'text/plain; charset=utf-8'); jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); fixRequestBody(proxyRequest, createRequestWithBody('some string')); const expectedBody = 'some string'; expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length); expect(proxyRequest.write).toHaveBeenCalledWith(expectedBody); }); it('should write when body is not empty and Content-Type is application/json', () => { const proxyRequest = fakeProxyRequest(); proxyRequest.setHeader('content-type', 'application/json; charset=utf-8'); jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); fixRequestBody(proxyRequest, createRequestWithBody({ someField: 'some value' })); const expectedBody = JSON.stringify({ someField: 'some value' }); expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length); expect(proxyRequest.write).toHaveBeenCalledWith(expectedBody); }); it('should write when body is not empty and Content-Type is multipart/form-data', () => { const proxyRequest = fakeProxyRequest(); proxyRequest.setHeader('content-type', 'multipart/form-data'); jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); fixRequestBody(proxyRequest, createRequestWithBody({ someField: 'some value' })); const expectedBody = handlerFormDataBodyData('multipart/form-data', { someField: 'some value', }); expect(expectedBody).toMatchInlineSnapshot(` "--multipart/form-data Content-Disposition: form-data; name="someField" some value " `); expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length); expect(proxyRequest.write).toHaveBeenCalledWith(expectedBody); }); it('should write when body is not empty and Content-Type includes multipart/form-data', () => { const proxyRequest = fakeProxyRequest(); proxyRequest.setHeader('content-type', 'multipart/form-data'); jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); fixRequestBody(proxyRequest, createRequestWithBody({ someField: 'some value' })); const expectedBody = handlerFormDataBodyData('multipart/form-data', { someField: 'some value', }); expect(expectedBody).toMatchInlineSnapshot(` "--multipart/form-data Content-Disposition: form-data; name="someField" some value " `); expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length); expect(proxyRequest.write).toHaveBeenCalledWith(expectedBody); }); it('should write when body is not empty and Content-Type ends with +json', () => { const proxyRequest = fakeProxyRequest(); proxyRequest.setHeader('content-type', 'application/merge-patch+json; charset=utf-8'); jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); fixRequestBody(proxyRequest, createRequestWithBody({ someField: 'some value' })); const expectedBody = JSON.stringify({ someField: 'some value' }); expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length); expect(proxyRequest.write).toHaveBeenCalledWith(expectedBody); }); it('should write when body is not empty and Content-Type is application/x-www-form-urlencoded', () => { const proxyRequest = fakeProxyRequest(); proxyRequest.setHeader('content-type', 'application/x-www-form-urlencoded'); jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); fixRequestBody(proxyRequest, createRequestWithBody({ someField: 'some value' })); const expectedBody = querystring.stringify({ someField: 'some value' }); expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length); expect(proxyRequest.write).toHaveBeenCalledWith(expectedBody); }); it('should write when body is not empty and Content-Type includes application/x-www-form-urlencoded', () => { const proxyRequest = fakeProxyRequest(); proxyRequest.setHeader('content-type', 'application/x-www-form-urlencoded; charset=UTF-8'); jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); fixRequestBody(proxyRequest, createRequestWithBody({ someField: 'some value' })); const expectedBody = querystring.stringify({ someField: 'some value' }); expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length); expect(proxyRequest.write).toHaveBeenCalledWith(expectedBody); }); it('should parse json and call write() once with incorrect content-type application/x-www-form-urlencoded+json', () => { const proxyRequest = fakeProxyRequest(); proxyRequest.setHeader('content-type', 'application/x-www-form-urlencoded+json'); jest.spyOn(proxyRequest, 'setHeader'); jest.spyOn(proxyRequest, 'write'); fixRequestBody(proxyRequest, createRequestWithBody({ someField: 'some value' })); const expectedBody = JSON.stringify({ someField: 'some value' }); expect(proxyRequest.setHeader).toHaveBeenCalledWith('Content-Length', expectedBody.length); expect(proxyRequest.write).toHaveBeenCalledTimes(1); expect(proxyRequest.write).toHaveBeenCalledWith(expectedBody); }); it('should not fixRequestBody() when there bodyParser fails', () => { const proxyRequest = fakeProxyRequest(); const request = { get readableLength() { return 4444; // simulate bodyParser failure }, } as BodyParserLikeRequest; const proxyResponse = fakeProxyResponse(); proxyRequest.setHeader('content-type', 'application/x-www-form-urlencoded'); jest.spyOn(proxyRequest, 'write'); jest.spyOn(proxyRequest, 'destroy'); jest.spyOn(proxyResponse, 'writeHead'); jest.spyOn(proxyResponse, 'end'); fixRequestBody(proxyRequest, request); expect(proxyResponse.end).toHaveBeenCalledTimes(0); expect(proxyRequest.write).toHaveBeenCalledTimes(0); expect(proxyRequest.destroy).toHaveBeenCalledTimes(0); }); }); ================================================ FILE: test/unit/get-plugins.spec.ts ================================================ import { debugProxyErrorsPlugin, errorResponsePlugin, loggerPlugin, proxyEventsPlugin, } from '../../src'; import { getPlugins } from '../../src/get-plugins'; import { Plugin } from '../../src/types'; describe('getPlugins', () => { let plugins: Plugin[]; it('should return default plugins when no user plugins are provided', () => { plugins = getPlugins({}); expect(plugins).toHaveLength(4); expect(plugins.map((plugin) => plugin.name)).toMatchInlineSnapshot(` [ "debugProxyErrorsPlugin", "proxyEventsPlugin", "loggerPlugin", "errorResponsePlugin", ] `); }); it('should return no plugins when ejectPlugins is configured in option', () => { plugins = getPlugins({ ejectPlugins: true, }); expect(plugins).toHaveLength(0); }); it('should return user plugins with default plugins when user plugins are provided', () => { const myPlugin: Plugin = () => { /* noop */ }; plugins = getPlugins({ plugins: [myPlugin], }); expect(plugins).toHaveLength(5); expect(plugins.map((plugin) => plugin.name)).toMatchInlineSnapshot(` [ "debugProxyErrorsPlugin", "proxyEventsPlugin", "loggerPlugin", "errorResponsePlugin", "myPlugin", ] `); }); it('should only return user plugins when user plugins are provided with ejectPlugins option', () => { const myPlugin: Plugin = () => { /* noop */ }; plugins = getPlugins({ ejectPlugins: true, plugins: [myPlugin], }); expect(plugins).toHaveLength(1); expect(plugins.map((plugin) => plugin.name)).toMatchInlineSnapshot(` [ "myPlugin", ] `); }); it('should return manually added default plugins in different order after using ejectPlugins', () => { plugins = getPlugins({ ejectPlugins: true, plugins: [debugProxyErrorsPlugin, errorResponsePlugin, loggerPlugin, proxyEventsPlugin], // alphabetical order }); expect(plugins).toHaveLength(4); expect(plugins.map((plugin) => plugin.name)).toMatchInlineSnapshot(` [ "debugProxyErrorsPlugin", "errorResponsePlugin", "loggerPlugin", "proxyEventsPlugin", ] `); }); it('should not configure errorResponsePlugin when user specifies their own error handler', () => { const myErrorHandler = () => { /* noop */ }; plugins = getPlugins({ on: { error: myErrorHandler, }, }); expect(plugins).toHaveLength(3); expect(plugins.map((plugin) => plugin.name)).toMatchInlineSnapshot(` [ "debugProxyErrorsPlugin", "proxyEventsPlugin", "loggerPlugin", ] `); }); }); ================================================ FILE: test/unit/logger.spec.ts ================================================ import { getLogger } from '../../src/logger'; describe('Logger', () => { it('should return global "console" logger when configured in Options', () => { const logger = getLogger({ logger: console }); expect(logger).toBe(console); }); it('should return noop logger when not configured in Options', () => { const logger = getLogger({}); expect(Object.keys(logger)).toMatchInlineSnapshot(` [ "info", "warn", "error", ] `); }); }); ================================================ FILE: test/unit/path-filter.spec.ts ================================================ import type * as http from 'node:http'; import { matchPathFilter } from '../../src/path-filter'; describe('Path Filter', () => { const fakeReq = {} as http.IncomingMessage; describe('String path matching', () => { let result; describe('Single path matching', () => { it('should match all paths', () => { result = matchPathFilter('', 'http://localhost/api/foo/bar', fakeReq); expect(result).toBe(true); }); it('should match all paths starting with forward-slash', () => { result = matchPathFilter('/', 'http://localhost/api/foo/bar', fakeReq); expect(result).toBe(true); }); it('should return true when the pathFilter is present in url', () => { result = matchPathFilter('/api', 'http://localhost/api/foo/bar', fakeReq); expect(result).toBe(true); }); it('should return false when the pathFilter is not present in url', () => { result = matchPathFilter('/abc', 'http://localhost/api/foo/bar', fakeReq); expect(result).toBe(false); }); it('should return false when the pathFilter is present half way in url', () => { result = matchPathFilter('/foo', 'http://localhost/api/foo/bar', fakeReq); expect(result).toBe(false); }); it('should return false when the pathFilter does not start with /', () => { result = matchPathFilter('api', 'http://localhost/api/foo/bar', fakeReq); expect(result).toBe(false); }); }); describe('Multi path matching', () => { it('should return true when the pathFilter is present in url', () => { result = matchPathFilter(['/api'], 'http://localhost/api/foo/bar', fakeReq); expect(result).toBe(true); }); it('should return true when the pathFilter is present in url', () => { result = matchPathFilter(['/api', '/ajax'], 'http://localhost/ajax/foo/bar', fakeReq); expect(result).toBe(true); }); it('should return false when the pathFilter does not match url', () => { result = matchPathFilter(['/api', '/ajax'], 'http://localhost/foo/bar', fakeReq); expect(result).toBe(false); }); it('should return false when empty array provided', () => { result = matchPathFilter([], 'http://localhost/api/foo/bar', fakeReq); expect(result).toBe(false); }); }); }); describe('Wildcard path matching', () => { describe('Single glob', () => { let url; beforeEach(() => { url = 'http://localhost/api/foo/bar.html'; }); describe('url-path matching', () => { it('should match any path', () => { expect(matchPathFilter('**', url, fakeReq)).toBe(true); expect(matchPathFilter('/**', url, fakeReq)).toBe(true); }); it('should only match paths starting with "/api" ', () => { expect(matchPathFilter('/api/**', url, fakeReq)).toBe(true); expect(matchPathFilter('/ajax/**', url, fakeReq)).toBe(false); }); it('should only match paths starting with "foo" folder in it ', () => { expect(matchPathFilter('**/foo/**', url, fakeReq)).toBe(true); expect(matchPathFilter('**/invalid/**', url, fakeReq)).toBe(false); }); }); describe('file matching', () => { it('should match any path, file and extension', () => { expect(matchPathFilter('**', url, fakeReq)).toBe(true); expect(matchPathFilter('**/*', url, fakeReq)).toBe(true); expect(matchPathFilter('**/*.*', url, fakeReq)).toBe(true); expect(matchPathFilter('/**', url, fakeReq)).toBe(true); expect(matchPathFilter('/**/*', url, fakeReq)).toBe(true); expect(matchPathFilter('/**/*.*', url, fakeReq)).toBe(true); }); it('should only match .html files', () => { expect(matchPathFilter('**/*.html', url, fakeReq)).toBe(true); expect(matchPathFilter('/**/*.html', url, fakeReq)).toBe(true); expect(matchPathFilter('/*.htm', url, fakeReq)).toBe(false); expect(matchPathFilter('/*.jpg', url, fakeReq)).toBe(false); }); it('should only match .html under root path', () => { const pattern = '/*.html'; expect(matchPathFilter(pattern, 'http://localhost/index.html', fakeReq)).toBe(true); expect(matchPathFilter(pattern, 'http://localhost/some/path/index.html', fakeReq)).toBe( false, ); }); it('should ignore query params', () => { expect(matchPathFilter('/**/*.php', 'http://localhost/a/b/c.php?d=e&e=f', fakeReq)).toBe( true, ); expect( matchPathFilter('/**/*.php?*', 'http://localhost/a/b/c.php?d=e&e=f', fakeReq), ).toBe(false); }); it('should only match any file in root path', () => { expect(matchPathFilter('/*', 'http://localhost/bar.html', fakeReq)).toBe(true); expect(matchPathFilter('/*.*', 'http://localhost/bar.html', fakeReq)).toBe(true); expect(matchPathFilter('/*', 'http://localhost/foo/bar.html', fakeReq)).toBe(false); }); it('should only match .html file is in root path', () => { expect(matchPathFilter('/*.html', 'http://localhost/bar.html', fakeReq)).toBe(true); expect(matchPathFilter('/*.html', 'http://localhost/api/foo/bar.html', fakeReq)).toBe( false, ); }); it('should only match .html files in "foo" folder', () => { expect(matchPathFilter('**/foo/*.html', url, fakeReq)).toBe(true); expect(matchPathFilter('**/bar/*.html', url, fakeReq)).toBe(false); }); it('should not match .html files', () => { expect(matchPathFilter('!**/*.html', url, fakeReq)).toBe(false); }); }); }); describe('Multi glob matching', () => { describe('Multiple patterns', () => { it('should return true when both path patterns match', () => { const pattern = ['/api/**', '/ajax/**']; expect(matchPathFilter(pattern, 'http://localhost/api/foo/bar.json', fakeReq)).toBe(true); expect(matchPathFilter(pattern, 'http://localhost/ajax/foo/bar.json', fakeReq)).toBe( true, ); expect(matchPathFilter(pattern, 'http://localhost/rest/foo/bar.json', fakeReq)).toBe( false, ); }); it('should return true when both file extensions pattern match', () => { const pattern = ['/**/*.html', '/**/*.jpeg']; expect(matchPathFilter(pattern, 'http://localhost/api/foo/bar.html', fakeReq)).toBe(true); expect(matchPathFilter(pattern, 'http://localhost/api/foo/bar.jpeg', fakeReq)).toBe(true); expect(matchPathFilter(pattern, 'http://localhost/api/foo/bar.gif', fakeReq)).toBe(false); }); }); describe('Negation patterns', () => { it('should not match file extension', () => { const url = 'http://localhost/api/foo/bar.html'; expect(matchPathFilter(['**', '!**/*.html'], url, fakeReq)).toBe(false); expect(matchPathFilter(['**', '!**/*.json'], url, fakeReq)).toBe(true); }); }); }); }); describe('Use function for matching', () => { const testFunctionAsPathFilter = (val) => { return matchPathFilter(fn, 'http://localhost/api/foo/bar', fakeReq); function fn(path, req) { return val; } }; describe('truthy', () => { it('should match when function returns true', () => { expect(testFunctionAsPathFilter(true)).toBeTruthy(); expect(testFunctionAsPathFilter('true')).toBeTruthy(); }); }); describe('falsy', () => { it('should not match when function returns falsy value', () => { expect(testFunctionAsPathFilter(undefined)).toBeFalsy(); expect(testFunctionAsPathFilter(false)).toBeFalsy(); expect(testFunctionAsPathFilter('')).toBeFalsy(); }); }); }); describe('Test invalid pathFilters', () => { let testPathFilter; beforeEach(() => { testPathFilter = (pathFilter) => { return () => { matchPathFilter(pathFilter, 'http://localhost/api/foo/bar', fakeReq); }; }; }); describe('Throw error', () => { it('should throw error with null', () => { expect(testPathFilter(null)).toThrow(Error); }); it('should throw error with object literal', () => { expect(testPathFilter(fakeReq)).toThrow(Error); }); it('should throw error with integers', () => { expect(testPathFilter(123)).toThrow(Error); }); it('should throw error with mixed string and glob pattern', () => { expect(testPathFilter(['/api', '!*.html'])).toThrow(Error); }); }); describe('Do not throw error', () => { it('should not throw error with string', () => { expect(testPathFilter('/123')).not.toThrow(Error); }); it('should not throw error with Array', () => { expect(testPathFilter(['/123'])).not.toThrow(Error); }); it('should not throw error with glob', () => { expect(testPathFilter('/**')).not.toThrow(Error); }); it('should not throw error with Array of globs', () => { expect(testPathFilter(['/**', '!*.html'])).not.toThrow(Error); }); it('should not throw error with Function', () => { expect(testPathFilter(() => {})).not.toThrow(Error); }); }); }); }); ================================================ FILE: test/unit/path-rewriter.spec.ts ================================================ import { createPathRewriter } from '../../src/path-rewriter'; describe('Path rewriting', () => { let rewriter; let result; let config; describe('Rewrite rules configuration and usage', () => { beforeEach(() => { config = { '^/api/old': '/api/new', '^/remove': '', invalid: 'path/new', '/valid': '/path/new', '/some/specific/path': '/awe/some/specific/path', '/some': '/awe/some', }; }); beforeEach(() => { rewriter = createPathRewriter(config); }); it('should rewrite path', () => { result = rewriter('/api/old/index.json'); expect(result).toBe('/api/new/index.json'); }); it('should remove path', () => { result = rewriter('/remove/old/index.json'); expect(result).toBe('/old/index.json'); }); it('should leave path intact', () => { result = rewriter('/foo/bar/index.json'); expect(result).toBe('/foo/bar/index.json'); }); it('should not rewrite path when config-key does not match url with test(regex)', () => { result = rewriter('/invalid/bar/foo.json'); expect(result).toBe('/path/new/bar/foo.json'); expect(result).not.toBe('/invalid/new/bar/foo.json'); }); it('should rewrite path when config-key does match url with test(regex)', () => { result = rewriter('/valid/foo/bar.json'); expect(result).toBe('/path/new/foo/bar.json'); }); it('should return first match when similar paths are configured', () => { result = rewriter('/some/specific/path/bar.json'); expect(result).toBe('/awe/some/specific/path/bar.json'); }); }); describe('Rewrite rule: add base path to requests', () => { beforeEach(() => { config = { '^/': '/extra/base/path/', }; }); beforeEach(() => { rewriter = createPathRewriter(config); }); it('should add base path to requests', () => { result = rewriter('/api/books/123'); expect(result).toBe('/extra/base/path/api/books/123'); }); }); describe('Rewrite function', () => { beforeEach(() => { rewriter = (fn) => { const rewriteFn = createPathRewriter(fn); const requestPath = '/123/456'; return rewriteFn(requestPath); }; }); it('should return unmodified path', () => { const rewriteFn = (path) => { return path; }; expect(rewriter(rewriteFn)).toBe('/123/456'); }); it('should return alternative path', () => { const rewriteFn = (path) => { return '/foo/bar'; }; expect(rewriter(rewriteFn)).toBe('/foo/bar'); }); it('should return replaced path', () => { const rewriteFn = (path) => { return path.replace('/456', '/789'); }; expect(rewriter(rewriteFn)).toBe('/123/789'); }); // Same tests as the above three, but async it('is async and should return unmodified path', () => { const rewriteFn = async (path) => { const promise = new Promise((resolve, reject) => { resolve(path); }); const changed = await promise; return changed; }; return expect(rewriter(rewriteFn)).resolves.toBe('/123/456'); }); it('is async and should return alternative path', () => { const rewriteFn = async (path) => { const promise = new Promise((resolve, reject) => { resolve('/foo/bar'); }); const changed = await promise; return changed; }; return expect(rewriter(rewriteFn)).resolves.toBe('/foo/bar'); }); it('is async and should return replaced path', () => { const rewriteFn = async (path) => { const promise = new Promise((resolve, reject) => { resolve(path.replace('/456', '/789')); }); const changed = await promise; return changed; }; return expect(rewriter(rewriteFn)).resolves.toBe('/123/789'); }); }); describe('Invalid configuration', () => { let badFn; beforeEach(() => { badFn = (cfg) => { return () => { createPathRewriter(cfg); }; }; }); it('should return undefined when no config is provided', () => { expect(badFn()()).toBeUndefined(); expect(badFn(null)()).toBeUndefined(); expect(badFn(undefined)()).toBeUndefined(); }); it('should throw when bad config is provided', () => { expect(badFn(123)).toThrow(Error); expect(badFn('abc')).toThrow(Error); expect(badFn([])).toThrow(Error); expect(badFn([1, 2, 3])).toThrow(Error); }); it('should not throw when empty Object config is provided', () => { expect(badFn({})).not.toThrow(Error); }); it('should not throw when function config is provided', () => { expect(badFn(() => {})).not.toThrow(Error); }); it('should not throw when async function config is provided', () => { expect(badFn(async () => {})).not.toThrow(Error); }); }); }); ================================================ FILE: test/unit/response-interceptor.spec.ts ================================================ import { IncomingMessage, ServerResponse } from 'node:http'; import { Socket } from 'node:net'; import { responseInterceptor } from '../../src/handlers/response-interceptor'; const fakeProxyResponse = () => { const httpIncomingMessage = new IncomingMessage(new Socket()); httpIncomingMessage._read = () => ({}); return httpIncomingMessage; }; const fakeResponse = () => { const httpIncomingMessage = fakeProxyResponse(); const response = new ServerResponse(httpIncomingMessage); response.setHeader = jest.fn(); response.write = jest.fn(); response.end = jest.fn(); return response; }; const waitInterceptorHandler = (ms = 1): Promise => new Promise((resolve) => setTimeout(resolve, ms)); describe('responseInterceptor', () => { it('should write body on end proxy event', async () => { const proxyRes = fakeProxyResponse(); const req = fakeProxyResponse(); const res = fakeResponse(); responseInterceptor(async () => JSON.stringify({ someField: '' }))(proxyRes, req, res); proxyRes.emit('end'); await waitInterceptorHandler(); const expectedBody = JSON.stringify({ someField: '' }); expect(res.setHeader).toHaveBeenCalledWith('content-length', expectedBody.length); expect(res.write).toHaveBeenCalledWith(Buffer.from(expectedBody)); expect(res.end).toHaveBeenCalledWith(); }); it('should end with error when receive a proxy error event', async () => { const proxyRes = fakeProxyResponse(); const req = fakeProxyResponse(); const res = fakeResponse(); responseInterceptor(async () => JSON.stringify({ someField: '' }))(proxyRes, req, res); proxyRes.emit('error', new Error('some error message')); expect(res.setHeader).not.toHaveBeenCalled(); expect(res.write).not.toHaveBeenCalled(); expect(res.end).toHaveBeenCalledWith('Error fetching proxied request: some error message'); }); }); ================================================ FILE: test/unit/router.spec.ts ================================================ import { getTarget } from '../../src/router'; describe('router unit test', () => { let fakeReq; let config; let result; let proxyOptionWithRouter; beforeEach(() => { fakeReq = { headers: { host: 'localhost', }, url: '/', }; config = { target: 'http://localhost:6000', }; }); describe('router.getTarget from function', () => { let request; beforeEach(() => { proxyOptionWithRouter = { target: 'http://localhost:6000', router(req) { request = req; return 'http://foobar.com:666'; }, }; result = getTarget(fakeReq, proxyOptionWithRouter); }); describe('custom dynamic router function', () => { it('should provide the request object for dynamic routing', () => { expect(request.headers.host).toBe('localhost'); expect(request.url).toBe('/'); }); it('should return new target', () => { return expect(result).resolves.toBe('http://foobar.com:666'); }); }); }); describe('router.getTarget from async function', () => { let request; beforeEach(() => { proxyOptionWithRouter = { target: 'http://localhost:6000', async router(req) { request = req; return 'http://foobar.com:666'; }, }; result = getTarget(fakeReq, proxyOptionWithRouter); }); describe('custom dynamic router async function', () => { it('should provide the request object for dynamic routing', () => { expect(request.headers.host).toBe('localhost'); expect(request.url).toBe('/'); }); it('should return new target', () => { return expect(result).resolves.toBe('http://foobar.com:666'); }); }); }); describe('router.getTarget from table', () => { beforeEach(() => { proxyOptionWithRouter = { target: 'http://localhost:6000', router: { 'alpha.localhost': 'http://localhost:6001', 'beta.localhost': 'http://localhost:6002', 'gamma.localhost/api': 'http://localhost:6003', 'gamma.localhost': 'http://localhost:6004', '/rest': 'http://localhost:6005', '/some/specific/path': 'http://localhost:6006', '/some': 'http://localhost:6007', }, }; }); describe('without router config', () => { it('should return the normal target when router not present in config', () => { result = getTarget(fakeReq, config); return expect(result).resolves.toBeUndefined(); }); }); describe('with just the host in router config', () => { it('should target http://localhost:6001 when for router:"alpha.localhost"', () => { fakeReq.headers.host = 'alpha.localhost'; result = getTarget(fakeReq, proxyOptionWithRouter); return expect(result).resolves.toBe('http://localhost:6001'); }); it('should target http://localhost:6002 when for router:"beta.localhost"', () => { fakeReq.headers.host = 'beta.localhost'; result = getTarget(fakeReq, proxyOptionWithRouter); return expect(result).resolves.toBe('http://localhost:6002'); }); }); describe('with host and host + path config', () => { it('should target http://localhost:6004 without path', () => { fakeReq.headers.host = 'gamma.localhost'; result = getTarget(fakeReq, proxyOptionWithRouter); return expect(result).resolves.toBe('http://localhost:6004'); }); it('should target http://localhost:6003 exact path match', () => { fakeReq.headers.host = 'gamma.localhost'; fakeReq.url = '/api'; result = getTarget(fakeReq, proxyOptionWithRouter); return expect(result).resolves.toBe('http://localhost:6003'); }); it('should target http://localhost:6004 when contains path', () => { fakeReq.headers.host = 'gamma.localhost'; fakeReq.url = '/api/books/123'; result = getTarget(fakeReq, proxyOptionWithRouter); return expect(result).resolves.toBe('http://localhost:6003'); }); }); describe('with just the path', () => { it('should target http://localhost:6005 with just a path as router config', () => { fakeReq.url = '/rest'; result = getTarget(fakeReq, proxyOptionWithRouter); return expect(result).resolves.toBe('http://localhost:6005'); }); it('should target http://localhost:6005 with just a path as router config', () => { fakeReq.url = '/rest/deep/path'; result = getTarget(fakeReq, proxyOptionWithRouter); return expect(result).resolves.toBe('http://localhost:6005'); }); it('should target http://localhost:6000 path in not present in router config', () => { fakeReq.url = '/unknown-path'; result = getTarget(fakeReq, proxyOptionWithRouter); return expect(result).resolves.toBeUndefined(); }); }); describe('matching order of router config', () => { it('should return first matching target when similar paths are configured', () => { fakeReq.url = '/some/specific/path'; result = getTarget(fakeReq, proxyOptionWithRouter); return expect(result).resolves.toBe('http://localhost:6006'); }); }); }); }); ================================================ FILE: test/unit/status-code.spec.ts ================================================ import { getStatusCode } from '../../src/status-code'; describe('getStatusCode', () => { const errorCodes = { HPE_INVALID_FOO: 502, HPE_INVALID_BAR: 502, ECONNREFUSED: 504, ECONNRESET: 504, ENOTFOUND: 504, ETIMEDOUT: 504, any: 500, }; it('should return http status code from error code', () => { Object.entries(errorCodes).forEach(([errorCode, statusCode]) => { expect(getStatusCode(errorCode)).toBe(statusCode); }); }); }); ================================================ FILE: test/unit/utils/function.spec.ts ================================================ import { getFunctionName } from '../../../src/utils/function'; describe('getFunctionName()', () => { it('should return Function name', () => { function myFunction() {} expect(getFunctionName(myFunction)).toBe('myFunction'); }); it('should return arrow Function name', () => { const myFunction = () => {}; expect(getFunctionName(myFunction)).toBe('myFunction'); }); it('should return anonymous Function name', () => { expect(getFunctionName(() => {})).toBe('[anonymous Function]'); }); }); ================================================ FILE: test/unit/utils/logger-plugin.spec.ts ================================================ import { type Sockets, getPort } from '../../../src/utils/logger-plugin'; describe('getPort()', () => { it('should return port from proxyRes.req.agent.sockets', () => { const sockets = { 'jsonplaceholder.typicode.com:80:': [], } as unknown as Sockets; expect(getPort(sockets)).toBe('80'); }); it('should handle missing "sockets" from proxyRes?.req?.agent?.sockets', () => { const sockets = undefined; expect(getPort(sockets)).toBe(undefined); }); it('should handle empty "sockets" from proxyRes?.req?.agent?.sockets', () => { const sockets = {} as unknown as Sockets; expect(getPort(sockets)).toBe(undefined); }); }); ================================================ FILE: test/unit/utils/sanitize.spec.ts ================================================ import { sanitize } from '../../../src/utils/sanitize'; describe('sanitize()', () => { it('should return empty string for undefined input', () => { expect(sanitize(undefined)).toEqual(''); }); it('should replace special characters with their HTML entity equivalents', () => { const input = '<>'; expect(sanitize(input)).toMatchInlineSnapshot(`"%3C%3E"`); }); it('should replace special characters with HTML entities', () => { const input = ''; expect(sanitize(input)).toMatchInlineSnapshot(`"%3Cscript%3Ealert("XSS")%3C/script%3E"`); }); }); ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "rootDir": "./src", "outDir": "./dist", "lib": ["es2021", "es2022"], "module": "commonjs", "moduleResolution": "node", "target": "es2021", "incremental": true, "declaration": true, "strict": true, "noImplicitAny": false }, "include": ["./src"] }