DEV Community

Craig Morten
Craig Morten

Posted on • Edited on

Proxy Middleware For Deno

Just finished putting together a loose port of express-http-proxy for the Deno Opine web framework called opine-http-proxy 🎉.

This middleware allows you to easily proxy requests to your Deno webserver off to an external / third party server, with several options for easily manipulating requests and responses.

import { proxy } from "https://deno.land/x/opineHttpProxy@2.1.0/mod.ts";
import { opine } from "https://deno.land/x/opine@0.21.2/mod.ts";

const app = opine();

app.use(proxy("https://github.com/asos-craigmorten/opine-http-proxy"));

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

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 opine-http-proxy straight into your project:

import { proxy } from "https://deno.land/x/opineHttpProxy@2.1.0/mod.ts";
Enter fullscreen mode Exit fullscreen mode

Usage

URL

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. The remaining path from a request that has not been matched by Opine will be appended to the provided url when making the proxied request.

app.get("/string", proxy("http://google.com"));

app.get("/url", proxy(new URL("http://google.com")));

app.get("/function", proxy(() => new URL("http://google.com")));
Enter fullscreen mode Exit fullscreen mode

Proxy Options

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

app.use(proxy("http://google.com", proxyOptions));
Enter fullscreen mode Exit fullscreen mode

filterReq(req, res) (supports Promises)

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

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

app.use(
  "/proxy",
  proxy("www.google.com", {
    filterReq: (req, res) => {
      return req.method === "GET";
    },
  })
);
Enter fullscreen mode Exit fullscreen mode

Promise form:

app.use(
  proxy("localhost:12346", {
    filterReq: (req, res) => {
      return new Promise((resolve) => {
        resolve(req.method === "GET");
      });
    },
  })
);
Enter fullscreen mode Exit fullscreen mode

Note that in the previous example, resolve(true) will execute the happy path for filter here (skipping the rest of the proxy, and calling next()). reject() will also skip the rest of proxy and call next().

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

Decorate the inbound response object from the proxied request.

app.use(
  "/proxy",
  proxy("www.google.com", {
    srcResDecorator: (req, res, proxyRes, proxyResData) => {
      data = JSON.parse(proxyResData.toString("utf8"));
      data.newProperty = "exciting data";

      return JSON.stringify(data);
    },
  })
);
Enter fullscreen mode Exit fullscreen mode
app.use(
  proxy("httpbin.org", {
    srcResDecorator: (req, res, proxyRes, proxyResData) => {
      return new Promise((resolve) => {
        proxyResData.message = "Hello Deno!";

        setTimeout(() => {
          resolve(proxyResData);
        }, 200);
      });
    },
  })
);
Enter fullscreen mode Exit fullscreen mode
304 - Not Modified

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

Exploiting references

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

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.

memoizeUrl

Defaults to true.

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

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

For example:

function coinToss() {
  return Math.random() > 0.5;
}

function getUrl() {
  return coinToss() ? "http://yahoo.com" : "http://google.com";
}

app.use(
  proxy(getUrl, {
    memoizeUrl: false,
  })
);
Enter fullscreen mode Exit fullscreen mode

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

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.

srcResHeaderDecorator

Decorate the inbound response headers from the proxied request.

app.use(
  "/proxy",
  proxy("www.google.com", {
    srcResHeaderDecorator(headers, req, res, proxyReq, proxyRes) {
      return headers;
    },
  })
);
Enter fullscreen mode Exit fullscreen mode

filterRes(proxyRes, proxyResData) (supports Promise form)

Allows you to inspect the proxy response, and decide if you want to continue processing (via opine-http-proxy) or call next() to return control to Opine.

app.use(
  "/proxy",
  proxy("www.google.com", {
    filterRes(proxyRes) {
      return proxyRes.status === 404;
    },
  })
);
Enter fullscreen mode Exit fullscreen mode

proxyErrorHandler

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

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

// Example of skipping all error handling.

app.use(
  proxy("localhost:12346", {
    proxyErrorHandler(err, res, next) {
      next(err);
    },
  })
);

// Example of rolling your own error handler

app.use(
  proxy("localhost:12346", {
    proxyErrorHandler(err, res, next) {
      switch (err && err.code) {
        case "ECONNRESET": {
          return res.sendStatus(405);
        }
        case "ECONNREFUSED": {
          return res.sendStatus(200);
        }
        default: {
          next(err);
        }
      }
    },
  })
);
Enter fullscreen mode Exit fullscreen mode

proxyReqUrlDecorator(url, req) (supports Promise form)

Decorate the outbound proxied request url.

The returned url is used for the fetch method internally.

app.use(
  "/proxy",
  proxy("www.google.com", {
    proxyReqUrlDecorator(url, req) {
      url.pathname = "/";

      return url;
    },
  })
);
Enter fullscreen mode Exit fullscreen mode

You can also use Promises:

app.use(
  "/proxy",
  proxy("localhost:3000", {
    proxyReqOptDecorator(url, req) {
      return new Promise((resolve, reject) => {
        if (url.pathname === "/login") {
          url.port = 8080;
        }

        resolve(url);
      });
    },
  })
);
Enter fullscreen mode Exit fullscreen mode

proxyReqInitDecorator(proxyReqOpts, req) (supports Promise form)

Decorate the outbound proxied request initialization options.

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

app.use(
  "/proxy",
  proxy("www.google.com", {
    proxyReqInitDecorator(proxyReqOpts, srcReq) {
      // you can update headers
      proxyReqOpts.headers.set("Content-Type", "text/html");
      // you can change the method
      proxyReqOpts.method = "GET";

      return proxyReqOpts;
    },
  })
);
Enter fullscreen mode Exit fullscreen mode

You can also use Promises:

app.use(
  "/proxy",
  proxy("www.google.com", {
    proxyReqOptDecorator(proxyReqOpts, srcReq) {
      return new Promise((resolve, reject) => {
        proxyReqOpts.headers.set("Content-Type", "text/html");

        resolve(proxyReqOpts);
      });
    },
  })
);
Enter fullscreen mode Exit fullscreen mode

secure

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.

app.use(
  "/proxy",
  proxy("http://www.google.com", {
    secure: true,
  })
);
Enter fullscreen mode Exit fullscreen mode

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

preserveHostHeader

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

app.use(
  "/proxy",
  proxy("www.google.com", {
    preserveHostHeader: true,
  })
);
Enter fullscreen mode Exit fullscreen mode

parseReqBody

The parseReqBody option allows you to control whether the request body should be parsed and sent with the proxied request.

reqAsBuffer

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

Ignored if parseReqBody is set to false.

app.use(
  "/proxy",
  proxy("www.google.com", {
    reqAsBuffer: true,
  })
);
Enter fullscreen mode Exit fullscreen mode

reqBodyEncoding

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

Ignored if parseReqBody is set to false.

app.use(
  "/post",
  proxy("httpbin.org", {
    reqBodyEncoding: "utf-8",
  })
);
Enter fullscreen mode Exit fullscreen mode

timeout

Configure a timeout in ms for the outbound proxied request.

If not provided the request will never time out.

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

app.use(
  "/",
  proxy("httpbin.org", {
    timeout: 2000, // in milliseconds, two seconds
  })
);
Enter fullscreen mode Exit fullscreen mode

Contributing

The project is very open to contributions and issues, or equally I'd love to hear your thoughts in the comments below!

What's next

Plan to write a similar proxy for the popular Oak framework in order to address one of the Oak issues requesting proxy middleware - if you want to collaborate leave a comment below.


Let me know what you think! What frameworks are you using with Deno? Drop questions, queries and comments below!

Top comments (0)