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 @@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
proxy
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
((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.
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