diff --git a/README.md b/README.md index f93228f..a7f304f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Proxy middleware for Deno Oak HTTP servers.

```ts -import { proxy } from "https://deno.land/x/oak_http_proxy@2.2.0/mod.ts"; +import { proxy } from "https://deno.land/x/oak_http_proxy@2.3.0/mod.ts"; import { Application } from "https://deno.land/x/oak@v12.6.2/mod.ts"; const app = new Application(); @@ -32,13 +32,13 @@ Before importing, [download and install Deno](https://deno.land/#installation). You can then import oak-http-proxy straight into your project: ```ts -import { proxy } from "https://deno.land/x/oak_http_proxy@2.2.0/mod.ts"; +import { proxy } from "https://deno.land/x/oak_http_proxy@2.3.0/mod.ts"; ``` oak-http-proxy is also available on [nest.land](https://nest.land/package/oak-http-proxy), a package registry for Deno on the Blockchain. ```ts -import { proxy } from "https://x.nest.land/oak-http-proxy@2.2.0/mod.ts"; +import { proxy } from "https://x.nest.land/oak-http-proxy@2.3.0/mod.ts"; ``` ## Docs @@ -67,6 +67,15 @@ router.get( 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. +### Streaming + +Proxy requests and user responses are piped/streamed/chunked by default. + +If you define a response modifier (`srcResDecorator`, `srcResHeaderDecorator`), +or need to inspect the response before continuing (`filterRes`), streaming is +disabled, and the request and response are buffered. This can cause performance +issues with large payloads. + ### Proxy Options You can also provide several options which allow you to filter, customize and decorate proxied requests and responses. diff --git a/deps.ts b/deps.ts index f7134fc..bc3c29c 100644 --- a/deps.ts +++ b/deps.ts @@ -1,15 +1,15 @@ export { STATUS_TEXT } from "https://deno.land/std@0.213.0/http/status.ts"; -export { createState } from "https://deno.land/x/opineHttpProxy@3.1.0/src/createState.ts"; +export { createState } from "https://deno.land/x/opineHttpProxy@3.2.0/src/createState.ts"; export type { ProxyState, ProxyUrlFunction, -} from "https://deno.land/x/opineHttpProxy@3.1.0/src/createState.ts"; -export type { ProxyOptions } from "https://deno.land/x/opineHttpProxy@3.1.0/src/resolveOptions.ts"; -export { isUnset } from "https://deno.land/x/opineHttpProxy@3.1.0/src/isUnset.ts"; -export { decorateProxyReqUrl } from "https://deno.land/x/opineHttpProxy@3.1.0/src/steps/decorateProxyReqUrl.ts"; -export { decorateProxyReqInit } from "https://deno.land/x/opineHttpProxy@3.1.0/src/steps/decorateProxyReqInit.ts"; -export { prepareProxyReq } from "https://deno.land/x/opineHttpProxy@3.1.0/src/steps/prepareProxyReq.ts"; -export { sendProxyReq } from "https://deno.land/x/opineHttpProxy@3.1.0/src/steps/sendProxyReq.ts"; -export { filterProxyRes } from "https://deno.land/x/opineHttpProxy@3.1.0/src/steps/filterProxyRes.ts"; -export { decorateSrcResHeaders } from "https://deno.land/x/opineHttpProxy@3.1.0/src/steps/decorateSrcResHeaders.ts"; -export { decorateSrcRes } from "https://deno.land/x/opineHttpProxy@3.1.0/src/steps/decorateSrcRes.ts"; +} from "https://deno.land/x/opineHttpProxy@3.2.0/src/createState.ts"; +export type { ProxyOptions } from "https://deno.land/x/opineHttpProxy@3.2.0/src/resolveOptions.ts"; +export { isUnset } from "https://deno.land/x/opineHttpProxy@3.2.0/src/isUnset.ts"; +export { decorateProxyReqUrl } from "https://deno.land/x/opineHttpProxy@3.2.0/src/steps/decorateProxyReqUrl.ts"; +export { decorateProxyReqInit } from "https://deno.land/x/opineHttpProxy@3.2.0/src/steps/decorateProxyReqInit.ts"; +export { prepareProxyReq } from "https://deno.land/x/opineHttpProxy@3.2.0/src/steps/prepareProxyReq.ts"; +export { sendProxyReq } from "https://deno.land/x/opineHttpProxy@3.2.0/src/steps/sendProxyReq.ts"; +export { filterProxyRes } from "https://deno.land/x/opineHttpProxy@3.2.0/src/steps/filterProxyRes.ts"; +export { decorateSrcResHeaders } from "https://deno.land/x/opineHttpProxy@3.2.0/src/steps/decorateSrcResHeaders.ts"; +export { decorateSrcRes } from "https://deno.land/x/opineHttpProxy@3.2.0/src/steps/decorateSrcRes.ts"; diff --git a/docs/index.html b/docs/index.html index 33a6b57..7b2b95f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -74,7 +74,7 @@

oak-http-proxy

oak-http-proxy dependency outdatedness oak-http-proxy cached size

-
import { proxy } from "https://deno.land/x/oak_http_proxy@2.2.0/mod.ts";
+				
import { proxy } from "https://deno.land/x/oak_http_proxy@2.3.0/mod.ts";
 import { Application } from "https://deno.land/x/oak@v12.6.2/mod.ts";
 
 const app = new Application();
@@ -89,10 +89,10 @@ 

Installation

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

Before importing, download and install Deno.

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

-
import { proxy } from "https://deno.land/x/oak_http_proxy@2.2.0/mod.ts";
+				
import { proxy } from "https://deno.land/x/oak_http_proxy@2.3.0/mod.ts";
 

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

-
import { proxy } from "https://x.nest.land/oak-http-proxy@2.2.0/mod.ts";
+				
import { proxy } from "https://x.nest.land/oak-http-proxy@2.3.0/mod.ts";
 

Docs

@@ -120,6 +120,14 @@

URL

);

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.

+
+

Streaming

+
+

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

+

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

Proxy Options

diff --git a/docs/interfaces/_types_.proxyoptions.html b/docs/interfaces/_types_.proxyoptions.html index 9e8c73a..47a1b1d 100644 --- a/docs/interfaces/_types_.proxyoptions.html +++ b/docs/interfaces/_types_.proxyoptions.html @@ -101,7 +101,7 @@

Optional reqBodyLimi
reqBodyLimit: number
diff --git a/docs/modules/_proxy_.html b/docs/modules/_proxy_.html index 4936573..c66dd4f 100644 --- a/docs/modules/_proxy_.html +++ b/docs/modules/_proxy_.html @@ -89,7 +89,7 @@

proxy

  • diff --git a/docs/modules/_requestoptions_.html b/docs/modules/_requestoptions_.html index 598e85c..a3b770f 100644 --- a/docs/modules/_requestoptions_.html +++ b/docs/modules/_requestoptions_.html @@ -92,7 +92,7 @@

    createRequestInit

  • Parameters

    @@ -118,7 +118,7 @@

    extendHeaders

  • Parameters

    @@ -147,7 +147,7 @@

    parseUrl

  • Parameters

    @@ -173,7 +173,7 @@

    reqHeaders

  • Parameters

    diff --git a/docs/modules/_steps_buildproxyreqinit_.html b/docs/modules/_steps_buildproxyreqinit_.html index 7c69da4..1a19c3e 100644 --- a/docs/modules/_steps_buildproxyreqinit_.html +++ b/docs/modules/_steps_buildproxyreqinit_.html @@ -89,7 +89,7 @@

    buildProxyReqInit

  • Parameters

    diff --git a/docs/modules/_steps_buildproxyurl_.html b/docs/modules/_steps_buildproxyurl_.html index 05abaee..ade924c 100644 --- a/docs/modules/_steps_buildproxyurl_.html +++ b/docs/modules/_steps_buildproxyurl_.html @@ -89,7 +89,7 @@

    buildProxyUrl

  • Parameters

    diff --git a/docs/modules/_steps_copyproxyresheaderstouserres_.html b/docs/modules/_steps_copyproxyresheaderstouserres_.html index 1cd8dc2..bd6f1b6 100644 --- a/docs/modules/_steps_copyproxyresheaderstouserres_.html +++ b/docs/modules/_steps_copyproxyresheaderstouserres_.html @@ -89,7 +89,7 @@

    copyProxyResHeadersToUserRes

  • Parameters

    diff --git a/docs/modules/_steps_filtersrcreq_.html b/docs/modules/_steps_filtersrcreq_.html index e68f871..7885186 100644 --- a/docs/modules/_steps_filtersrcreq_.html +++ b/docs/modules/_steps_filtersrcreq_.html @@ -90,7 +90,7 @@

    Const defaultFilter

  • Returns boolean

    @@ -107,7 +107,7 @@

    filterSrcReq

  • Parameters

    diff --git a/docs/modules/_steps_handleproxyerrors_.html b/docs/modules/_steps_handleproxyerrors_.html index a738511..83757bf 100644 --- a/docs/modules/_steps_handleproxyerrors_.html +++ b/docs/modules/_steps_handleproxyerrors_.html @@ -90,7 +90,7 @@

    connectionResetHandler

  • Parameters

    @@ -113,7 +113,7 @@

    handleProxyErrors

  • Parameters

    diff --git a/docs/modules/_steps_sendsrcres_.html b/docs/modules/_steps_sendsrcres_.html index 3a252f0..7b3fa87 100644 --- a/docs/modules/_steps_sendsrcres_.html +++ b/docs/modules/_steps_sendsrcres_.html @@ -90,7 +90,7 @@

    Const isNullBodySt
  • Parameters

    @@ -113,7 +113,7 @@

    sendSrcRes

  • Parameters

    diff --git a/egg.json b/egg.json index 35b3a1a..be12dc6 100644 --- a/egg.json +++ b/egg.json @@ -1,7 +1,7 @@ { "name": "oak-http-proxy", "description": "Proxy middleware for Deno Oak HTTP servers.", - "version": "2.2.0", + "version": "2.3.0", "repository": "https://github.com/cmorten/oak-http-proxy", "stable": true, "files": [ diff --git a/examples/basic/README.md b/examples/basic/README.md index 5f32395..4564f32 100644 --- a/examples/basic/README.md +++ b/examples/basic/README.md @@ -13,7 +13,7 @@ deno run --allow-net --allow-read ./examples/proxy/index.ts if have the repo cloned locally _OR_ ```bash -deno run --allow-net --allow-read https://deno.land/x/oak_http_proxy@2.2.0/examples/basic/index.ts +deno run --allow-net --allow-read https://deno.land/x/oak_http_proxy@2.3.0/examples/basic/index.ts ``` if you don't! diff --git a/examples/basic/index.ts b/examples/basic/index.ts index 2b4aae7..c16cd37 100644 --- a/examples/basic/index.ts +++ b/examples/basic/index.ts @@ -1,14 +1,13 @@ /** * Run this example using: - * + * * deno run --allow-net ./examples/basic/index.ts - * + * * if have the repo cloned locally OR - * - * deno run --allow-net https://deno.land/x/oak_http_proxy@2.2.0/examples/basic/index.ts - * + * + * deno run --allow-net https://deno.land/x/oak_http_proxy@2.3.0/examples/basic/index.ts + * * if you don't! - * */ import { proxy } from "../../mod.ts"; diff --git a/src/steps/sendSrcRes.ts b/src/steps/sendSrcRes.ts index 7e8d8cd..f51a9ca 100644 --- a/src/steps/sendSrcRes.ts +++ b/src/steps/sendSrcRes.ts @@ -4,7 +4,9 @@ const isNullBodyStatus = (status: number) => status === 101 || status === 204 || status === 205 || status === 304; export function sendSrcRes(state: ProxyState) { - if (!isNullBodyStatus(state.src.res.status)) { + if (state.options.stream) { + state.src.res.body = state.proxy.res?.body; + } else if (!isNullBodyStatus(state.src.res.status)) { state.src.res.body = state.proxy.resData; } diff --git a/test/streaming.test.ts b/test/streaming.test.ts new file mode 100644 index 0000000..c38504f --- /dev/null +++ b/test/streaming.test.ts @@ -0,0 +1,192 @@ +// deno-lint-ignore-file no-explicit-any +import { describe, it } from "./support/utils.ts"; +import { proxyTarget } from "./support/proxyTarget.ts"; +import { expect, Oak } from "./deps.ts"; +import { proxy, ProxyOptions } from "../mod.ts"; + +const { Application, Router } = Oak; + +function chunkingProxyServer() { + const proxyRouteFn = [{ + method: "get", + path: "/stream", + fn: function (_req: any, res: any) { + let timer: number | undefined = undefined; + let counter = 0; + + const body = new ReadableStream({ + start(controller) { + timer = setInterval(() => { + if (counter > 3) { + clearInterval(timer); + controller.close(); + + return; + } + + const message = `${counter}`; + controller.enqueue(new TextEncoder().encode(message)); + counter++; + }, 50); + }, + + cancel() { + if (timer !== undefined) { + clearInterval(timer); + } + }, + }); + + res.end(body); + }, + }]; + + return proxyTarget({ port: 8309, timeout: 1000, handlers: proxyRouteFn }); +} + +const decoder = new TextDecoder(); + +async function simulateUserRequest() { + const response = await fetch("http://localhost:8308/stream"); + const chunks = []; + + for await (const chunk of response.body!) { + const decodedChunk = decoder.decode(chunk); + chunks.push(decodedChunk); + } + + return chunks; +} + +async function startLocalServer(proxyOptions: ProxyOptions) { + const router = new Router(); + + router.get("/stream", proxy("http://localhost:8309/stream", proxyOptions)); + + const app = new Application(); + app.use(router.routes()); + app.use(router.allowedMethods()); + + const controller = new AbortController(); + const { signal } = controller; + + let listenerPromiseResolver!: () => void; + + const listenerPromise = new Promise((resolve) => { + listenerPromiseResolver = resolve; + }); + + app.addEventListener("listen", () => listenerPromiseResolver()); + + const serverPromise = app.listen({ + hostname: "localhost", + port: 8308, + signal, + }); + + await listenerPromise; + + return { controller, serverPromise }; +} + +describe("streams / piped requests", function () { + describe("when streaming options are truthy", function () { + const TEST_CASES = [{ + name: "vanilla, no options defined", + options: {}, + }, { + name: "proxyReqOptDecorator is defined", + options: { + proxyReqInitDecorator: function (reqInit: any) { + return reqInit; + }, + }, + }, { + name: "proxyReqOptDecorator is a Promise", + options: { + proxyReqInitDecorator: function (reqInit: any) { + return Promise.resolve(reqInit); + }, + }, + }]; + + TEST_CASES.forEach(function (testCase) { + describe(testCase.name, function () { + it( + testCase.name + + ": chunks are received without any buffering, e.g. before request end", + async function (done) { + const targetServer = chunkingProxyServer(); + const { controller, serverPromise } = await startLocalServer( + testCase.options, + ); + + simulateUserRequest() + .then(async function (res) { + expect(res instanceof Array).toBeTruthy(); + expect(res).toHaveLength(4); + + controller.abort(); + await serverPromise; + + targetServer.close(); + + done(); + }) + .catch(async (error) => { + controller.abort(); + await serverPromise; + + targetServer.close(); + + done(error); + }); + }, + ); + }); + }); + }); + + describe("when streaming options are falsy", function () { + const TEST_CASES = [{ + name: "filterRes is defined", + options: { + filterRes: function () { + return false; + }, + }, + }]; + + TEST_CASES.forEach(function (testCase) { + describe(testCase.name, function () { + it("response arrives in one large chunk", async function (done) { + const targetServer = chunkingProxyServer(); + const { controller, serverPromise } = await startLocalServer( + testCase.options, + ); + + simulateUserRequest() + .then(async function (res) { + expect(res instanceof Array).toBeTruthy(); + expect(res).toHaveLength(1); + + controller.abort(); + await serverPromise; + + targetServer.close(); + + done(); + }) + .catch(async (error) => { + controller.abort(); + await serverPromise; + + targetServer.close(); + + done(error); + }); + }); + }); + }); + }); +}); diff --git a/test/support/proxyTarget.ts b/test/support/proxyTarget.ts index ade6478..a2b152e 100644 --- a/test/support/proxyTarget.ts +++ b/test/support/proxyTarget.ts @@ -1,23 +1,50 @@ +import { ErrorRequestHandler } from "https://deno.land/x/opine@2.3.4/mod.ts"; import { Opine } from "../deps.ts"; const { opine, json, urlencoded } = Opine; export function proxyTarget( - { port = 0, handlers }: { + { port = 0, timeout = 100, handlers }: { port?: number; + timeout?: number; handlers?: any; - } = { port: 0 }, + } = { port: 0, timeout: 100 }, ) { const target = opine(); target.use(urlencoded()); target.use(json()); + target.use(function (_req, _res, next) { + setTimeout(function () { + next(); + }, timeout); + }); + if (handlers) { handlers.forEach((handler: any) => { (target as any)[handler.method](handler.path, handler.fn); }); } + target.get("/get", function (_req, res) { + res.send("OK"); + }); + + target.use("/headers", function (req, res) { + res.json({ headers: req.headers }); + }); + + target.use( + (function (err, _req, res, next) { + res.send(err); + next(); + }) as ErrorRequestHandler, + ); + + target.use(function (_req, res) { + res.sendStatus(404); + }); + return target.listen(port); } diff --git a/version.ts b/version.ts index ef19dd9..baa6408 100644 --- a/version.ts +++ b/version.ts @@ -1,7 +1,7 @@ /** * Version of oak-http-proxy. */ -export const VERSION = "2.2.0"; +export const VERSION = "2.3.0"; /** * Supported versions of Deno.