├── .gitignore ├── .github ├── CODEOWNERS.md ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md ├── workflows │ ├── publish-egg.yml │ ├── publish-docs.yml │ └── test.yml ├── CONTRIBUTING.md └── CHANGELOG.md ├── test ├── index.test.html ├── deps.ts ├── support │ ├── proxyTarget.ts │ └── utils.ts ├── decorateSrcResHeaders.test.ts ├── headers.test.ts ├── status.test.ts ├── utils.ts ├── urlParsing.test.ts ├── https.test.ts ├── catchingErrors.test.ts ├── url.test.ts ├── streaming.test.ts ├── payload.test.ts ├── handleProxyError.test.ts └── filterReq.test.ts ├── docs ├── _config.yaml ├── assets │ ├── images │ │ ├── icons.png │ │ ├── widgets.png │ │ ├── icons@2x.png │ │ └── widgets@2x.png │ └── js │ │ └── search.json ├── modules │ ├── _types_.html │ ├── _steps_buildproxyurl_.html │ ├── _steps_buildproxyreqinit_.html │ ├── _steps_copyproxyresheaderstouserres_.html │ ├── _steps_filtersrcreq_.html │ ├── _proxy_.html │ ├── _steps_sendsrcres_.html │ ├── _steps_handleproxyerrors_.html │ └── _requestoptions_.html ├── globals.html ├── interfaces │ └── _types_.proxyoptions.html └── index.html ├── mod.ts ├── version.ts ├── src ├── types.ts ├── steps │ ├── filterSrcReq.ts │ ├── buildProxyReqInit.ts │ ├── sendSrcRes.ts │ ├── copyProxyResHeadersToUserRes.ts │ ├── buildProxyUrl.ts │ └── handleProxyErrors.ts ├── requestOptions.ts └── proxy.ts ├── examples ├── README.md └── basic │ ├── README.md │ └── index.ts ├── .vscode └── settings.json ├── egg.json ├── Makefile ├── LICENSE.md ├── deps.ts ├── CODE_OF_CONDUCT.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /.github/CODEOWNERS.md: -------------------------------------------------------------------------------- 1 | * @cmorten -------------------------------------------------------------------------------- /test/index.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [cmorten] 4 | -------------------------------------------------------------------------------- /docs/_config.yaml: -------------------------------------------------------------------------------- 1 | future: true 2 | encoding: "UTF-8" 3 | include: 4 | - "_*_.html" 5 | - "_*_.*.html" 6 | -------------------------------------------------------------------------------- /docs/assets/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmorten/oak-http-proxy/HEAD/docs/assets/images/icons.png -------------------------------------------------------------------------------- /docs/assets/images/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmorten/oak-http-proxy/HEAD/docs/assets/images/widgets.png -------------------------------------------------------------------------------- /docs/assets/images/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmorten/oak-http-proxy/HEAD/docs/assets/images/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/images/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmorten/oak-http-proxy/HEAD/docs/assets/images/widgets@2x.png -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export { proxy } from "./src/proxy.ts"; 2 | export type { ProxyOptions } from "./src/types.ts"; 3 | export { DENO_SUPPORTED_VERSIONS, VERSION } from "./version.ts"; 4 | -------------------------------------------------------------------------------- /version.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Version of oak-http-proxy. 3 | */ 4 | export const VERSION = "2.3.0"; 5 | 6 | /** 7 | * Supported versions of Deno. 8 | */ 9 | export const DENO_SUPPORTED_VERSIONS: string[] = ["1.40.2"]; 10 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { ProxyOptions as BaseProxyOptions } from "../deps.ts"; 2 | 3 | export interface ProxyOptions extends BaseProxyOptions { 4 | /** 5 | * The request body size limit to use. 6 | * 7 | * @public 8 | */ 9 | reqBodyLimit?: number; 10 | } 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Issue 2 | 3 | Fixes <#_ISSUE_ID_> 4 | 5 | ## Details 6 | 7 | Brief summary of PR purpose and code changes. 8 | 9 | ## CheckList 10 | 11 | - [ ] PR starts with [#_ISSUE_ID_]. 12 | - [ ] Has been tested (where required) before merge to main. 13 | -------------------------------------------------------------------------------- /test/deps.ts: -------------------------------------------------------------------------------- 1 | export * as Oak from "https://deno.land/x/oak@v12.6.2/mod.ts"; 2 | export * as Opine from "https://deno.land/x/opine@2.3.4/mod.ts"; 3 | export { expect } from "https://deno.land/x/expect@v0.4.0/mod.ts"; 4 | export { superoak } from "https://deno.land/x/superoak@4.8.0/mod.ts"; 5 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This directory contains a series of self-contained examples that you can use as starting points for your app, or as snippets to pull into your existing applications: 4 | 5 | - [basic](./basic) - Basic example using `oak-http-proxy` as a proxy middleware for Oak. 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "[typescript]": { 4 | "editor.defaultFormatter": "denoland.vscode-deno" 5 | }, 6 | "[typescriptreact]": { 7 | "editor.defaultFormatter": "denoland.vscode-deno" 8 | }, 9 | "editor.defaultFormatter": "esbenp.prettier-vscode", 10 | "deno.suggest.imports.hosts": { 11 | "https://deno.land": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/steps/filterSrcReq.ts: -------------------------------------------------------------------------------- 1 | import type { ProxyState } from "../../deps.ts"; 2 | 3 | const defaultFilter = () => false; 4 | 5 | export function filterSrcReq(state: ProxyState) { 6 | const resolverFn = state.options.filterReq || defaultFilter; 7 | 8 | return Promise 9 | .resolve(resolverFn(state.src.req, state.src.res)) 10 | .then((isFiltered) => !isFiltered ? state : Promise.reject()); 11 | } 12 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | # basic 2 | 3 | Basic example using `oak-http-proxy` as a proxy middleware. 4 | 5 | ## How to run this example 6 | 7 | Run this example using: 8 | 9 | ```bash 10 | deno run --allow-net --allow-read ./examples/proxy/index.ts 11 | ``` 12 | 13 | if have the repo cloned locally _OR_ 14 | 15 | ```bash 16 | deno run --allow-net --allow-read https://deno.land/x/oak_http_proxy@2.3.0/examples/basic/index.ts 17 | ``` 18 | 19 | if you don't! 20 | -------------------------------------------------------------------------------- /src/steps/buildProxyReqInit.ts: -------------------------------------------------------------------------------- 1 | import type { ProxyState } from "../../deps.ts"; 2 | import { createRequestInit } from "../requestOptions.ts"; 3 | 4 | export function buildProxyReqInit(state: ProxyState) { 5 | const options = state.options; 6 | const req = state.src.req; 7 | 8 | return Promise.resolve(createRequestInit(req, options)) 9 | .then((resolvedReqInit) => { 10 | state.proxy.reqInit = resolvedReqInit; 11 | 12 | return state; 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /src/steps/sendSrcRes.ts: -------------------------------------------------------------------------------- 1 | import type { ProxyState } from "../../deps.ts"; 2 | 3 | const isNullBodyStatus = (status: number) => 4 | status === 101 || status === 204 || status === 205 || status === 304; 5 | 6 | export function sendSrcRes(state: ProxyState) { 7 | if (state.options.stream) { 8 | state.src.res.body = state.proxy.res?.body; 9 | } else if (!isNullBodyStatus(state.src.res.status)) { 10 | state.src.res.body = state.proxy.resData; 11 | } 12 | 13 | return Promise.resolve(state); 14 | } 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Issue 2 | 3 | Setup: 4 | 5 | - Deno Version: 6 | - v8 Version: 7 | - Typescript Version: 8 | - Oak HTTP Proxy Version: 9 | - Oak Version: 10 | 11 | ## Details 12 | 13 | > Please replace this quote block with the details of the feature / bug you wish to be addressed. If it is a bug please do your best to add steps to reproduce. 14 | -------------------------------------------------------------------------------- /src/steps/copyProxyResHeadersToUserRes.ts: -------------------------------------------------------------------------------- 1 | import type { ProxyState } from "../../deps.ts"; 2 | 3 | export function copyProxyResHeadersToUserRes(state: ProxyState) { 4 | const res = state.src.res; 5 | const rsp = state.proxy.res as Response; 6 | 7 | res.status = rsp.status; 8 | 9 | Array.from(rsp.headers.entries()) 10 | .forEach(([field, value]) => { 11 | if (field.toLowerCase() !== "transfer-encoding") { 12 | res.headers.append(field, value); 13 | } 14 | }); 15 | 16 | return Promise.resolve(state); 17 | } 18 | -------------------------------------------------------------------------------- /src/steps/buildProxyUrl.ts: -------------------------------------------------------------------------------- 1 | import type { ProxyState } from "../../deps.ts"; 2 | import { parseUrl } from "../requestOptions.ts"; 3 | 4 | export function buildProxyUrl(ctx: any) { 5 | return function (state: ProxyState) { 6 | let parsedUrl; 7 | 8 | if (state.options.memoizeUrl) { 9 | parsedUrl = state.options.memoizedUrl = state.options.memoizedUrl || 10 | parseUrl(state, ctx); 11 | } else { 12 | parsedUrl = parseUrl(state, ctx); 13 | } 14 | 15 | state.proxy.url = parsedUrl; 16 | 17 | return Promise.resolve(state); 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/steps/handleProxyErrors.ts: -------------------------------------------------------------------------------- 1 | import { STATUS_TEXT } from "../../deps.ts"; 2 | 3 | function connectionResetHandler(ctx: any) { 4 | ctx.response.headers.set( 5 | "X-Timeout-Reason", 6 | "oak-http-proxy reset the request.", 7 | ); 8 | ctx.response.status = 504; 9 | ctx.response.body = STATUS_TEXT[504]; 10 | } 11 | 12 | export function handleProxyErrors(err: any, ctx: any) { 13 | if (err && err.code === "ECONNRESET" || err.code === "ECONTIMEDOUT") { 14 | connectionResetHandler(ctx); 15 | } else { 16 | ctx.throw(err.status, err.message); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/publish-egg.yml: -------------------------------------------------------------------------------- 1 | name: Publish Egg 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | publish-egg: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: denolib/setup-deno@v2 13 | with: 14 | deno-version: 1.40.2 15 | - run: deno install -A -f --unstable -n eggs https://x.nest.land/eggs@0.3.10/eggs.ts 16 | - run: | 17 | export PATH="/home/runner/.deno/bin:$PATH" 18 | eggs link ${NEST_LAND_KEY} 19 | eggs publish --yes 20 | env: 21 | NEST_LAND_KEY: ${{secrets.NEST_LAND_KEY}} 22 | -------------------------------------------------------------------------------- /egg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oak-http-proxy", 3 | "description": "Proxy middleware for Deno Oak HTTP servers.", 4 | "version": "2.3.0", 5 | "repository": "https://github.com/cmorten/oak-http-proxy", 6 | "stable": true, 7 | "files": [ 8 | "./mod.ts", 9 | "./deps.ts", 10 | "./version.ts", 11 | "./src/**/*", 12 | "./README.md", 13 | "./LICENSE.md", 14 | "./.github/CHANGELOG.md" 15 | ], 16 | "checkFormat": false, 17 | "checkTests": false, 18 | "checkInstallation": false, 19 | "check": false, 20 | "entry": "./mod.ts", 21 | "homepage": "https://github.com/cmorten/oak-http-proxy", 22 | "ignore": [], 23 | "unlisted": false 24 | } 25 | -------------------------------------------------------------------------------- /examples/basic/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Run this example using: 3 | * 4 | * deno run --allow-net ./examples/basic/index.ts 5 | * 6 | * if have the repo cloned locally OR 7 | * 8 | * deno run --allow-net https://deno.land/x/oak_http_proxy@2.3.0/examples/basic/index.ts 9 | * 10 | * if you don't! 11 | */ 12 | 13 | import { proxy } from "../../mod.ts"; 14 | import { Application } from "https://deno.land/x/oak@v12.6.2/mod.ts"; 15 | 16 | const app = new Application(); 17 | 18 | app.addEventListener("error", (event) => { 19 | console.log(event.error); 20 | }); 21 | 22 | app.use(proxy("https://github.com/oakserver/oak")); 23 | 24 | await app.listen({ port: 3000 }); 25 | -------------------------------------------------------------------------------- /.github/workflows/publish-docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish TypeDocs 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | publish-docs: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Use Node.js 20 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 20 17 | - name: Install deps 18 | run: make deps 19 | - name: Use Deno 20 | uses: denolib/setup-deno@v2 21 | with: 22 | deno-version: 1.40.2 23 | - run: make typedoc 24 | - run: make ci 25 | - name: Publish Updated Type Docs 26 | uses: stefanzweifel/git-auto-commit-action@v4 27 | with: 28 | commit_message: publish typedocs 29 | push_options: --force 30 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, macos-latest] 14 | deno-version: [1.40.2] 15 | 16 | runs-on: ${{ matrix.os }} 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Deno ${{ matrix.deno-version }} 21 | uses: denolib/setup-deno@v2 22 | with: 23 | deno-version: ${{ matrix.deno-version }} 24 | - run: make ci 25 | 26 | test-win: 27 | strategy: 28 | matrix: 29 | os: [windows-latest] 30 | deno-version: [1.40.2] 31 | 32 | runs-on: ${{ matrix.os }} 33 | 34 | steps: 35 | - uses: actions/checkout@v2 36 | - uses: denolib/setup-deno@v2 37 | with: 38 | deno-version: ${{ matrix.deno-version }} 39 | - run: make build 40 | - run: make test 41 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build ci deps doc fmt fmt-check lint precommit test typedoc 2 | 3 | FILES_TO_FORMAT = ./src ./test ./deps.ts ./mod.ts ./version.ts 4 | 5 | build: 6 | @deno run --allow-net --allow-read --allow-env --reload mod.ts 7 | 8 | ci: 9 | @make fmt-check 10 | @make build 11 | @make test 12 | 13 | deps: 14 | @npm install -g typescript@4 typedoc@0.19.2 15 | 16 | doc: 17 | @deno doc ./mod.ts 18 | 19 | fmt: 20 | @deno fmt ${FILES_TO_FORMAT} 21 | 22 | fmt-check: 23 | @deno fmt --check ${FILES_TO_FORMAT} 24 | 25 | lint: 26 | @deno lint ${FILES_TO_FORMAT} 27 | 28 | precommit: 29 | @make typedoc 30 | @make fmt 31 | @make ci 32 | 33 | test: 34 | @deno test --allow-net --allow-read 35 | 36 | typedoc: 37 | @rm -rf docs 38 | @typedoc --ignoreCompilerErrors --out ./docs --mode modules --includeDeclarations --excludeExternals --name oak-http-proxy ./src 39 | @make fmt 40 | @make fmt 41 | @echo 'future: true\nencoding: "UTF-8"\ninclude:\n - "_*_.html"\n - "_*_.*.html"' > ./docs/_config.yaml 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2020 Craig Morten 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export { STATUS_TEXT } from "https://deno.land/std@0.213.0/http/status.ts"; 2 | export { createState } from "https://deno.land/x/opineHttpProxy@3.2.0/src/createState.ts"; 3 | export type { 4 | ProxyState, 5 | ProxyUrlFunction, 6 | } from "https://deno.land/x/opineHttpProxy@3.2.0/src/createState.ts"; 7 | export type { ProxyOptions } from "https://deno.land/x/opineHttpProxy@3.2.0/src/resolveOptions.ts"; 8 | export { isUnset } from "https://deno.land/x/opineHttpProxy@3.2.0/src/isUnset.ts"; 9 | export { decorateProxyReqUrl } from "https://deno.land/x/opineHttpProxy@3.2.0/src/steps/decorateProxyReqUrl.ts"; 10 | export { decorateProxyReqInit } from "https://deno.land/x/opineHttpProxy@3.2.0/src/steps/decorateProxyReqInit.ts"; 11 | export { prepareProxyReq } from "https://deno.land/x/opineHttpProxy@3.2.0/src/steps/prepareProxyReq.ts"; 12 | export { sendProxyReq } from "https://deno.land/x/opineHttpProxy@3.2.0/src/steps/sendProxyReq.ts"; 13 | export { filterProxyRes } from "https://deno.land/x/opineHttpProxy@3.2.0/src/steps/filterProxyRes.ts"; 14 | export { decorateSrcResHeaders } from "https://deno.land/x/opineHttpProxy@3.2.0/src/steps/decorateSrcResHeaders.ts"; 15 | export { decorateSrcRes } from "https://deno.land/x/opineHttpProxy@3.2.0/src/steps/decorateSrcRes.ts"; 16 | -------------------------------------------------------------------------------- /test/support/proxyTarget.ts: -------------------------------------------------------------------------------- 1 | import { ErrorRequestHandler } from "https://deno.land/x/opine@2.3.4/mod.ts"; 2 | import { Opine } from "../deps.ts"; 3 | 4 | const { opine, json, urlencoded } = Opine; 5 | 6 | export function proxyTarget( 7 | { port = 0, timeout = 100, handlers }: { 8 | port?: number; 9 | timeout?: number; 10 | handlers?: any; 11 | } = { port: 0, timeout: 100 }, 12 | ) { 13 | const target = opine(); 14 | 15 | target.use(urlencoded()); 16 | target.use(json()); 17 | 18 | target.use(function (_req, _res, next) { 19 | setTimeout(function () { 20 | next(); 21 | }, timeout); 22 | }); 23 | 24 | if (handlers) { 25 | handlers.forEach((handler: any) => { 26 | (target as any)[handler.method](handler.path, handler.fn); 27 | }); 28 | } 29 | 30 | target.get("/get", function (_req, res) { 31 | res.send("OK"); 32 | }); 33 | 34 | target.use("/headers", function (req, res) { 35 | res.json({ headers: req.headers }); 36 | }); 37 | 38 | target.use( 39 | (function (err, _req, res, next) { 40 | res.send(err); 41 | next(); 42 | }) as ErrorRequestHandler, 43 | ); 44 | 45 | target.use(function (_req, res) { 46 | res.sendStatus(404); 47 | }); 48 | 49 | return target.listen(port); 50 | } 51 | -------------------------------------------------------------------------------- /test/decorateSrcResHeaders.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "./support/utils.ts"; 2 | import { expect, Oak, Opine, superoak } from "./deps.ts"; 3 | import { proxy } from "../mod.ts"; 4 | 5 | const { Application } = Oak; 6 | const { opine } = Opine; 7 | 8 | describe("when userResHeaderDecorator is defined", () => { 9 | it("provides an interface for updating headers", async (done) => { 10 | // Create a server to proxy through to. 11 | // Using Opine as simpler to retrieve the target port than Oak. 12 | const target = opine(); 13 | target.use((req, res) => { 14 | res.json(req.headers); 15 | }); 16 | const targetServer = target.listen(); 17 | const targetPort = (targetServer.addrs[0] as Deno.NetAddr).port; 18 | 19 | // Setup Oak with proxy middleware. 20 | const app = new Application(); 21 | app.use(proxy(`http://localhost:${targetPort}`, { 22 | srcResHeaderDecorator: (headers) => { 23 | headers.set("x-proxy", "oak-http-proxy"); 24 | 25 | return headers; 26 | }, 27 | })); 28 | 29 | const request = await superoak(app); 30 | 31 | request.get("/proxy") 32 | .end((err, res) => { 33 | targetServer.close(); 34 | expect(res.header["x-proxy"]).toEqual("oak-http-proxy"); 35 | done(err); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/headers.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "./support/utils.ts"; 2 | import { expect, Oak, superoak } from "./deps.ts"; 3 | import { proxy } from "../mod.ts"; 4 | 5 | const { Application } = Oak; 6 | 7 | describe("proxies headers", () => { 8 | it("should pass on headers set as options", async (done) => { 9 | const app = new Application(); 10 | app.use(proxy("http://httpbin.org/headers", { 11 | headers: { 12 | "X-Deno": "oak-http-proxy", 13 | }, 14 | })); 15 | 16 | const request = await superoak(app); 17 | request.get("/") 18 | .expect(200) 19 | .end((err, res) => { 20 | if (err) return done(err); 21 | expect(res.body.headers["X-Deno"]).toEqual("oak-http-proxy"); 22 | done(); 23 | }); 24 | }); 25 | 26 | it("should pass on headers set on the request", async (done) => { 27 | const app = new Application(); 28 | app.use(proxy("http://httpbin.org/headers", { 29 | headers: { 30 | "X-Deno": "oak-http-proxy", 31 | }, 32 | })); 33 | 34 | const request = await superoak(app); 35 | request.get("/") 36 | .set("X-Powered-By", "Deno") 37 | .expect(200) 38 | .end((err, res) => { 39 | if (err) return done(err); 40 | expect(res.body.headers["X-Powered-By"]).toEqual("Deno"); 41 | done(); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/status.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "./support/utils.ts"; 2 | import { expect, Oak, Opine, superoak } from "./deps.ts"; 3 | import { proxy } from "../mod.ts"; 4 | 5 | const { opine } = Opine; 6 | const { Application, Router } = Oak; 7 | 8 | describe("proxies status code", () => { 9 | [304, 404, 200, 401, 500].forEach((status) => { 10 | it(`should handle a "${status}" proxied status code`, async (done) => { 11 | const target = opine(); 12 | 13 | target.use("/status/:status", (req, res) => { 14 | res.sendStatus(parseInt(req.params.status)); 15 | }); 16 | 17 | const targetServer = target.listen(); 18 | const targetPort = (targetServer.addrs[0] as Deno.NetAddr).port; 19 | 20 | const router = new Router(); 21 | router.get( 22 | "/status/:status", 23 | proxy(`http://localhost:${targetPort}`, { 24 | proxyReqUrlDecorator: (url, req) => { 25 | url.pathname = req.url.pathname; 26 | 27 | return url; 28 | }, 29 | }), 30 | ); 31 | 32 | const app = new Application(); 33 | app.use(router.routes()); 34 | app.use(router.allowedMethods()); 35 | 36 | const request = await superoak(app); 37 | request.get(`/status/${status}`) 38 | .end((err, res) => { 39 | targetServer.close(); 40 | expect(res.status).toEqual(status); 41 | done(err); 42 | }); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Test timeout. 3 | */ 4 | export const TEST_TIMEOUT = 3000; 5 | 6 | /** 7 | * A no-op _describe_ method. 8 | * 9 | * @param name 10 | * @param fn 11 | */ 12 | export async function describe(name: string, fn: () => void | Promise) { 13 | fn(); 14 | } 15 | 16 | /** 17 | * An _it_ wrapper around `Deno.test`. 18 | * 19 | * @param name 20 | * @param fn 21 | */ 22 | export async function it( 23 | name: string, 24 | fn: (done?: any) => void | Promise, 25 | ) { 26 | Deno.test(name, async () => { 27 | let done: any = (err?: any) => { 28 | if (err) throw err; 29 | }; 30 | let race: Promise = Promise.resolve(); 31 | 32 | if (fn.length === 1) { 33 | let resolve: (value: unknown) => void; 34 | const donePromise = new Promise((r) => { 35 | resolve = r; 36 | }); 37 | 38 | let timeoutId: number; 39 | 40 | race = Promise.race([ 41 | new Promise((_, reject) => 42 | timeoutId = setTimeout(() => { 43 | reject( 44 | new Error( 45 | `test "${name}" failed to complete by calling "done" within ${TEST_TIMEOUT}ms.`, 46 | ), 47 | ); 48 | }, TEST_TIMEOUT) 49 | ), 50 | donePromise, 51 | ]); 52 | 53 | done = (err?: any) => { 54 | clearTimeout(timeoutId); 55 | resolve(undefined); 56 | if (err) throw err; 57 | }; 58 | } 59 | 60 | await fn(done); 61 | await race; 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /test/urlParsing.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "./support/utils.ts"; 2 | import { proxyTarget } from "./support/proxyTarget.ts"; 3 | import { Oak, superoak } from "./deps.ts"; 4 | import { proxy } from "../mod.ts"; 5 | 6 | const { Application } = Oak; 7 | 8 | const proxyRouteFn = [ 9 | { 10 | method: "get", 11 | path: "/", 12 | fn: (req: any, res: any) => { 13 | res.send("Hello Deno"); 14 | }, 15 | }, 16 | ]; 17 | 18 | describe("url parsing", () => { 19 | it("can parse a local url with a port", async (done) => { 20 | const target = proxyTarget({ handlers: proxyRouteFn }); 21 | const targetPort = (target.addrs[0] as Deno.NetAddr).port; 22 | 23 | const app = new Application(); 24 | app.use(proxy(`http://localhost:${targetPort}`)); 25 | 26 | const request = await superoak(app); 27 | request.get("/") 28 | .end((err) => { 29 | target.close(); 30 | done(err); 31 | }); 32 | }); 33 | 34 | it("can parse a url with a port", async (done) => { 35 | const app = new Application(); 36 | app.use(proxy("http://httpbin.org:80")); 37 | 38 | const request = await superoak(app); 39 | request.get("/") 40 | .end(done); 41 | }); 42 | 43 | it("does not throw `Uncaught RangeError` if you have both a port and a trailing slash", async (done) => { 44 | const app = new Application(); 45 | app.use(proxy("http://httpbin.org:80/")); 46 | 47 | const request = await superoak(app); 48 | request.get("/") 49 | .end(done); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/https.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "./support/utils.ts"; 2 | import { expect, Oak, superoak } from "./deps.ts"; 3 | import { proxy } from "../mod.ts"; 4 | 5 | const { Application } = Oak; 6 | 7 | describe("proxies https", () => { 8 | async function assertSecureRequest(app: any, done: any) { 9 | const request = await superoak(app); 10 | 11 | request.get("/") 12 | .end((err, res) => { 13 | expect(res.body.headers["X-Forwarded-Port"]).toEqual("443"); 14 | expect(res.body.headers["X-Forwarded-Proto"]).toEqual("https"); 15 | done(err); 16 | }); 17 | } 18 | 19 | it("should proxy via https for a string url with 'https' protocol", (done) => { 20 | const app = new Application(); 21 | app.use(proxy("https://httpbin.org/get?show_env=1")); 22 | assertSecureRequest(app, done); 23 | }); 24 | 25 | it("should proxy via https for a string url with 'http' protocol but 'secure' option set to true", (done) => { 26 | const app = new Application(); 27 | app.use(proxy("http://httpbin.org/get?show_env=1", { secure: true })); 28 | assertSecureRequest(app, done); 29 | }); 30 | 31 | it("should proxy via https for a function url with 'https' protocol", (done) => { 32 | const app = new Application(); 33 | app.use(proxy(() => { 34 | return "https://httpbin.org/get?show_env=1"; 35 | })); 36 | assertSecureRequest(app, done); 37 | }); 38 | 39 | it("should proxy via https for a function url with 'http' protocol but 'secure' option set to true", (done) => { 40 | const app = new Application(); 41 | app.use(proxy(() => { 42 | return "http://httpbin.org/get?show_env=1"; 43 | }, { secure: true })); 44 | assertSecureRequest(app, done); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this repository 2 | 3 | First of all, thanks for taking the time to read this document and contributing to our codebase! :tada: :beers: 4 | 5 | ## Getting started 6 | 7 | If you're working on an existing issue then awesome! Let us know by dropping a comment in the issue. 8 | 9 | If it's a new bug fix or feature that you would like to contribute, then please raise an issue so it can be tracked ( and to help out others who are experiencing the same issue / want the new thing know that it's being looked at! ). Be sure to check for existing issues before raising your own! 10 | 11 | ## Working on your feature 12 | 13 | ### Branching 14 | 15 | On this project we follow mainline development (or trunk based development), and our default branch is `main`. 16 | 17 | Therefore you need to branch from `main` and merge into `main`. 18 | 19 | ### Coding style 20 | 21 | Generally try to match the style and conventions of the code around your changes. Ultimately we want code that is clear, concise, consistent and easy to read. 22 | 23 | As this is a Deno project will also insist on meeting the Deno `fmt` standards. 24 | 25 | ### Format Code 26 | 27 | To format the code run: 28 | 29 | ```bash 30 | make fmt 31 | ``` 32 | 33 | ### Tests 34 | 35 | Before opening a PR, please run the following command to make sure your branch will build and pass all the checks and tests: 36 | 37 | ```console 38 | make ci 39 | ``` 40 | 41 | ## Opening a PR 42 | 43 | Once you're confident your branch is ready to review, open a PR against `main` on this repo. 44 | 45 | Please use the PR template as a guide, but if your change doesn't quite fit it, feel free to customize. 46 | 47 | ## Merging and publishing 48 | 49 | When your feature branch / PR has been tested and has an approval, it is then ready to merge. Please contact the maintainer to action the merge. 50 | -------------------------------------------------------------------------------- /.github/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ChangeLog 2 | 3 | For the latest changes, please refer to the 4 | [releases page](https://github.com/cmorten/oak-http-proxy/releases). 5 | 6 | ## [2.1.0] - 29-08-2022 7 | 8 | - feat: support Deno `1.25.0` and std `0.153.0` 9 | - feat: upgrade to `opineHttpProxy@3.0.2` 10 | - feat: other minor dependency upgrades 11 | - feat: add `reqBodyLimit` for changing request body size limits 12 | 13 | ## [2.0.1] - 10-01-2022 14 | 15 | - feat: support Deno `1.17.2` and std `0.120.0` 16 | - feat: upgrade to `opineHttpProxy@3.0.1` 17 | - feat: other minor dependency upgrades 18 | 19 | ## [2.0.0] - 21-11-2021 20 | 21 | - feat: support Deno `1.16.2` and std `0.115.1` 22 | - feat: upgrade to `opineHttpProxy@3.0.0` 23 | - fix: usage of `ctx.throw()` in error scenarios 24 | - fix: don't send body if have null body status 25 | - feat: upgrade to opine, oak, and superdeno deps 26 | - test: set content-length header when make POST requests to an oak server in tests following body size limits protection 27 | 28 | ## [1.4.1] - 13-07-2021 29 | 30 | - fix: filterReq crashes send middleware (#3) 31 | 32 | ## [1.4.0] - 13-07-2021 33 | 34 | - feat: Support Deno `1.12.0` and std `0.101.0` 35 | 36 | ## [1.3.0] - 26-04-2021 37 | 38 | - feat: Support Deno `1.9.2` and std `0.95.0` 39 | 40 | ## [1.2.0] - 10-02-2021 41 | 42 | - feat: Support Deno 1.7.2 and std 0.85.0 43 | 44 | ## [1.1.1] - 19-09-2020 45 | 46 | - chore: upgrade to eggs@0.2.2 in CI 47 | 48 | ## [1.1.0] - 19-09-2020 49 | 50 | - feat: Support Deno 1.4.1 and std 0.70.0 51 | 52 | ## [1.0.3] - 26-08-2020 53 | 54 | - fix: deno.land/x registry no longer supports mixed case. 55 | 56 | ## [1.0.2] - 26-08-2020 57 | 58 | - fix: use supported module name format for deno.land/x registry. 59 | 60 | ## [1.0.1] - 26-08-2020 61 | 62 | - docs: readme typo for nest.land. 63 | 64 | ## [1.0.0] - 26-08-2020 65 | 66 | - feat: initial loose port of [opine-http-proxy](https://github.com/cmorten/opine-http-proxy). 67 | -------------------------------------------------------------------------------- /test/support/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Test timeout. 3 | */ 4 | export const TEST_TIMEOUT = 3000; 5 | 6 | /** 7 | * A no-op _describe_ method. 8 | * 9 | * @param name 10 | * @param fn 11 | */ 12 | export function describe(_name: string, fn: () => void | Promise) { 13 | return fn(); 14 | } 15 | 16 | export type Done = (err?: unknown) => void; 17 | 18 | /** 19 | * An _it_ wrapper around `Deno.test`. 20 | * 21 | * @param name 22 | * @param fn 23 | */ 24 | export function it( 25 | name: string, 26 | fn: (done: Done) => void | Promise, 27 | options?: Partial, 28 | ) { 29 | Deno.test({ 30 | ...options, 31 | name, 32 | fn: async () => { 33 | let testError: unknown; 34 | 35 | let done: Done = (err?: unknown) => { 36 | if (err) { 37 | testError = err; 38 | } 39 | }; 40 | 41 | let race: Promise = Promise.resolve(); 42 | let timeoutId: number; 43 | 44 | if (fn.length === 1) { 45 | let resolve: (value?: unknown) => void; 46 | const donePromise = new Promise((r) => { 47 | resolve = r; 48 | }); 49 | 50 | race = Promise.race([ 51 | new Promise((_, reject) => 52 | timeoutId = setTimeout(() => { 53 | clearTimeout(timeoutId); 54 | 55 | reject( 56 | new Error( 57 | `test "${name}" failed to complete by calling "done" within ${TEST_TIMEOUT}ms.`, 58 | ), 59 | ); 60 | }, TEST_TIMEOUT) 61 | ), 62 | donePromise, 63 | ]); 64 | 65 | done = (err?: unknown) => { 66 | clearTimeout(timeoutId); 67 | resolve(); 68 | 69 | if (err) { 70 | testError = err; 71 | } 72 | }; 73 | } 74 | 75 | await fn(done); 76 | await race; 77 | 78 | if (timeoutId!) { 79 | clearTimeout(timeoutId); 80 | } 81 | 82 | // REF: https://github.com/denoland/deno/blob/987716798fb3bddc9abc7e12c25a043447be5280/ext/timers/01_timers.js#L353 83 | await new Promise((resolve) => setTimeout(resolve, 20)); 84 | 85 | if (testError) { 86 | throw testError; 87 | } 88 | }, 89 | }); 90 | } 91 | -------------------------------------------------------------------------------- /test/catchingErrors.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "./support/utils.ts"; 2 | import { expect, Oak, Opine, superoak } from "./deps.ts"; 3 | import { proxy } from "../mod.ts"; 4 | 5 | const { Application, Router } = Oak; 6 | const { opine } = Opine; 7 | 8 | const STATUS_CODES = [ 9 | { 10 | code: 403, 11 | text: "Forbidden", 12 | toString: /Error\: cannot GET http\:\/\/127.0.0.1\:\d+\/proxy \(403\)/, 13 | }, 14 | { 15 | code: 404, 16 | text: "Not Found", 17 | toString: /Error\: cannot GET http\:\/\/127.0.0.1\:\d+\/proxy \(404\)/, 18 | }, 19 | { 20 | code: 500, 21 | text: "Internal Server Error", 22 | toString: /Error\: cannot GET http\:\/\/127\.0\.0\.1\:\d+\/proxy \(500\)/, 23 | }, 24 | ]; 25 | 26 | describe("when server responds with an error", () => { 27 | STATUS_CODES.forEach((statusCode) => { 28 | it(`oak-http-proxy responds with ${statusCode.text} when proxy server responds ${statusCode.code}`, async (done) => { 29 | // Create a server to proxy through to. 30 | // Using Opine as simpler to retrieve the target port than Oak. 31 | const target = opine(); 32 | target.use(function (_req, res) { 33 | res.sendStatus(statusCode.code); 34 | }); 35 | const targetServer = target.listen(0); 36 | const targetPort = (targetServer.addrs[0] as Deno.NetAddr).port; 37 | 38 | // Setup Oak router with proxy middleware applied to `/proxy` route. 39 | const router = new Router(); 40 | router.get( 41 | "/proxy", 42 | proxy(`http://localhost:${targetPort}`, { 43 | reqAsBuffer: true, 44 | reqBodyEncoding: null, 45 | parseReqBody: false, 46 | }), 47 | ); 48 | 49 | // Initialise Oak application with router. 50 | const app = new Application(); 51 | app.use(router.routes()); 52 | app.use(router.allowedMethods()); 53 | 54 | const request = await superoak(app); 55 | 56 | request.get("/proxy") 57 | .end((err, res) => { 58 | targetServer.close(); 59 | expect(res.status).toEqual(statusCode.code); 60 | expect(res.text).toEqual(statusCode.text); 61 | expect(res.error).toBeDefined(); 62 | expect(res.error.toString()).toMatch(statusCode.toString); 63 | done(err); 64 | }); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /src/requestOptions.ts: -------------------------------------------------------------------------------- 1 | import type { ProxyState } from "../deps.ts"; 2 | import type { ProxyOptions } from "./types.ts"; 3 | 4 | export function parseUrl(state: ProxyState, ctx: any) { 5 | const req = state.src.req; 6 | const options = state.options; 7 | let url = state.params.url; 8 | 9 | if (typeof url === "function") { 10 | url = url(ctx); 11 | } 12 | 13 | url = new URL(`${req.url.search}${req.url.hash}`, url); 14 | 15 | const secure = typeof options.secure !== "undefined" 16 | ? options.secure 17 | : url.protocol === "https:" 18 | ? true 19 | : req.secure; 20 | 21 | url.protocol = secure ? "https:" : "http:"; 22 | 23 | return url; 24 | } 25 | 26 | function extendHeaders( 27 | baseHeaders: HeadersInit, 28 | reqHeaders: Headers, 29 | ignoreHeaders: string[], 30 | ): Headers { 31 | const headers = new Headers(baseHeaders); 32 | 33 | if (!reqHeaders) { 34 | return headers; 35 | } 36 | 37 | reqHeaders.forEach((value, field) => { 38 | if (!ignoreHeaders.includes(field.toLowerCase())) { 39 | headers.set(field, value); 40 | } 41 | }); 42 | 43 | return headers; 44 | } 45 | 46 | function reqHeaders(req: Request, options: ProxyOptions): Headers { 47 | const baseHeaders: HeadersInit = options.headers || {}; 48 | const ignoreHeaders = ["connection", "content-length"]; 49 | 50 | if (!options.preserveHostHeader) { 51 | ignoreHeaders.push("host"); 52 | } 53 | 54 | const headers = extendHeaders(baseHeaders, req.headers, ignoreHeaders); 55 | headers.set("connection", "close"); 56 | 57 | return headers; 58 | } 59 | 60 | export async function createRequestInit( 61 | req: any, 62 | options: ProxyOptions, 63 | ): Promise { 64 | const body = options.parseReqBody && req.hasBody 65 | ? await req.body({ type: "text", limit: options.reqBodyLimit ?? Infinity }) 66 | .value 67 | : null; 68 | 69 | return { 70 | body, 71 | cache: options.cache, 72 | credentials: options.credentials, 73 | integrity: options.integrity, 74 | keepalive: options.keepalive, 75 | mode: options.mode || "cors", 76 | redirect: options.redirect, 77 | referrer: options.referrer, 78 | referrerPolicy: options.referrerPolicy, 79 | signal: options.signal, 80 | headers: reqHeaders(req, options), 81 | method: options.method || req.method, 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /src/proxy.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createState, 3 | decorateProxyReqInit, 4 | decorateProxyReqUrl, 5 | decorateSrcRes, 6 | decorateSrcResHeaders, 7 | filterProxyRes, 8 | prepareProxyReq, 9 | ProxyUrlFunction, 10 | sendProxyReq, 11 | } from "../deps.ts"; 12 | import type { ProxyOptions } from "./types.ts"; 13 | import { filterSrcReq } from "./steps/filterSrcReq.ts"; 14 | import { buildProxyUrl } from "./steps/buildProxyUrl.ts"; 15 | import { buildProxyReqInit } from "./steps/buildProxyReqInit.ts"; 16 | import { copyProxyResHeadersToUserRes } from "./steps/copyProxyResHeadersToUserRes.ts"; 17 | import { sendSrcRes } from "./steps/sendSrcRes.ts"; 18 | import { handleProxyErrors } from "./steps/handleProxyErrors.ts"; 19 | 20 | /** 21 | * Takes a url argument that can be a string, URL or a function 22 | * that returns one of the previous to proxy requests to. The 23 | * remaining path from a request that has not been matched by 24 | * Oak will be appended to the provided url when making the 25 | * proxy request. 26 | * 27 | * Also accepts optional options configuration allowing the user 28 | * to modified all aspects of proxied request via option 29 | * properties or a series of hooks allowing decoration of the 30 | * outbound request and the inbound response objects. 31 | * 32 | * Requests and responses can also be filtered via the `filterReq` 33 | * and `filterRes` function options, allowing requests to bypass 34 | * the proxy. 35 | * 36 | * @param {string|URL|ProxyUrlFunction} url 37 | * @param {ProxyOptions} options 38 | * 39 | * @returns {Function} Oak proxy middleware 40 | * @public 41 | */ 42 | export function proxy( 43 | url: string | URL | ProxyUrlFunction, 44 | options: ProxyOptions = {}, 45 | ) { 46 | return async function handleProxy( 47 | ctx: any, 48 | next: any, 49 | ) { 50 | const state = createState(ctx.request, ctx.response, next, url, options); 51 | 52 | await filterSrcReq(state) 53 | .then(buildProxyUrl(ctx)) 54 | .then(decorateProxyReqUrl) 55 | .then(buildProxyReqInit) 56 | .then(decorateProxyReqInit) 57 | .then(prepareProxyReq) 58 | .then(sendProxyReq) 59 | .then(filterProxyRes) 60 | .then(copyProxyResHeadersToUserRes) 61 | .then(decorateSrcResHeaders) 62 | .then(decorateSrcRes) 63 | .then(sendSrcRes) 64 | .catch((err) => { 65 | if (err) { 66 | const resolver = (state.options.proxyErrorHandler) 67 | ? state.options.proxyErrorHandler 68 | : handleProxyErrors; 69 | 70 | resolver(err, ctx, next); 71 | } else { 72 | return next(); 73 | } 74 | }); 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /test/url.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "./support/utils.ts"; 2 | import { proxyTarget } from "./support/proxyTarget.ts"; 3 | import { expect, Oak, Opine, superoak } from "./deps.ts"; 4 | import { proxy } from "../mod.ts"; 5 | 6 | const { Application, Router } = Oak; 7 | const { opine } = Opine; 8 | 9 | const proxyRoutes = [ 10 | "/somePath/", 11 | "/somePath/longer/path", 12 | "/somePath/long/path/with/many/tokens", 13 | ]; 14 | 15 | describe("url: string", () => { 16 | proxyRoutes.forEach((path) => { 17 | it(`should work independently of "${path}" from the inbound request to proxy to the remote server (url: string)`, async (done) => { 18 | const proxyRouteFn = { 19 | method: "get", 20 | path: "/", 21 | fn: (req: any, res: any) => { 22 | res.json({ path }); 23 | }, 24 | }; 25 | 26 | const target = proxyTarget({ handlers: [proxyRouteFn] }); 27 | const targetPort = (target.addrs[0] as Deno.NetAddr).port; 28 | 29 | const router = new Router(); 30 | router.get("/somePath/(.*)", proxy(`http://localhost:${targetPort}`)); 31 | 32 | const app = new Application(); 33 | app.use(router.routes()); 34 | app.use(router.allowedMethods()); 35 | 36 | const request = await superoak(app); 37 | request.get(path) 38 | .expect(200) 39 | .end((err, res) => { 40 | expect(res.body.path).toEqual(path); 41 | target.close(); 42 | done(err); 43 | }); 44 | }); 45 | }); 46 | }); 47 | 48 | describe("url: URL", () => { 49 | proxyRoutes.forEach((path) => { 50 | it(`should use the unmatched path component of "${path}" from the inbound request to proxy to the remote server (url: URL)`, async (done) => { 51 | const proxyRouteFn = { 52 | method: "get", 53 | path: "/", 54 | fn: (req: any, res: any) => { 55 | res.json({ path }); 56 | }, 57 | }; 58 | 59 | const target = proxyTarget({ handlers: [proxyRouteFn] }); 60 | const targetPort = (target.addrs[0] as Deno.NetAddr).port; 61 | 62 | const router = new Router(); 63 | router.get( 64 | "/somePath/(.*)", 65 | proxy(new URL(`http://localhost:${targetPort}`)), 66 | ); 67 | 68 | const app = new Application(); 69 | app.use(router.routes()); 70 | app.use(router.allowedMethods()); 71 | 72 | const request = await superoak(app); 73 | request.get(path) 74 | .expect(200) 75 | .end((err, res) => { 76 | expect(res.body.path).toEqual(path); 77 | target.close(); 78 | done(err); 79 | }); 80 | }); 81 | }); 82 | }); 83 | 84 | describe("url: function", () => { 85 | it("should handle a dynamic url function", async (done) => { 86 | const firstProxyApp = opine(); 87 | const secondProxyApp = opine(); 88 | 89 | const firstPort = 10031; 90 | const secondPort = 10032; 91 | 92 | firstProxyApp.use("/", (_req, res) => res.sendStatus(204)); 93 | secondProxyApp.use("/", (_req, res) => res.sendStatus(200)); 94 | 95 | const firstServer = firstProxyApp.listen(firstPort); 96 | const secondServer = secondProxyApp.listen(secondPort); 97 | 98 | const router = new Router(); 99 | router.get( 100 | "/proxy/:port", 101 | proxy( 102 | (ctx) => `http://localhost:${ctx.params.port}`, 103 | { memoizeUrl: false }, 104 | ), 105 | ); 106 | 107 | const app = new Application(); 108 | app.use(router.routes()); 109 | app.use(router.allowedMethods()); 110 | 111 | (await superoak(app)) 112 | .get(`/proxy/${firstPort}`) 113 | .expect(204) 114 | .end(async (err) => { 115 | firstServer.close(); 116 | if (err) return done(err); 117 | 118 | (await superoak(app)) 119 | .get(`/proxy/${secondPort}`) 120 | .expect(200, (err) => { 121 | secondServer.close(); 122 | done(err); 123 | }); 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . All complaints will be reviewed and investigated promptly 64 | and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | . 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | . Translations are available at 128 | . 129 | -------------------------------------------------------------------------------- /test/streaming.test.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | import { describe, it } from "./support/utils.ts"; 3 | import { proxyTarget } from "./support/proxyTarget.ts"; 4 | import { expect, Oak } from "./deps.ts"; 5 | import { proxy, ProxyOptions } from "../mod.ts"; 6 | 7 | const { Application, Router } = Oak; 8 | 9 | function chunkingProxyServer() { 10 | const proxyRouteFn = [{ 11 | method: "get", 12 | path: "/stream", 13 | fn: function (_req: any, res: any) { 14 | let timer: number | undefined = undefined; 15 | let counter = 0; 16 | 17 | const body = new ReadableStream({ 18 | start(controller) { 19 | timer = setInterval(() => { 20 | if (counter > 3) { 21 | clearInterval(timer); 22 | controller.close(); 23 | 24 | return; 25 | } 26 | 27 | const message = `${counter}`; 28 | controller.enqueue(new TextEncoder().encode(message)); 29 | counter++; 30 | }, 50); 31 | }, 32 | 33 | cancel() { 34 | if (timer !== undefined) { 35 | clearInterval(timer); 36 | } 37 | }, 38 | }); 39 | 40 | res.end(body); 41 | }, 42 | }]; 43 | 44 | return proxyTarget({ port: 8309, timeout: 1000, handlers: proxyRouteFn }); 45 | } 46 | 47 | const decoder = new TextDecoder(); 48 | 49 | async function simulateUserRequest() { 50 | const response = await fetch("http://localhost:8308/stream"); 51 | const chunks = []; 52 | 53 | for await (const chunk of response.body!) { 54 | const decodedChunk = decoder.decode(chunk); 55 | chunks.push(decodedChunk); 56 | } 57 | 58 | return chunks; 59 | } 60 | 61 | async function startLocalServer(proxyOptions: ProxyOptions) { 62 | const router = new Router(); 63 | 64 | router.get("/stream", proxy("http://localhost:8309/stream", proxyOptions)); 65 | 66 | const app = new Application(); 67 | app.use(router.routes()); 68 | app.use(router.allowedMethods()); 69 | 70 | const controller = new AbortController(); 71 | const { signal } = controller; 72 | 73 | let listenerPromiseResolver!: () => void; 74 | 75 | const listenerPromise = new Promise((resolve) => { 76 | listenerPromiseResolver = resolve; 77 | }); 78 | 79 | app.addEventListener("listen", () => listenerPromiseResolver()); 80 | 81 | const serverPromise = app.listen({ 82 | hostname: "localhost", 83 | port: 8308, 84 | signal, 85 | }); 86 | 87 | await listenerPromise; 88 | 89 | return { controller, serverPromise }; 90 | } 91 | 92 | describe("streams / piped requests", function () { 93 | describe("when streaming options are truthy", function () { 94 | const TEST_CASES = [{ 95 | name: "vanilla, no options defined", 96 | options: {}, 97 | }, { 98 | name: "proxyReqOptDecorator is defined", 99 | options: { 100 | proxyReqInitDecorator: function (reqInit: any) { 101 | return reqInit; 102 | }, 103 | }, 104 | }, { 105 | name: "proxyReqOptDecorator is a Promise", 106 | options: { 107 | proxyReqInitDecorator: function (reqInit: any) { 108 | return Promise.resolve(reqInit); 109 | }, 110 | }, 111 | }]; 112 | 113 | TEST_CASES.forEach(function (testCase) { 114 | describe(testCase.name, function () { 115 | it( 116 | testCase.name + 117 | ": chunks are received without any buffering, e.g. before request end", 118 | async function (done) { 119 | const targetServer = chunkingProxyServer(); 120 | const { controller, serverPromise } = await startLocalServer( 121 | testCase.options, 122 | ); 123 | 124 | simulateUserRequest() 125 | .then(async function (res) { 126 | expect(res instanceof Array).toBeTruthy(); 127 | expect(res).toHaveLength(4); 128 | 129 | controller.abort(); 130 | await serverPromise; 131 | 132 | targetServer.close(); 133 | 134 | done(); 135 | }) 136 | .catch(async (error) => { 137 | controller.abort(); 138 | await serverPromise; 139 | 140 | targetServer.close(); 141 | 142 | done(error); 143 | }); 144 | }, 145 | ); 146 | }); 147 | }); 148 | }); 149 | 150 | describe("when streaming options are falsy", function () { 151 | const TEST_CASES = [{ 152 | name: "filterRes is defined", 153 | options: { 154 | filterRes: function () { 155 | return false; 156 | }, 157 | }, 158 | }]; 159 | 160 | TEST_CASES.forEach(function (testCase) { 161 | describe(testCase.name, function () { 162 | it("response arrives in one large chunk", async function (done) { 163 | const targetServer = chunkingProxyServer(); 164 | const { controller, serverPromise } = await startLocalServer( 165 | testCase.options, 166 | ); 167 | 168 | simulateUserRequest() 169 | .then(async function (res) { 170 | expect(res instanceof Array).toBeTruthy(); 171 | expect(res).toHaveLength(1); 172 | 173 | controller.abort(); 174 | await serverPromise; 175 | 176 | targetServer.close(); 177 | 178 | done(); 179 | }) 180 | .catch(async (error) => { 181 | controller.abort(); 182 | await serverPromise; 183 | 184 | targetServer.close(); 185 | 186 | done(error); 187 | }); 188 | }); 189 | }); 190 | }); 191 | }); 192 | }); 193 | -------------------------------------------------------------------------------- /docs/modules/_types_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "types" | oak-http-proxy 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 60 |

Module "types"

61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |

Index

69 |
70 |
71 |
72 |

Interfaces

73 | 76 |
77 |
78 |
79 |
80 |
81 | 124 |
125 |
126 |
127 |
128 |

Legend

129 |
130 |
    131 |
  • Function
  • 132 |
133 |
    134 |
  • Interface
  • 135 |
136 |
137 |
138 |
139 |
140 |

Generated using TypeDoc

141 |
142 |
143 | 144 | 145 | -------------------------------------------------------------------------------- /test/payload.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "./support/utils.ts"; 2 | import { expect, Oak, Opine, superoak } from "./deps.ts"; 3 | import { proxy } from "../mod.ts"; 4 | 5 | const { Application, Router } = Oak; 6 | const { opine, json, urlencoded } = Opine; 7 | 8 | describe("when making a proxy request with a payload", () => { 9 | const testCases = [ 10 | { name: "form encoded", encoding: "application/x-www-form-urlencoded" }, 11 | { name: "JSON encoded", encoding: "application/json" }, 12 | ]; 13 | 14 | testCases.forEach((test) => { 15 | it(`should deliver non-empty querystring params when ${test.name} (GET)`, async (done) => { 16 | // Set up target server to proxy through to. 17 | const target = opine(); 18 | target.use(json()); 19 | target.use(urlencoded()); 20 | 21 | target.get("/", (req, res) => { 22 | expect(req.query.name).toEqual("Deno"); 23 | expect(req.headers.get("content-type")).toEqual(test.encoding); 24 | res.json({ message: "Hello Deno!" }); 25 | }); 26 | 27 | const proxyServer = target.listen(); 28 | const proxyPort = (proxyServer.addrs[0] as Deno.NetAddr).port; 29 | 30 | // Setup our Oak server with proxy middleware. 31 | const router = new Router(); 32 | router.get("/proxy", proxy(`http://localhost:${proxyPort}`)); 33 | 34 | const app = new Application(); 35 | app.use(router.routes()); 36 | app.use(router.allowedMethods()); 37 | 38 | const request = await superoak(app); 39 | 40 | request.get("/proxy") 41 | .query({ name: "Deno" }) 42 | .set("Content-Type", test.encoding) 43 | .end((err, res) => { 44 | proxyServer.close(); 45 | expect(res.body.message).toEqual("Hello Deno!"); 46 | done(err); 47 | }); 48 | }); 49 | 50 | it(`should deliver an empty body when ${test.name} (POST)`, async (done) => { 51 | // Set up target server to proxy through to. 52 | const target = opine(); 53 | target.use(json()); 54 | target.use(urlencoded()); 55 | 56 | target.post("/", (req, res) => { 57 | expect(req.parsedBody).toEqual({}); 58 | expect(req.headers.get("content-type")).toEqual(test.encoding); 59 | res.json({ message: "Hello Deno!" }); 60 | }); 61 | 62 | const proxyServer = target.listen(); 63 | const proxyPort = (proxyServer.addrs[0] as Deno.NetAddr).port; 64 | 65 | // Setup our Oak server with proxy middleware. 66 | const router = new Router(); 67 | router.post("/proxy", proxy(`http://localhost:${proxyPort}`)); 68 | 69 | const app = new Application(); 70 | app.use(router.routes()); 71 | app.use(router.allowedMethods()); 72 | 73 | const payload = test.encoding.includes("json") ? "{}" : ""; 74 | 75 | const request = await superoak(app); 76 | request.post("/proxy") 77 | .send(payload) 78 | .set("Content-Type", test.encoding) 79 | // https://github.com/oakserver/oak/issues/402 80 | .set("Content-Length", `${payload.length}`) 81 | .end((err, res) => { 82 | proxyServer.close(); 83 | expect(res.body.message).toEqual("Hello Deno!"); 84 | done(err); 85 | }); 86 | }); 87 | 88 | it(`should deliver a non-empty body when ${test.name} (POST)`, async (done) => { 89 | // Set up target server to proxy through to. 90 | const target = opine(); 91 | target.use(json()); 92 | target.use(urlencoded()); 93 | 94 | target.post("/", (req, res) => { 95 | expect(req.parsedBody.name).toEqual("Deno"); 96 | expect(req.headers.get("content-type")).toEqual(test.encoding); 97 | res.json({ message: "Hello Deno!" }); 98 | }); 99 | 100 | const proxyServer = target.listen(); 101 | const proxyPort = (proxyServer.addrs[0] as Deno.NetAddr).port; 102 | 103 | // Setup our Oak server with proxy middleware. 104 | const router = new Router(); 105 | router.post("/proxy", proxy(`http://localhost:${proxyPort}`)); 106 | 107 | const app = new Application(); 108 | app.use(router.routes()); 109 | app.use(router.allowedMethods()); 110 | 111 | const payload = test.encoding.includes("json") 112 | ? JSON.stringify({ name: "Deno" }) 113 | : "name=Deno"; 114 | 115 | const request = await superoak(app); 116 | request.post("/proxy") 117 | .send(payload) 118 | .set("Content-Type", test.encoding) 119 | // https://github.com/oakserver/oak/issues/402 120 | .set("Content-Length", `${payload.length}`) 121 | .end((err, res) => { 122 | proxyServer.close(); 123 | expect(res.body.message).toEqual("Hello Deno!"); 124 | done(err); 125 | }); 126 | }); 127 | 128 | it(`should not deliver a non-empty body when "parseReqBody" is for when ${test.name} (POST)`, async (done) => { 129 | // Set up target server to proxy through to. 130 | const target = opine(); 131 | target.use(json()); 132 | target.use(urlencoded()); 133 | 134 | target.post("/", (req, res) => { 135 | expect(req.parsedBody).toEqual({}); 136 | expect(req.headers.get("content-type")).toEqual(test.encoding); 137 | res.json({ message: "Hello Deno!" }); 138 | }); 139 | 140 | const proxyServer = target.listen(); 141 | const proxyPort = (proxyServer.addrs[0] as Deno.NetAddr).port; 142 | 143 | // Setup our Oak server with proxy middleware. 144 | const router = new Router(); 145 | router.post( 146 | "/proxy", 147 | proxy(`http://localhost:${proxyPort}`, { parseReqBody: false }), 148 | ); 149 | 150 | const app = new Application(); 151 | app.use(router.routes()); 152 | app.use(router.allowedMethods()); 153 | 154 | const request = await superoak(app); 155 | request.post("/proxy") 156 | .send(test.encoding.includes("json") ? { name: "Deno" } : "name=Deno") 157 | .set("Content-Type", test.encoding) 158 | .end((err, res) => { 159 | proxyServer.close(); 160 | expect(res.body.message).toEqual("Hello Deno!"); 161 | done(err); 162 | }); 163 | }); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /test/handleProxyError.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "./support/utils.ts"; 2 | import { proxyTarget } from "./support/proxyTarget.ts"; 3 | import { expect, Oak, superoak } from "./deps.ts"; 4 | import { proxy } from "../mod.ts"; 5 | 6 | const { Application } = Oak; 7 | 8 | const REMOTE_SERVER_LATENCY = 500; 9 | 10 | const timeoutManager = { 11 | resolver: () => {}, 12 | promise: Promise.resolve(), 13 | respond: true, 14 | reset() { 15 | this.respond = true; 16 | this.promise = new Promise((resolve) => { 17 | this.resolver = resolve; 18 | }); 19 | }, 20 | async close() { 21 | this.respond = false; 22 | return await this.end(); 23 | }, 24 | async end() { 25 | return await this.promise; 26 | }, 27 | }; 28 | 29 | const proxyRouteFn = [ 30 | { 31 | method: "get", 32 | path: "/:errorCode", 33 | fn: async (req: any, res: any) => { 34 | setTimeout(() => { 35 | timeoutManager.resolver(); 36 | 37 | if (timeoutManager.respond) { 38 | const errorCode = req.params.errorCode; 39 | 40 | if (errorCode === "timeout") { 41 | return res.setStatus(504).send("test-timeout"); 42 | } 43 | 44 | res.setStatus(parseInt(errorCode)).send("test-case-error"); 45 | } 46 | }, REMOTE_SERVER_LATENCY); 47 | }, 48 | }, 49 | ]; 50 | 51 | describe("error handling can be overridden by user", () => { 52 | describe("when user provides a null function", () => { 53 | describe("when a timeout is set that fires", () => { 54 | it("passes 504 directly to client", async (done) => { 55 | timeoutManager.reset(); 56 | const targetServer = proxyTarget({ handlers: proxyRouteFn }); 57 | const targetPort = (targetServer.addrs[0] as Deno.NetAddr).port; 58 | 59 | const app = new Application(); 60 | app.use(proxy(`http://localhost:${targetPort}/200`, { timeout: 0 })); 61 | 62 | const request = await superoak(app); 63 | 64 | request.get("/") 65 | .end(async (err, res) => { 66 | await timeoutManager.close(); 67 | targetServer.close(); 68 | expect(res.status).toEqual(504); 69 | expect(res.header["x-timeout-reason"]).toEqual( 70 | "oak-http-proxy reset the request.", 71 | ); 72 | done(); 73 | }); 74 | }); 75 | }); 76 | 77 | describe("when a timeout is not set", () => { 78 | it("passes status code (e.g. 504) directly to the client", async (done) => { 79 | timeoutManager.reset(); 80 | const targetServer = proxyTarget({ handlers: proxyRouteFn }); 81 | const targetPort = (targetServer.addrs[0] as Deno.NetAddr).port; 82 | 83 | const app = new Application(); 84 | app.use(proxy(`http://localhost:${targetPort}/504`)); 85 | 86 | const request = await superoak(app); 87 | 88 | request.get("/") 89 | .end(async (err, res) => { 90 | expect(res.status).toEqual(504); 91 | expect(res.text).toEqual("test-case-error"); 92 | await timeoutManager.end(); 93 | targetServer.close(); 94 | done(); 95 | }); 96 | }); 97 | 98 | it("passes status code (e.g. 500) back to the client", async (done) => { 99 | timeoutManager.reset(); 100 | const targetServer = proxyTarget({ handlers: proxyRouteFn }); 101 | const targetPort = (targetServer.addrs[0] as Deno.NetAddr).port; 102 | 103 | const app = new Application(); 104 | app.use(proxy(`http://localhost:${targetPort}/500`)); 105 | 106 | const request = await superoak(app); 107 | 108 | request.get("/") 109 | .end(async (err, res) => { 110 | expect(res.status).toEqual(500); 111 | expect(res.text).toEqual("test-case-error"); 112 | await timeoutManager.end(); 113 | targetServer.close(); 114 | done(); 115 | }); 116 | }); 117 | }); 118 | }); 119 | 120 | describe("when user provides a handler function", () => { 121 | const statusCode = 418; 122 | const message = 123 | "Whoever you are, and wherever you may be, friendship is always granted over a good cup of tea."; 124 | 125 | describe("when a timeout is set that fires", () => { 126 | it("should use the provided handler function passing on the timeout error", async (done) => { 127 | timeoutManager.reset(); 128 | const targetServer = proxyTarget({ handlers: proxyRouteFn }); 129 | const targetPort = (targetServer.addrs[0] as Deno.NetAddr).port; 130 | 131 | const app = new Application(); 132 | app.use(proxy(`http://localhost:${targetPort}/200`, { 133 | timeout: 1, 134 | proxyErrorHandler: (err, ctx, next) => { 135 | ctx.response.status = statusCode; 136 | ctx.response.body = message; 137 | }, 138 | })); 139 | 140 | const request = await superoak(app); 141 | 142 | request.get("/") 143 | .end(async (err, res) => { 144 | await timeoutManager.close(); 145 | targetServer.close(); 146 | expect(res.status).toEqual(statusCode); 147 | expect(res.text).toEqual(message); 148 | done(); 149 | }); 150 | }); 151 | }); 152 | 153 | describe("when the remote server is down", () => { 154 | it("should use the provided handler function passing on the connection refused error", async (done) => { 155 | const targetServer = proxyTarget({ handlers: proxyRouteFn }); 156 | const targetPort = (targetServer.addrs[0] as Deno.NetAddr).port; 157 | targetServer.close(); 158 | 159 | const app = new Application(); 160 | app.use(proxy(`http://localhost:${targetPort}/200`, { 161 | proxyErrorHandler: (err, ctx, next) => { 162 | ctx.response.status = statusCode; 163 | ctx.response.body = message; 164 | }, 165 | })); 166 | 167 | const request = await superoak(app); 168 | 169 | request.get("/") 170 | .end((err, res) => { 171 | expect(res.status).toEqual(statusCode); 172 | expect(res.text).toEqual(message); 173 | done(); 174 | }); 175 | }); 176 | }); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /test/filterReq.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "./support/utils.ts"; 2 | import { proxyTarget } from "./support/proxyTarget.ts"; 3 | import { Oak, superoak } from "./deps.ts"; 4 | import { proxy } from "../mod.ts"; 5 | 6 | const { Application } = Oak; 7 | 8 | const proxyRouteFn = [{ 9 | method: "get", 10 | path: "/", 11 | fn: (req: any, res: any) => { 12 | return res.setStatus(200).send("Proxy Server"); 13 | }, 14 | }]; 15 | 16 | function nextMethod(ctx: any, next: any) { 17 | ctx.response.status = 201; 18 | ctx.response.body = "Client Application"; 19 | } 20 | 21 | async function nextMethodThatReliesOnPrivateStatesOfOak( 22 | ctx: Oak.Context, 23 | next: any, 24 | ) { 25 | await ctx.send({ root: `${Deno.cwd()}/test`, index: "index.test.html" }); 26 | ctx.response.status = 201; 27 | } 28 | 29 | describe("filterReq", () => { 30 | it("should continue route processing when the filter function returns false (i.e. don't filter)", async (done) => { 31 | const proxyServer = proxyTarget({ handlers: proxyRouteFn }); 32 | const proxyPort = (proxyServer.addrs[0] as Deno.NetAddr).port; 33 | 34 | const app = new Application(); 35 | app.use(proxy(`http://localhost:${proxyPort}`, { 36 | filterReq: () => false, 37 | })); 38 | app.use(nextMethod); 39 | 40 | const request = await superoak(app); 41 | 42 | request.get("/") 43 | .expect(200) 44 | .end((err) => { 45 | proxyServer.close(); 46 | done(err); 47 | }); 48 | }); 49 | 50 | it("should stop route processing when the filter function returns true (i.e. filter)", async (done) => { 51 | const proxyServer = proxyTarget({ handlers: proxyRouteFn }); 52 | const proxyPort = (proxyServer.addrs[0] as Deno.NetAddr).port; 53 | 54 | const app = new Application(); 55 | app.use(proxy(`http://localhost:${proxyPort}`, { 56 | filterReq: () => true, 57 | })); 58 | app.use(nextMethod); 59 | 60 | const request = await superoak(app); 61 | 62 | request.get("/") 63 | .expect(201) 64 | .end((err) => { 65 | proxyServer.close(); 66 | done(err); 67 | }); 68 | }); 69 | 70 | it("should continue route processing when the filter function returns Promise (i.e. don't filter)", async (done) => { 71 | const proxyServer = proxyTarget({ handlers: proxyRouteFn }); 72 | const proxyPort = (proxyServer.addrs[0] as Deno.NetAddr).port; 73 | 74 | const app = new Application(); 75 | app.use(proxy(`http://localhost:${proxyPort}`, { 76 | filterReq: () => Promise.resolve(false), 77 | })); 78 | app.use(nextMethod); 79 | 80 | const request = await superoak(app); 81 | 82 | request.get("/") 83 | .expect(200) 84 | .end((err) => { 85 | proxyServer.close(); 86 | done(err); 87 | }); 88 | }); 89 | 90 | it("should stop route processing when the filter function returns Promise (i.e. filter)", async (done) => { 91 | const proxyServer = proxyTarget({ handlers: proxyRouteFn }); 92 | const proxyPort = (proxyServer.addrs[0] as Deno.NetAddr).port; 93 | 94 | const app = new Application(); 95 | app.use(proxy(`http://localhost:${proxyPort}`, { 96 | filterReq: () => Promise.resolve(true), 97 | })); 98 | app.use(nextMethod); 99 | 100 | const request = await superoak(app); 101 | 102 | request.get("/") 103 | .expect(201) 104 | .end((err) => { 105 | proxyServer.close(); 106 | done(err); 107 | }); 108 | }); 109 | 110 | it("should continue route processing when the filter function returns false (i.e. don't filter and don't serve static files)", async (done) => { 111 | const proxyServer = proxyTarget({ handlers: proxyRouteFn }); 112 | const proxyPort = (proxyServer.addrs[0] as Deno.NetAddr).port; 113 | 114 | const app = new Application(); 115 | app.use(proxy(`http://localhost:${proxyPort}`, { 116 | filterReq: () => false, 117 | })); 118 | app.use(nextMethodThatReliesOnPrivateStatesOfOak); 119 | 120 | const request = await superoak(app); 121 | 122 | request.get("/") 123 | .expect(200) 124 | .end((err) => { 125 | proxyServer.close(); 126 | done(err); 127 | }); 128 | }); 129 | 130 | it("should stop route processing when the filter function returns true (i.e. filter and serve static files)", async (done) => { 131 | const proxyServer = proxyTarget({ handlers: proxyRouteFn }); 132 | const proxyPort = (proxyServer.addrs[0] as Deno.NetAddr).port; 133 | 134 | const app = new Application(); 135 | app.use(proxy(`http://localhost:${proxyPort}`, { 136 | filterReq: () => true, 137 | })); 138 | app.use(nextMethodThatReliesOnPrivateStatesOfOak); 139 | 140 | const request = await superoak(app); 141 | 142 | request.get("/") 143 | .expect(201) 144 | .end((err) => { 145 | proxyServer.close(); 146 | done(err); 147 | }); 148 | }); 149 | 150 | it("should continue route processing when the filter function returns Promise (i.e. don't filter and don't serve static files)", async (done) => { 151 | const proxyServer = proxyTarget({ handlers: proxyRouteFn }); 152 | const proxyPort = (proxyServer.addrs[0] as Deno.NetAddr).port; 153 | 154 | const app = new Application(); 155 | app.use(proxy(`http://localhost:${proxyPort}`, { 156 | filterReq: () => Promise.resolve(false), 157 | })); 158 | app.use(nextMethodThatReliesOnPrivateStatesOfOak); 159 | 160 | const request = await superoak(app); 161 | 162 | request.get("/") 163 | .expect(200) 164 | .end((err) => { 165 | proxyServer.close(); 166 | done(err); 167 | }); 168 | }); 169 | 170 | it("should stop route processing when the filter function returns Promise (i.e. filter and serve static files)", async (done) => { 171 | const proxyServer = proxyTarget({ handlers: proxyRouteFn }); 172 | const proxyPort = (proxyServer.addrs[0] as Deno.NetAddr).port; 173 | 174 | const app = new Application(); 175 | app.use(proxy(`http://localhost:${proxyPort}`, { 176 | filterReq: () => Promise.resolve(true), 177 | })); 178 | app.use(nextMethodThatReliesOnPrivateStatesOfOak); 179 | 180 | const request = await superoak(app); 181 | 182 | request.get("/") 183 | .expect(201) 184 | .end((err) => { 185 | proxyServer.close(); 186 | done(err); 187 | }); 188 | }); 189 | }); 190 | -------------------------------------------------------------------------------- /docs/globals.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | oak-http-proxy 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 57 |

oak-http-proxy

58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |

Index

66 |
67 | 83 |
84 |
85 |
86 | 126 |
127 |
128 |
129 |
130 |

Legend

131 |
132 |
    133 |
  • Function
  • 134 |
135 |
    136 |
  • Interface
  • 137 |
138 |
139 |
140 |
141 |
142 |

Generated using TypeDoc

143 |
144 |
145 | 146 | 147 | -------------------------------------------------------------------------------- /docs/assets/js/search.json: -------------------------------------------------------------------------------- 1 | {"kinds":{"1":"Module","64":"Function","256":"Interface","1024":"Property"},"rows":[{"id":0,"kind":1,"name":"\"proxy\"","url":"modules/_proxy_.html","classes":"tsd-kind-module"},{"id":1,"kind":64,"name":"proxy","url":"modules/_proxy_.html#proxy","classes":"tsd-kind-function tsd-parent-kind-module","parent":"\"proxy\""},{"id":2,"kind":1,"name":"\"requestOptions\"","url":"modules/_requestoptions_.html","classes":"tsd-kind-module"},{"id":3,"kind":64,"name":"parseUrl","url":"modules/_requestoptions_.html#parseurl","classes":"tsd-kind-function tsd-parent-kind-module","parent":"\"requestOptions\""},{"id":4,"kind":64,"name":"extendHeaders","url":"modules/_requestoptions_.html#extendheaders","classes":"tsd-kind-function tsd-parent-kind-module tsd-is-not-exported","parent":"\"requestOptions\""},{"id":5,"kind":64,"name":"reqHeaders","url":"modules/_requestoptions_.html#reqheaders","classes":"tsd-kind-function tsd-parent-kind-module tsd-is-not-exported","parent":"\"requestOptions\""},{"id":6,"kind":64,"name":"createRequestInit","url":"modules/_requestoptions_.html#createrequestinit","classes":"tsd-kind-function tsd-parent-kind-module","parent":"\"requestOptions\""},{"id":7,"kind":1,"name":"\"steps/buildProxyReqInit\"","url":"modules/_steps_buildproxyreqinit_.html","classes":"tsd-kind-module"},{"id":8,"kind":64,"name":"buildProxyReqInit","url":"modules/_steps_buildproxyreqinit_.html#buildproxyreqinit","classes":"tsd-kind-function tsd-parent-kind-module","parent":"\"steps/buildProxyReqInit\""},{"id":9,"kind":1,"name":"\"steps/buildProxyUrl\"","url":"modules/_steps_buildproxyurl_.html","classes":"tsd-kind-module"},{"id":10,"kind":64,"name":"buildProxyUrl","url":"modules/_steps_buildproxyurl_.html#buildproxyurl","classes":"tsd-kind-function tsd-parent-kind-module","parent":"\"steps/buildProxyUrl\""},{"id":11,"kind":1,"name":"\"steps/copyProxyResHeadersToUserRes\"","url":"modules/_steps_copyproxyresheaderstouserres_.html","classes":"tsd-kind-module"},{"id":12,"kind":64,"name":"copyProxyResHeadersToUserRes","url":"modules/_steps_copyproxyresheaderstouserres_.html#copyproxyresheaderstouserres","classes":"tsd-kind-function tsd-parent-kind-module","parent":"\"steps/copyProxyResHeadersToUserRes\""},{"id":13,"kind":1,"name":"\"steps/filterSrcReq\"","url":"modules/_steps_filtersrcreq_.html","classes":"tsd-kind-module"},{"id":14,"kind":64,"name":"defaultFilter","url":"modules/_steps_filtersrcreq_.html#defaultfilter","classes":"tsd-kind-function tsd-parent-kind-module tsd-is-not-exported","parent":"\"steps/filterSrcReq\""},{"id":15,"kind":64,"name":"filterSrcReq","url":"modules/_steps_filtersrcreq_.html#filtersrcreq","classes":"tsd-kind-function tsd-parent-kind-module","parent":"\"steps/filterSrcReq\""},{"id":16,"kind":1,"name":"\"steps/handleProxyErrors\"","url":"modules/_steps_handleproxyerrors_.html","classes":"tsd-kind-module"},{"id":17,"kind":64,"name":"connectionResetHandler","url":"modules/_steps_handleproxyerrors_.html#connectionresethandler","classes":"tsd-kind-function tsd-parent-kind-module tsd-is-not-exported","parent":"\"steps/handleProxyErrors\""},{"id":18,"kind":64,"name":"handleProxyErrors","url":"modules/_steps_handleproxyerrors_.html#handleproxyerrors","classes":"tsd-kind-function tsd-parent-kind-module","parent":"\"steps/handleProxyErrors\""},{"id":19,"kind":1,"name":"\"steps/sendSrcRes\"","url":"modules/_steps_sendsrcres_.html","classes":"tsd-kind-module"},{"id":20,"kind":64,"name":"isNullBodyStatus","url":"modules/_steps_sendsrcres_.html#isnullbodystatus","classes":"tsd-kind-function tsd-parent-kind-module tsd-is-not-exported","parent":"\"steps/sendSrcRes\""},{"id":21,"kind":64,"name":"sendSrcRes","url":"modules/_steps_sendsrcres_.html#sendsrcres","classes":"tsd-kind-function tsd-parent-kind-module","parent":"\"steps/sendSrcRes\""},{"id":22,"kind":1,"name":"\"types\"","url":"modules/_types_.html","classes":"tsd-kind-module"},{"id":23,"kind":256,"name":"ProxyOptions","url":"interfaces/_types_.proxyoptions.html","classes":"tsd-kind-interface tsd-parent-kind-module","parent":"\"types\""},{"id":24,"kind":1024,"name":"reqBodyLimit","url":"interfaces/_types_.proxyoptions.html#reqbodylimit","classes":"tsd-kind-property tsd-parent-kind-interface","parent":"\"types\".ProxyOptions"}],"index":{"version":"2.3.9","fields":["name","parent"],"fieldVectors":[["name/0",[0,20.053]],["parent/0",[]],["name/1",[0,20.053]],["parent/1",[0,1.63]],["name/2",[1,15.533]],["parent/2",[]],["name/3",[2,28.526]],["parent/3",[1,1.263]],["name/4",[3,28.526]],["parent/4",[1,1.263]],["name/5",[4,28.526]],["parent/5",[1,1.263]],["name/6",[5,28.526]],["parent/6",[1,1.263]],["name/7",[6,23.418]],["parent/7",[]],["name/8",[7,28.526]],["parent/8",[6,1.904]],["name/9",[8,23.418]],["parent/9",[]],["name/10",[9,28.526]],["parent/10",[8,1.904]],["name/11",[10,23.418]],["parent/11",[]],["name/12",[11,28.526]],["parent/12",[10,1.904]],["name/13",[12,20.053]],["parent/13",[]],["name/14",[13,28.526]],["parent/14",[12,1.63]],["name/15",[14,28.526]],["parent/15",[12,1.63]],["name/16",[15,20.053]],["parent/16",[]],["name/17",[16,28.526]],["parent/17",[15,1.63]],["name/18",[17,28.526]],["parent/18",[15,1.63]],["name/19",[18,20.053]],["parent/19",[]],["name/20",[19,28.526]],["parent/20",[18,1.63]],["name/21",[20,28.526]],["parent/21",[18,1.63]],["name/22",[21,23.418]],["parent/22",[]],["name/23",[22,28.526]],["parent/23",[21,1.904]],["name/24",[23,28.526]],["parent/24",[24,2.319]]],"invertedIndex":[["buildproxyreqinit",{"_index":7,"name":{"8":{}},"parent":{}}],["buildproxyurl",{"_index":9,"name":{"10":{}},"parent":{}}],["connectionresethandler",{"_index":16,"name":{"17":{}},"parent":{}}],["copyproxyresheaderstouserres",{"_index":11,"name":{"12":{}},"parent":{}}],["createrequestinit",{"_index":5,"name":{"6":{}},"parent":{}}],["defaultfilter",{"_index":13,"name":{"14":{}},"parent":{}}],["extendheaders",{"_index":3,"name":{"4":{}},"parent":{}}],["filtersrcreq",{"_index":14,"name":{"15":{}},"parent":{}}],["handleproxyerrors",{"_index":17,"name":{"18":{}},"parent":{}}],["isnullbodystatus",{"_index":19,"name":{"20":{}},"parent":{}}],["parseurl",{"_index":2,"name":{"3":{}},"parent":{}}],["proxy",{"_index":0,"name":{"0":{},"1":{}},"parent":{"1":{}}}],["proxyoptions",{"_index":22,"name":{"23":{}},"parent":{}}],["reqbodylimit",{"_index":23,"name":{"24":{}},"parent":{}}],["reqheaders",{"_index":4,"name":{"5":{}},"parent":{}}],["requestoptions",{"_index":1,"name":{"2":{}},"parent":{"3":{},"4":{},"5":{},"6":{}}}],["sendsrcres",{"_index":20,"name":{"21":{}},"parent":{}}],["steps/buildproxyreqinit",{"_index":6,"name":{"7":{}},"parent":{"8":{}}}],["steps/buildproxyurl",{"_index":8,"name":{"9":{}},"parent":{"10":{}}}],["steps/copyproxyresheaderstouserres",{"_index":10,"name":{"11":{}},"parent":{"12":{}}}],["steps/filtersrcreq",{"_index":12,"name":{"13":{}},"parent":{"14":{},"15":{}}}],["steps/handleproxyerrors",{"_index":15,"name":{"16":{}},"parent":{"17":{},"18":{}}}],["steps/sendsrcres",{"_index":18,"name":{"19":{}},"parent":{"20":{},"21":{}}}],["types",{"_index":21,"name":{"22":{}},"parent":{"23":{}}}],["types\".proxyoptions",{"_index":24,"name":{},"parent":{"24":{}}}]],"pipeline":[]}} -------------------------------------------------------------------------------- /docs/modules/_steps_buildproxyurl_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "steps/buildProxyUrl" | oak-http-proxy 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 60 |

Module "steps/buildProxyUrl"

61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |

Index

69 |
70 |
71 |
72 |

Functions

73 | 76 |
77 |
78 |
79 |
80 |
81 |

Functions

82 |
83 | 84 |

buildProxyUrl

85 |
    86 |
  • buildProxyUrl(ctx: any): (Anonymous function)
  • 87 |
88 |
    89 |
  • 90 | 95 |

    Parameters

    96 |
      97 |
    • 98 |
      ctx: any
      99 |
    • 100 |
    101 |

    Returns (Anonymous function)

    102 |
  • 103 |
104 |
105 |
106 |
107 | 150 |
151 |
152 |
153 |
154 |

Legend

155 |
156 |
    157 |
  • Function
  • 158 |
159 |
    160 |
  • Interface
  • 161 |
162 |
163 |
164 |
165 |
166 |

Generated using TypeDoc

167 |
168 |
169 | 170 | 171 | -------------------------------------------------------------------------------- /docs/modules/_steps_buildproxyreqinit_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "steps/buildProxyReqInit" | oak-http-proxy 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 60 |

Module "steps/buildProxyReqInit"

61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |

Index

69 |
70 |
71 |
72 |

Functions

73 | 76 |
77 |
78 |
79 |
80 |
81 |

Functions

82 |
83 | 84 |

buildProxyReqInit

85 |
    86 |
  • buildProxyReqInit(state: ProxyState): any
  • 87 |
88 |
    89 |
  • 90 | 95 |

    Parameters

    96 |
      97 |
    • 98 |
      state: ProxyState
      99 |
    • 100 |
    101 |

    Returns any

    102 |
  • 103 |
104 |
105 |
106 |
107 | 150 |
151 |
152 |
153 |
154 |

Legend

155 |
156 |
    157 |
  • Function
  • 158 |
159 |
    160 |
  • Interface
  • 161 |
162 |
163 |
164 |
165 |
166 |

Generated using TypeDoc

167 |
168 |
169 | 170 | 171 | -------------------------------------------------------------------------------- /docs/modules/_steps_copyproxyresheaderstouserres_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "steps/copyProxyResHeadersToUserRes" | oak-http-proxy 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 60 |

Module "steps/copyProxyResHeadersToUserRes"

61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |

Index

69 |
70 |
71 |
72 |

Functions

73 | 76 |
77 |
78 |
79 |
80 |
81 |

Functions

82 |
83 | 84 |

copyProxyResHeadersToUserRes

85 |
    86 |
  • copyProxyResHeadersToUserRes(state: ProxyState): any
  • 87 |
88 | 104 |
105 |
106 |
107 | 150 |
151 |
152 |
153 |
154 |

Legend

155 |
156 |
    157 |
  • Function
  • 158 |
159 |
    160 |
  • Interface
  • 161 |
162 |
163 |
164 |
165 |
166 |

Generated using TypeDoc

167 |
168 |
169 | 170 | 171 | -------------------------------------------------------------------------------- /docs/interfaces/_types_.proxyoptions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ProxyOptions | oak-http-proxy 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 63 |

Interface ProxyOptions

64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |

Hierarchy

72 |
    73 |
  • 74 | any 75 |
      76 |
    • 77 | ProxyOptions 78 |
    • 79 |
    80 |
  • 81 |
82 |
83 |
84 |

Index

85 |
86 |
87 |
88 |

Properties

89 | 92 |
93 |
94 |
95 |
96 |
97 |

Properties

98 |
99 | 100 |

Optional reqBodyLimit

101 |
reqBodyLimit: number
102 | 107 |
108 |
109 |

The request body size limit to use.

110 |
111 |
112 |
113 |
114 |
115 | 167 |
168 |
169 |
170 |
171 |

Legend

172 |
173 |
    174 |
  • Interface
  • 175 |
  • Property
  • 176 |
177 |
    178 |
  • Function
  • 179 |
180 |
181 |
182 |
183 |
184 |

Generated using TypeDoc

185 |
186 |
187 | 188 | 189 | -------------------------------------------------------------------------------- /docs/modules/_steps_filtersrcreq_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "steps/filterSrcReq" | oak-http-proxy 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 60 |

Module "steps/filterSrcReq"

61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |

Index

69 |
70 |
71 |
72 |

Functions

73 | 77 |
78 |
79 |
80 |
81 |
82 |

Functions

83 |
84 | 85 |

Const defaultFilter

86 |
    87 |
  • defaultFilter(): boolean
  • 88 |
89 | 99 |
100 |
101 | 102 |

filterSrcReq

103 |
    104 |
  • filterSrcReq(state: ProxyState): any
  • 105 |
106 |
    107 |
  • 108 | 113 |

    Parameters

    114 |
      115 |
    • 116 |
      state: ProxyState
      117 |
    • 118 |
    119 |

    Returns any

    120 |
  • 121 |
122 |
123 |
124 |
125 | 171 |
172 |
173 |
174 |
175 |

Legend

176 |
177 |
    178 |
  • Function
  • 179 |
180 |
    181 |
  • Interface
  • 182 |
183 |
184 |
185 |
186 |
187 |

Generated using TypeDoc

188 |
189 |
190 | 191 | 192 | -------------------------------------------------------------------------------- /docs/modules/_proxy_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "proxy" | oak-http-proxy 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 60 |

Module "proxy"

61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |

Index

69 |
70 |
71 |
72 |

Functions

73 | 76 |
77 |
78 |
79 |
80 |
81 |

Functions

82 |
83 | 84 |

proxy

85 |
    86 |
  • proxy(url: string | URL | ProxyUrlFunction, options?: ProxyOptions): handleProxy
  • 87 |
88 |
    89 |
  • 90 | 95 |
    96 |
    97 |

    Takes a url argument that can be a string, URL or a function 98 | that returns one of the previous to proxy requests to. The 99 | remaining path from a request that has not been matched by 100 | Oak will be appended to the provided url when making the 101 | proxy request.

    102 |
    103 |

    Also accepts optional options configuration allowing the user 104 | to modified all aspects of proxied request via option 105 | properties or a series of hooks allowing decoration of the 106 | outbound request and the inbound response objects.

    107 |

    Requests and responses can also be filtered via the filterReq 108 | and filterRes function options, allowing requests to bypass 109 | the proxy.

    110 |
    111 |

    Parameters

    112 |
      113 |
    • 114 |
      url: string | URL | ProxyUrlFunction
      115 |
    • 116 |
    • 117 |
      Default value options: ProxyOptions = {}
      118 |
      119 |
      120 |
    • 121 |
    122 |

    Returns handleProxy

    123 |

    Oak proxy middleware

    124 |
  • 125 |
126 |
127 |
128 |
129 | 172 |
173 |
174 |
175 |
176 |

Legend

177 |
178 |
    179 |
  • Function
  • 180 |
181 |
    182 |
  • Interface
  • 183 |
184 |
185 |
186 |
187 |
188 |

Generated using TypeDoc

189 |
190 |
191 | 192 | 193 | -------------------------------------------------------------------------------- /docs/modules/_steps_sendsrcres_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "steps/sendSrcRes" | oak-http-proxy 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 60 |

Module "steps/sendSrcRes"

61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |

Index

69 |
70 |
71 |
72 |

Functions

73 | 77 |
78 |
79 |
80 |
81 |
82 |

Functions

83 |
84 | 85 |

Const isNullBodyStatus

86 |
    87 |
  • isNullBodyStatus(status: number): boolean
  • 88 |
89 |
    90 |
  • 91 | 96 |

    Parameters

    97 |
      98 |
    • 99 |
      status: number
      100 |
    • 101 |
    102 |

    Returns boolean

    103 |
  • 104 |
105 |
106 |
107 | 108 |

sendSrcRes

109 |
    110 |
  • sendSrcRes(state: ProxyState): any
  • 111 |
112 |
    113 |
  • 114 | 119 |

    Parameters

    120 |
      121 |
    • 122 |
      state: ProxyState
      123 |
    • 124 |
    125 |

    Returns any

    126 |
  • 127 |
128 |
129 |
130 |
131 | 177 |
178 |
179 |
180 |
181 |

Legend

182 |
183 |
    184 |
  • Function
  • 185 |
186 |
    187 |
  • Interface
  • 188 |
189 |
190 |
191 |
192 |
193 |

Generated using TypeDoc

194 |
195 |
196 | 197 | 198 | -------------------------------------------------------------------------------- /docs/modules/_steps_handleproxyerrors_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "steps/handleProxyErrors" | oak-http-proxy 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 60 |

Module "steps/handleProxyErrors"

61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |

Index

69 |
70 |
71 |
72 |

Functions

73 | 77 |
78 |
79 |
80 |
81 |
82 |

Functions

83 |
84 | 85 |

connectionResetHandler

86 |
    87 |
  • connectionResetHandler(ctx: any): void
  • 88 |
89 |
    90 |
  • 91 | 96 |

    Parameters

    97 |
      98 |
    • 99 |
      ctx: any
      100 |
    • 101 |
    102 |

    Returns void

    103 |
  • 104 |
105 |
106 |
107 | 108 |

handleProxyErrors

109 |
    110 |
  • handleProxyErrors(err: any, ctx: any): void
  • 111 |
112 |
    113 |
  • 114 | 119 |

    Parameters

    120 |
      121 |
    • 122 |
      err: any
      123 |
    • 124 |
    • 125 |
      ctx: any
      126 |
    • 127 |
    128 |

    Returns void

    129 |
  • 130 |
131 |
132 |
133 |
134 | 180 |
181 |
182 |
183 |
184 |

Legend

185 |
186 |
    187 |
  • Function
  • 188 |
189 |
    190 |
  • Interface
  • 191 |
192 |
193 |
194 |
195 |
196 |

Generated using TypeDoc

197 |
198 |
199 | 200 | 201 | -------------------------------------------------------------------------------- /docs/modules/_requestoptions_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "requestOptions" | oak-http-proxy 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 60 |

Module "requestOptions"

61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |

Index

69 |
70 |
71 |
72 |

Functions

73 | 79 |
80 |
81 |
82 |
83 |
84 |

Functions

85 |
86 | 87 |

createRequestInit

88 |
    89 |
  • createRequestInit(req: any, options: ProxyOptions): Promise<RequestInit>
  • 90 |
91 |
    92 |
  • 93 | 98 |

    Parameters

    99 |
      100 |
    • 101 |
      req: any
      102 |
    • 103 |
    • 104 |
      options: ProxyOptions
      105 |
    • 106 |
    107 |

    Returns Promise<RequestInit>

    108 |
  • 109 |
110 |
111 |
112 | 113 |

extendHeaders

114 |
    115 |
  • extendHeaders(baseHeaders: HeadersInit, reqHeaders: Headers, ignoreHeaders: string[]): Headers
  • 116 |
117 |
    118 |
  • 119 | 124 |

    Parameters

    125 |
      126 |
    • 127 |
      baseHeaders: HeadersInit
      128 |
    • 129 |
    • 130 |
      reqHeaders: Headers
      131 |
    • 132 |
    • 133 |
      ignoreHeaders: string[]
      134 |
    • 135 |
    136 |

    Returns Headers

    137 |
  • 138 |
139 |
140 |
141 | 142 |

parseUrl

143 |
    144 |
  • parseUrl(state: ProxyState, ctx: any): any
  • 145 |
146 |
    147 |
  • 148 | 153 |

    Parameters

    154 |
      155 |
    • 156 |
      state: ProxyState
      157 |
    • 158 |
    • 159 |
      ctx: any
      160 |
    • 161 |
    162 |

    Returns any

    163 |
  • 164 |
165 |
166 |
167 | 168 |

reqHeaders

169 |
    170 |
  • reqHeaders(req: Request, options: ProxyOptions): Headers
  • 171 |
172 |
    173 |
  • 174 | 179 |

    Parameters

    180 |
      181 |
    • 182 |
      req: Request
      183 |
    • 184 |
    • 185 |
      options: ProxyOptions
      186 |
    • 187 |
    188 |

    Returns Headers

    189 |
  • 190 |
191 |
192 |
193 |
194 | 246 |
247 |
248 |
249 |
250 |

Legend

251 |
252 |
    253 |
  • Function
  • 254 |
255 |
    256 |
  • Interface
  • 257 |
258 |
259 |
260 |
261 |
262 |

Generated using TypeDoc

263 |
264 |
265 | 266 | 267 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # oak-http-proxy 2 | 3 | Proxy middleware for Deno Oak HTTP servers. 4 | 5 | [![GitHub tag](https://img.shields.io/github/tag/cmorten/oak-http-proxy)](https://github.com/cmorten/oak-http-proxy/tags/) ![Test](https://github.com/cmorten/oak-http-proxy/workflows/Test/badge.svg) [![deno doc](https://doc.deno.land/badge.svg)](https://doc.deno.land/https/deno.land/x/oak_http_proxy/mod.ts) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com) [![GitHub issues](https://img.shields.io/github/issues/cmorten/oak-http-proxy)](https://img.shields.io/github/issues/cmorten/oak-http-proxy) 6 | ![GitHub stars](https://img.shields.io/github/stars/cmorten/oak-http-proxy) ![GitHub forks](https://img.shields.io/github/forks/cmorten/oak-http-proxy) ![oak-http-proxy License](https://img.shields.io/github/license/cmorten/oak-http-proxy) [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://GitHub.com/cmorten/oak-http-proxy/graphs/commit-activity) 7 | 8 |

9 | oak-http-proxy latest /x/ version 10 | oak-http-proxy dependency count 11 | oak-http-proxy dependency outdatedness 12 | oak-http-proxy cached size 13 |

14 | 15 | ```ts 16 | import { proxy } from "https://deno.land/x/oak_http_proxy@2.3.0/mod.ts"; 17 | import { Application } from "https://deno.land/x/oak@v12.6.2/mod.ts"; 18 | 19 | const app = new Application(); 20 | 21 | app.use(proxy("https://github.com/oakserver/oak")); 22 | 23 | await app.listen({ port: 3000 }); 24 | ``` 25 | 26 | ## Installation 27 | 28 | This is a [Deno](https://deno.land/) module available to import direct from this repo and via the [Deno Registry](https://deno.land/x). 29 | 30 | Before importing, [download and install Deno](https://deno.land/#installation). 31 | 32 | You can then import oak-http-proxy straight into your project: 33 | 34 | ```ts 35 | import { proxy } from "https://deno.land/x/oak_http_proxy@2.3.0/mod.ts"; 36 | ``` 37 | 38 | oak-http-proxy is also available on [nest.land](https://nest.land/package/oak-http-proxy), a package registry for Deno on the Blockchain. 39 | 40 | ```ts 41 | import { proxy } from "https://x.nest.land/oak-http-proxy@2.3.0/mod.ts"; 42 | ``` 43 | 44 | ## Docs 45 | 46 | - [oak-http-proxy Type Docs](https://cmorten.github.io/oak-http-proxy/) 47 | - [oak-http-proxy Deno Docs](https://doc.deno.land/https/deno.land/x/oak_http_proxy/mod.ts) 48 | - [License](https://github.com/cmorten/oak-http-proxy/blob/main/LICENSE.md) 49 | - [Changelog](https://github.com/cmorten/oak-http-proxy/blob/main/.github/CHANGELOG.md) 50 | 51 | ## Usage 52 | 53 | ### URL 54 | 55 | The url argument that can be a string, URL or a function that returns a string or URL. This is used as the url to proxy requests to. Query string parameters and hashes are transferred from incoming request urls onto the proxy url. 56 | 57 | ```ts 58 | router.get("/string", proxy("http://google.com")); 59 | 60 | router.get("/url", proxy(new URL("http://google.com"))); 61 | 62 | router.get( 63 | "/function", 64 | proxy((ctx) => new URL("http://google.com")) 65 | ); 66 | ``` 67 | 68 | Note: Unmatched path segments of the incoming request url _are not_ transferred to the outbound proxy URL. For dynamic proxy urls use the function form. 69 | 70 | ### Streaming 71 | 72 | Proxy requests and user responses are piped/streamed/chunked by default. 73 | 74 | If you define a response modifier (`srcResDecorator`, `srcResHeaderDecorator`), 75 | or need to inspect the response before continuing (`filterRes`), streaming is 76 | disabled, and the request and response are buffered. This can cause performance 77 | issues with large payloads. 78 | 79 | ### Proxy Options 80 | 81 | You can also provide several options which allow you to filter, customize and decorate proxied requests and responses. 82 | 83 | ```ts 84 | app.use(proxy("http://google.com", proxyOptions)); 85 | ``` 86 | 87 | #### filterReq(req, res) (supports Promises) 88 | 89 | The `filterReq` option can be used to limit what requests are proxied. 90 | 91 | Return false to continue to execute the proxy; return true to skip the proxy for this request. 92 | 93 | ```ts 94 | app.use( 95 | "/proxy", 96 | proxy("www.google.com", { 97 | filterReq: (req, res) => { 98 | return req.method === "GET"; 99 | }, 100 | }) 101 | ); 102 | ``` 103 | 104 | Promise form: 105 | 106 | ```ts 107 | app.use( 108 | proxy("localhost:12346", { 109 | filterReq: (req, res) => { 110 | return new Promise((resolve) => { 111 | resolve(req.method === "GET"); 112 | }); 113 | }, 114 | }) 115 | ); 116 | ``` 117 | 118 | #### srcResDecorator(req, res, proxyRes, proxyResData) (supports Promise) 119 | 120 | Decorate the inbound response object from the proxied request. 121 | 122 | ```ts 123 | router.get( 124 | "/proxy", 125 | proxy("www.example.com", { 126 | srcResDecorator: (req, res, proxyRes, proxyResData) => { 127 | data = JSON.parse(new TextDecoder().decode(proxyResData)); 128 | data.newProperty = "exciting data"; 129 | 130 | return JSON.stringify(data); 131 | }, 132 | }) 133 | ); 134 | ``` 135 | 136 | ```ts 137 | app.use( 138 | proxy("httpbin.org", { 139 | srcResDecorator: (req, res, proxyRes, proxyResData) => { 140 | return new Promise((resolve) => { 141 | proxyResData.message = "Hello Deno!"; 142 | 143 | setTimeout(() => { 144 | resolve(proxyResData); 145 | }, 200); 146 | }); 147 | }, 148 | }) 149 | ); 150 | ``` 151 | 152 | ##### 304 - Not Modified 153 | 154 | When your proxied service returns 304 Not Modified this step will be skipped, since there should be no body to decorate. 155 | 156 | ##### Exploiting references 157 | 158 | The intent is that this be used to modify the proxy response data only. 159 | 160 | Note: The other arguments are passed by reference, so you _can_ currently exploit this to modify either response's headers, for instance, but this is not a reliable interface. 161 | 162 | #### memoizeUrl 163 | 164 | Defaults to `true`. 165 | 166 | When true, the `url` argument will be parsed on first request, and memoized for subsequent requests. 167 | 168 | When `false`, `url` argument will be parsed on each request. 169 | 170 | For example: 171 | 172 | ```ts 173 | function coinToss() { 174 | return Math.random() > 0.5; 175 | } 176 | 177 | function getUrl() { 178 | return coinToss() ? "http://yahoo.com" : "http://google.com"; 179 | } 180 | 181 | app.use( 182 | proxy(getUrl, { 183 | memoizeUrl: false, 184 | }) 185 | ); 186 | ``` 187 | 188 | In this example, when `memoizeUrl: false`, the coinToss occurs on each request, and each request could get either value. 189 | 190 | Conversely, When `memoizeUrl: true`, the coinToss would occur on the first request, and all additional requests would return the value resolved on the first request. 191 | 192 | ### srcResHeaderDecorator 193 | 194 | Decorate the inbound response headers from the proxied request. 195 | 196 | ```ts 197 | router.get( 198 | "/proxy", 199 | proxy("www.google.com", { 200 | srcResHeaderDecorator(headers, req, res, proxyReq, proxyRes) { 201 | return headers; 202 | }, 203 | }) 204 | ); 205 | ``` 206 | 207 | #### filterRes(proxyRes, proxyResData) (supports Promise form) 208 | 209 | Allows you to inspect the proxy response, and decide if you want to continue processing (via oak-http-proxy) or continue onto the next middleware. 210 | 211 | ```ts 212 | router.get( 213 | "/proxy", 214 | proxy("www.google.com", { 215 | filterRes(proxyRes) { 216 | return proxyRes.status === 404; 217 | }, 218 | }) 219 | ); 220 | ``` 221 | 222 | ### proxyErrorHandler 223 | 224 | By default, `oak-http-proxy` will throw any errors except `ECONNRESET` and `ECONTIMEDOUT` via `ctx.throw(err)`, so that your application can handle or react to them, or just drop through to your default error handling. 225 | 226 | If you would like to modify this behavior, you can provide your own `proxyErrorHandler`. 227 | 228 | ```ts 229 | // Example of skipping all error handling. 230 | 231 | app.use( 232 | proxy("localhost:12346", { 233 | proxyErrorHandler(err, ctx, next) { 234 | ctx.throw(err); 235 | }, 236 | }) 237 | ); 238 | 239 | // Example of rolling your own error handler 240 | 241 | app.use( 242 | proxy("localhost:12346", { 243 | proxyErrorHandler(err, ctx, next) { 244 | switch (err && err.code) { 245 | case "ECONNRESET": { 246 | ctx.response.status = 405; 247 | 248 | return; 249 | } 250 | case "ECONNREFUSED": { 251 | ctx.response.status = 200; 252 | 253 | return; 254 | } 255 | default: { 256 | ctx.throw(err); 257 | } 258 | } 259 | }, 260 | }) 261 | ); 262 | ``` 263 | 264 | #### proxyReqUrlDecorator(url, req) (supports Promise form) 265 | 266 | Decorate the outbound proxied request url. 267 | 268 | The returned url is used for the `fetch` method internally. 269 | 270 | ```ts 271 | router.get( 272 | "/proxy", 273 | proxy("www.google.com", { 274 | proxyReqUrlDecorator(url, req) { 275 | url.pathname = "/"; 276 | 277 | return url; 278 | }, 279 | }) 280 | ); 281 | ``` 282 | 283 | You can also use Promises: 284 | 285 | ```ts 286 | router.get( 287 | "/proxy", 288 | proxy("localhost:3000", { 289 | proxyReqOptDecorator(url, req) { 290 | return new Promise((resolve, reject) => { 291 | if (url.pathname === "/login") { 292 | url.port = 8080; 293 | } 294 | 295 | resolve(url); 296 | }); 297 | }, 298 | }) 299 | ); 300 | ``` 301 | 302 | Generally it is advised to use the function form for the passed URL argument as this provides the full context object whereas the `proxyReqOptDecorator` passes only the `context.request` object. 303 | 304 | Potential use cases for `proxyReqOptDecorator` include: 305 | 306 | - Overriding default protocol behaviour. 307 | - Overriding default query-string and hash transfer behaviour. 308 | 309 | #### proxyReqInitDecorator(proxyReqOpts, req) (supports Promise form) 310 | 311 | Decorate the outbound proxied request initialization options. 312 | 313 | This configuration will be used within the `fetch` method internally to make the request to the provided url. 314 | 315 | ```ts 316 | router.get( 317 | "/proxy", 318 | proxy("www.google.com", { 319 | proxyReqInitDecorator(proxyReqOpts, srcReq) { 320 | // you can update headers 321 | proxyReqOpts.headers.set("Content-Type", "text/html"); 322 | // you can change the method 323 | proxyReqOpts.method = "GET"; 324 | 325 | return proxyReqOpts; 326 | }, 327 | }) 328 | ); 329 | ``` 330 | 331 | You can also use Promises: 332 | 333 | ```ts 334 | router.get( 335 | "/proxy", 336 | proxy("www.google.com", { 337 | proxyReqOptDecorator(proxyReqOpts, srcReq) { 338 | return new Promise((resolve, reject) => { 339 | proxyReqOpts.headers.set("Content-Type", "text/html"); 340 | 341 | resolve(proxyReqOpts); 342 | }); 343 | }, 344 | }) 345 | ); 346 | ``` 347 | 348 | #### secure 349 | 350 | Normally, your proxy request will be made on the same protocol as the `url` parameter. If you'd like to force the proxy request to be https, use this option. 351 | 352 | ```ts 353 | app.use( 354 | "/proxy", 355 | proxy("http://www.google.com", { 356 | secure: true, 357 | }) 358 | ); 359 | ``` 360 | 361 | Note: if the proxy is passed a url without a protocol then HTTP will be used by default unless overridden by this option. 362 | 363 | #### preserveHostHeader 364 | 365 | You can copy the host HTTP header to the proxied Oak server using the `preserveHostHeader` option. 366 | 367 | ```ts 368 | router.get( 369 | "/proxy", 370 | proxy("www.google.com", { 371 | preserveHostHeader: true, 372 | }) 373 | ); 374 | ``` 375 | 376 | #### parseReqBody 377 | 378 | The `parseReqBody` option allows you to control whether the request body should be parsed and sent with the proxied request. If set to `false` then an incoming request body will not be sent with the proxied request. 379 | 380 | #### reqAsBuffer 381 | 382 | Configure whether the proxied request body should be sent as a UInt8Array buffer. 383 | 384 | Ignored if `parseReqBody` is set to `false`. 385 | 386 | ```ts 387 | router.get( 388 | "/proxy", 389 | proxy("www.google.com", { 390 | reqAsBuffer: true, 391 | }) 392 | ); 393 | ``` 394 | 395 | #### reqBodyEncoding 396 | 397 | The request body encoding to use. Currently only "utf-8" is supported. 398 | 399 | Ignored if `parseReqBody` is set to `false`. 400 | 401 | ```ts 402 | router.get( 403 | "/post", 404 | proxy("httpbin.org", { 405 | reqBodyEncoding: "utf-8", 406 | }) 407 | ); 408 | ``` 409 | 410 | #### reqBodyLimit 411 | 412 | The request body size limit to use. 413 | 414 | Ignored if `reqBodyLimit` is set to `Infinity`. 415 | 416 | ```ts 417 | router.get( 418 | "/post", 419 | proxy("httpbin.org", { 420 | reqBodyLimit: 10_485_760, // 10MB 421 | }) 422 | ); 423 | ``` 424 | 425 | #### timeout 426 | 427 | Configure a timeout in ms for the outbound proxied request. 428 | 429 | If not provided the request will never time out. 430 | 431 | Timed-out requests will respond with 504 status code and a X-Timeout-Reason header. 432 | 433 | ```ts 434 | router.get( 435 | "/", 436 | proxy("httpbin.org", { 437 | timeout: 2000, // in milliseconds, two seconds 438 | }) 439 | ); 440 | ``` 441 | 442 | ## Contributing 443 | 444 | [Contributing guide](https://github.com/cmorten/oak-http-proxy/blob/main/.github/CONTRIBUTING.md) 445 | 446 | --- 447 | 448 | ## License 449 | 450 | oak-http-proxy is licensed under the [MIT License](./LICENSE.md). 451 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | oak-http-proxy 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | Menu 46 |
47 |
48 |
49 |
50 |
51 |
52 | 57 |

oak-http-proxy

58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | 66 |

oak-http-proxy

67 |
68 |

Proxy middleware for Deno Oak HTTP servers.

69 |

GitHub tag Test deno doc PRs Welcome GitHub issues 70 | GitHub stars GitHub forks oak-http-proxy License Maintenance

71 |

72 | oak-http-proxy latest /x/ version 73 | oak-http-proxy dependency count 74 | oak-http-proxy dependency outdatedness 75 | oak-http-proxy cached size 76 |

77 |
import { proxy } from "https://deno.land/x/oak_http_proxy@2.3.0/mod.ts";
 78 | import { Application } from "https://deno.land/x/oak@v12.6.2/mod.ts";
 79 | 
 80 | const app = new Application();
 81 | 
 82 | app.use(proxy("https://github.com/oakserver/oak"));
 83 | 
 84 | await app.listen({ port: 3000 });
 85 | 
86 | 87 |

Installation

88 |
89 |

This is a Deno module available to import direct from this repo and via the Deno Registry.

90 |

Before importing, download and install Deno.

91 |

You can then import oak-http-proxy straight into your project:

92 |
import { proxy } from "https://deno.land/x/oak_http_proxy@2.3.0/mod.ts";
 93 | 
94 |

oak-http-proxy is also available on nest.land, a package registry for Deno on the Blockchain.

95 |
import { proxy } from "https://x.nest.land/oak-http-proxy@2.3.0/mod.ts";
 96 | 
97 | 98 |

Docs

99 |
100 | 106 | 107 |

Usage

108 |
109 | 110 |

URL

111 |
112 |

The url argument that can be a string, URL or a function that returns a string or URL. This is used as the url to proxy requests to. Query string parameters and hashes are transferred from incoming request urls onto the proxy url.

113 |
router.get("/string", proxy("http://google.com"));
114 | 
115 | router.get("/url", proxy(new URL("http://google.com")));
116 | 
117 | router.get(
118 |   "/function",
119 |   proxy((ctx) => new URL("http://google.com"))
120 | );
121 | 
122 |

Note: Unmatched path segments of the incoming request url are not transferred to the outbound proxy URL. For dynamic proxy urls use the function form.

123 | 124 |

Streaming

125 |
126 |

Proxy requests and user responses are piped/streamed/chunked by default.

127 |

If you define a response modifier (srcResDecorator, srcResHeaderDecorator), 128 | or need to inspect the response before continuing (filterRes), streaming is 129 | disabled, and the request and response are buffered. This can cause performance 130 | issues with large payloads.

131 | 132 |

Proxy Options

133 |
134 |

You can also provide several options which allow you to filter, customize and decorate proxied requests and responses.

135 |
app.use(proxy("http://google.com", proxyOptions));
136 | 
137 | 138 |

filterReq(req, res) (supports Promises)

139 |
140 |

The filterReq option can be used to limit what requests are proxied.

141 |

Return false to continue to execute the proxy; return true to skip the proxy for this request.

142 |
app.use(
143 |   "/proxy",
144 |   proxy("www.google.com", {
145 |     filterReq: (req, res) => {
146 |       return req.method === "GET";
147 |     },
148 |   })
149 | );
150 | 
151 |

Promise form:

152 |
app.use(
153 |   proxy("localhost:12346", {
154 |     filterReq: (req, res) => {
155 |       return new Promise((resolve) => {
156 |         resolve(req.method === "GET");
157 |       });
158 |     },
159 |   })
160 | );
161 | 
162 | 163 |

srcResDecorator(req, res, proxyRes, proxyResData) (supports Promise)

164 |
165 |

Decorate the inbound response object from the proxied request.

166 |
router.get(
167 |   "/proxy",
168 |   proxy("www.example.com", {
169 |     srcResDecorator: (req, res, proxyRes, proxyResData) => {
170 |       data = JSON.parse(new TextDecoder().decode(proxyResData));
171 |       data.newProperty = "exciting data";
172 | 
173 |       return JSON.stringify(data);
174 |     },
175 |   })
176 | );
177 | 
178 |
app.use(
179 |   proxy("httpbin.org", {
180 |     srcResDecorator: (req, res, proxyRes, proxyResData) => {
181 |       return new Promise((resolve) => {
182 |         proxyResData.message = "Hello Deno!";
183 | 
184 |         setTimeout(() => {
185 |           resolve(proxyResData);
186 |         }, 200);
187 |       });
188 |     },
189 |   })
190 | );
191 | 
192 | 193 |
304 - Not Modified
194 |
195 |

When your proxied service returns 304 Not Modified this step will be skipped, since there should be no body to decorate.

196 | 197 |
Exploiting references
198 |
199 |

The intent is that this be used to modify the proxy response data only.

200 |

Note: The other arguments are passed by reference, so you can currently exploit this to modify either response's headers, for instance, but this is not a reliable interface.

201 | 202 |

memoizeUrl

203 |
204 |

Defaults to true.

205 |

When true, the url argument will be parsed on first request, and memoized for subsequent requests.

206 |

When false, url argument will be parsed on each request.

207 |

For example:

208 |
function coinToss() {
209 |   return Math.random() > 0.5;
210 | }
211 | 
212 | function getUrl() {
213 |   return coinToss() ? "http://yahoo.com" : "http://google.com";
214 | }
215 | 
216 | app.use(
217 |   proxy(getUrl, {
218 |     memoizeUrl: false,
219 |   })
220 | );
221 | 
222 |

In this example, when memoizeUrl: false, the coinToss occurs on each request, and each request could get either value.

223 |

Conversely, When memoizeUrl: true, the coinToss would occur on the first request, and all additional requests would return the value resolved on the first request.

224 | 225 |

srcResHeaderDecorator

226 |
227 |

Decorate the inbound response headers from the proxied request.

228 |
router.get(
229 |   "/proxy",
230 |   proxy("www.google.com", {
231 |     srcResHeaderDecorator(headers, req, res, proxyReq, proxyRes) {
232 |       return headers;
233 |     },
234 |   })
235 | );
236 | 
237 | 238 |

filterRes(proxyRes, proxyResData) (supports Promise form)

239 |
240 |

Allows you to inspect the proxy response, and decide if you want to continue processing (via oak-http-proxy) or continue onto the next middleware.

241 |
router.get(
242 |   "/proxy",
243 |   proxy("www.google.com", {
244 |     filterRes(proxyRes) {
245 |       return proxyRes.status === 404;
246 |     },
247 |   })
248 | );
249 | 
250 | 251 |

proxyErrorHandler

252 |
253 |

By default, oak-http-proxy will throw any errors except ECONNRESET and ECONTIMEDOUT via ctx.throw(err), so that your application can handle or react to them, or just drop through to your default error handling.

254 |

If you would like to modify this behavior, you can provide your own proxyErrorHandler.

255 |
// Example of skipping all error handling.
256 | 
257 | app.use(
258 |   proxy("localhost:12346", {
259 |     proxyErrorHandler(err, ctx, next) {
260 |       ctx.throw(err);
261 |     },
262 |   })
263 | );
264 | 
265 | // Example of rolling your own error handler
266 | 
267 | app.use(
268 |   proxy("localhost:12346", {
269 |     proxyErrorHandler(err, ctx, next) {
270 |       switch (err && err.code) {
271 |         case "ECONNRESET": {
272 |           ctx.response.status = 405;
273 | 
274 |           return;
275 |         }
276 |         case "ECONNREFUSED": {
277 |           ctx.response.status = 200;
278 | 
279 |           return;
280 |         }
281 |         default: {
282 |           ctx.throw(err);
283 |         }
284 |       }
285 |     },
286 |   })
287 | );
288 | 
289 | 290 |

proxyReqUrlDecorator(url, req) (supports Promise form)

291 |
292 |

Decorate the outbound proxied request url.

293 |

The returned url is used for the fetch method internally.

294 |
router.get(
295 |   "/proxy",
296 |   proxy("www.google.com", {
297 |     proxyReqUrlDecorator(url, req) {
298 |       url.pathname = "/";
299 | 
300 |       return url;
301 |     },
302 |   })
303 | );
304 | 
305 |

You can also use Promises:

306 |
router.get(
307 |   "/proxy",
308 |   proxy("localhost:3000", {
309 |     proxyReqOptDecorator(url, req) {
310 |       return new Promise((resolve, reject) => {
311 |         if (url.pathname === "/login") {
312 |           url.port = 8080;
313 |         }
314 | 
315 |         resolve(url);
316 |       });
317 |     },
318 |   })
319 | );
320 | 
321 |

Generally it is advised to use the function form for the passed URL argument as this provides the full context object whereas the proxyReqOptDecorator passes only the context.request object.

322 |

Potential use cases for proxyReqOptDecorator include:

323 |
    324 |
  • Overriding default protocol behaviour.
  • 325 |
  • Overriding default query-string and hash transfer behaviour.
  • 326 |
327 | 328 |

proxyReqInitDecorator(proxyReqOpts, req) (supports Promise form)

329 |
330 |

Decorate the outbound proxied request initialization options.

331 |

This configuration will be used within the fetch method internally to make the request to the provided url.

332 |
router.get(
333 |   "/proxy",
334 |   proxy("www.google.com", {
335 |     proxyReqInitDecorator(proxyReqOpts, srcReq) {
336 |       // you can update headers
337 |       proxyReqOpts.headers.set("Content-Type", "text/html");
338 |       // you can change the method
339 |       proxyReqOpts.method = "GET";
340 | 
341 |       return proxyReqOpts;
342 |     },
343 |   })
344 | );
345 | 
346 |

You can also use Promises:

347 |
router.get(
348 |   "/proxy",
349 |   proxy("www.google.com", {
350 |     proxyReqOptDecorator(proxyReqOpts, srcReq) {
351 |       return new Promise((resolve, reject) => {
352 |         proxyReqOpts.headers.set("Content-Type", "text/html");
353 | 
354 |         resolve(proxyReqOpts);
355 |       });
356 |     },
357 |   })
358 | );
359 | 
360 | 361 |

secure

362 |
363 |

Normally, your proxy request will be made on the same protocol as the url parameter. If you'd like to force the proxy request to be https, use this option.

364 |
app.use(
365 |   "/proxy",
366 |   proxy("http://www.google.com", {
367 |     secure: true,
368 |   })
369 | );
370 | 
371 |

Note: if the proxy is passed a url without a protocol then HTTP will be used by default unless overridden by this option.

372 | 373 |

preserveHostHeader

374 |
375 |

You can copy the host HTTP header to the proxied Oak server using the preserveHostHeader option.

376 |
router.get(
377 |   "/proxy",
378 |   proxy("www.google.com", {
379 |     preserveHostHeader: true,
380 |   })
381 | );
382 | 
383 | 384 |

parseReqBody

385 |
386 |

The parseReqBody option allows you to control whether the request body should be parsed and sent with the proxied request. If set to false then an incoming request body will not be sent with the proxied request.

387 | 388 |

reqAsBuffer

389 |
390 |

Configure whether the proxied request body should be sent as a UInt8Array buffer.

391 |

Ignored if parseReqBody is set to false.

392 |
router.get(
393 |   "/proxy",
394 |   proxy("www.google.com", {
395 |     reqAsBuffer: true,
396 |   })
397 | );
398 | 
399 | 400 |

reqBodyEncoding

401 |
402 |

The request body encoding to use. Currently only "utf-8" is supported.

403 |

Ignored if parseReqBody is set to false.

404 |
router.get(
405 |   "/post",
406 |   proxy("httpbin.org", {
407 |     reqBodyEncoding: "utf-8",
408 |   })
409 | );
410 | 
411 | 412 |

reqBodyLimit

413 |
414 |

The request body size limit to use.

415 |

Ignored if reqBodyLimit is set to Infinity.

416 |
router.get(
417 |   "/post",
418 |   proxy("httpbin.org", {
419 |     reqBodyLimit: 10_485_760, // 10MB
420 |   })
421 | );
422 | 
423 | 424 |

timeout

425 |
426 |

Configure a timeout in ms for the outbound proxied request.

427 |

If not provided the request will never time out.

428 |

Timed-out requests will respond with 504 status code and a X-Timeout-Reason header.

429 |
router.get(
430 |   "/",
431 |   proxy("httpbin.org", {
432 |     timeout: 2000, // in milliseconds, two seconds
433 |   })
434 | );
435 | 
436 | 437 |

Contributing

438 |
439 |

Contributing guide

440 |
441 | 442 |

License

443 |
444 |

oak-http-proxy is licensed under the MIT License.

445 |
446 |
447 | 487 |
488 |
489 |
490 |
491 |

Legend

492 |
493 |
    494 |
  • Function
  • 495 |
496 |
    497 |
  • Interface
  • 498 |
499 |
500 |
501 |
502 |
503 |

Generated using TypeDoc

504 |
505 |
506 | 507 | 508 | --------------------------------------------------------------------------------