218

To avoid same-domain AJAX issues, I want my node.js web server to forward all requests from URL /api/BLABLA to another server, for example other_domain.com:3000/BLABLA, and return to user the same thing that this remote server returned, transparently.

All other URLs (beside /api/*) are to be served directly, no proxying.

How do I achieve this with node.js + express.js? Can you give a simple code example?

(both the web server and the remote 3000 server are under my control, both running node.js with express.js)


So far I found this https://github.com/http-party/node-http-proxy , but reading the documentation there didn't make me any wiser. I ended up with

var proxy = new httpProxy.RoutingProxy();
app.all("/api/*", function(req, res) {
    console.log("old request url " + req.url)
    req.url = '/' + req.url.split('/').slice(2).join('/'); // remove the '/api' part
    console.log("new request url " + req.url)
    proxy.proxyRequest(req, res, {
        host: "other_domain.com",
        port: 3000
    });
});

but nothing is returned to the original web server (or to the end user), so no luck.

tanguy_k
  • 11,307
  • 6
  • 54
  • 58
user124114
  • 8,372
  • 11
  • 41
  • 63
  • the way you do it is working for me, without any modifications – Saule Mar 29 '16 at 08:44
  • 1
    Although a bit too late to answer, but was facing similar issue and resolved it by removing body parser so that request body is not being parsed before proxying it further. – VyvIT Apr 12 '17 at 16:45

12 Answers12

240

Note that request has been deprecated as of February 2020, so I'll leave the answer below for historical reasons, but please consider moving to an alternative listed in this issue, moving to the natively built-in Fetch API (as of Node 18), or using express-http-proxy.

Original answer

I did something similar but I used request instead:

var request = require('request');
app.get('/', function(req,res) {
  //modify the url in any way you want
  var newurl = 'http://google.com/';
  request(newurl).pipe(res);
});
Mike 'Pomax' Kamermans
  • 49,297
  • 16
  • 112
  • 153
trigoman
  • 3,585
  • 2
  • 21
  • 20
  • 6
    Thanks, much simpler than using Node.js' HTTP request – Alex Turpin Aug 30 '13 at 15:13
  • 18
    Even simpler, if you also pipe the request: http://stackoverflow.com/questions/7559862/proxy-with-nodejs-and-express/20539239#20539239 – Stephan Hoyer Feb 28 '14 at 22:40
  • 1
    Nice and clean solution. I posted an answer to make it work with POST request also (otherwise it doesn't forward your post body to the API). If you edit your answer I'd be happy to remove mine. – Henrik Peinar Mar 11 '14 at 13:04
  • Also see [this answer](http://stackoverflow.com/a/20198377/132208) for improved error handling. – Tamlyn Oct 09 '14 at 11:17
  • whenever I try to do similar routing (or the exact same) I end up with the following: stream.js:94 throw er; // Unhandled stream error in pipe. ^ Error: getaddrinfo ENOTFOUND google.com at errnoException (dns.js:44:10) at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:94:26) any ideas? – keinabel May 20 '15 at 13:57
  • Can you pass the IP address through this somehow? – User Mar 17 '17 at 05:40
  • Doesn't seem to work anymore with `request`. Throws a `write after end` error. – Juicy May 24 '18 at 20:14
  • What does `pipe()` do here? does it return the complete response get from the remote service? – nish1013 Jul 10 '18 at 10:08
  • 1
    @Jonathan @trigoman now that `request` has been deprecated (see notice at https://github.com/request/request), what is the alternative? – drmrbrewer Mar 03 '20 at 15:27
  • 1
    Hi, what if I need to alter a string on the HTML body before piping out to the client? – FiNeX Aug 04 '21 at 09:28
  • 1
    [This](https://stackoverflow.com/a/66991063/3965) answer helped me with a non-deprecated solution using `node-fetch`. – Mr Rogers Nov 20 '21 at 19:40
138

I found a shorter and very straightforward solution which works seamlessly, and with authentication as well, using express-http-proxy:

const url = require('url');
const proxy = require('express-http-proxy');

// New hostname+path as specified by question:
const apiProxy = proxy('other_domain.com:3000/BLABLA', {
    proxyReqPathResolver: req => url.parse(req.baseUrl).path
});

And then simply:

app.use('/api/*', apiProxy);

Note: as mentioned by @MaxPRafferty, use req.originalUrl in place of baseUrl to preserve the querystring:

    forwardPath: req => url.parse(req.baseUrl).path

Update: As mentioned by Andrew (thank you!), there's a ready-made solution using the same principle:

npm i --save http-proxy-middleware

And then:

const proxy = require('http-proxy-middleware')
var apiProxy = proxy('/api', {target: 'http://www.example.org/api'});
app.use(apiProxy)

Documentation: http-proxy-middleware on Github

Mike 'Pomax' Kamermans
  • 49,297
  • 16
  • 112
  • 153
Selfish
  • 6,023
  • 4
  • 44
  • 63
  • 5
    The req.url doesn't have the full url, so updated the answer to use req.baseUrl instead of req.url – Vinoth Kumar Dec 22 '15 at 09:21
  • 4
    I also like to use req.originalUrl in place of baseUrl to preserve querystrings, but this may not always be the desired behavior. – MaxPRafferty Jan 07 '16 at 16:32
  • @MaxPRafferty - vaid comment. Worth noting. Thanks. – Selfish Jan 07 '16 at 16:54
  • 8
    This is the best solution. I'm using http-proxy-middleware, but it's the same concept. Don't spin your own proxy solution when there are great ones out there already. – Andrew Sep 07 '17 at 21:48
  • there's another package that is more simple to use npm install express-proxy-server --save var proxy = require('express-proxy-server'); app.use('/proxy', proxy('http://www.example.org/api')); – Mustafa Hussain Dec 23 '19 at 10:51
  • `http-proxy-middleware/dist/index.js' does not provide an export named 'default` – Dimitri Kopriwa Mar 04 '20 at 16:28
  • This resolved my issue. The only difference was the version of http-proxy-middleware that I installed did things slightly different which was easy to figure out following documentations. – Wildhammer May 21 '20 at 17:52
  • This helped me but I had to change forwardPath to proxyReqPathResolver. It looks like they deprecated forwardPath. – tanner burton Jun 22 '20 at 21:34
  • 1
    http-proxy-middleware depends on http-proxy package. The http-proxy has memory leaks when client closes connection prematurely. You can follow the issue here https://github.com/chimurai/http-proxy-middleware/issues/782 – Amir Saleem Dec 19 '22 at 07:28
  • 1
    @AmirSaleem thanks for the info! This answer is 7 years old - if there are any alternatives nowadays, I'd be happy to update it. – Selfish Dec 20 '22 at 09:05
69

You want to use http.request to create a similar request to the remote API and return its response.

Something like this:

const http = require('http');
// or use import http from 'http';


/* your app config here */

app.post('/api/BLABLA', (oreq, ores) => {
  const options = {
    // host to forward to
    host: 'www.google.com',
    // port to forward to
    port: 80,
    // path to forward to
    path: '/api/BLABLA',
    // request method
    method: 'POST',
    // headers to send
    headers: oreq.headers,
  };

  const creq = http
    .request(options, pres => {
      // set encoding
      pres.setEncoding('utf8');

      // set http status code based on proxied response
      ores.writeHead(pres.statusCode);

      // wait for data
      pres.on('data', chunk => {
        ores.write(chunk);
      });

      pres.on('close', () => {
        // closed, let's end client request as well
        ores.end();
      });

      pres.on('end', () => {
        // finished, let's finish client request as well
        ores.end();
      });
    })
    .on('error', e => {
      // we got an error
      console.log(e.message);
      try {
        // attempt to set error message and http status
        ores.writeHead(500);
        ores.write(e.message);
      } catch (e) {
        // ignore
      }
      ores.end();
    });

  creq.end();
});

Notice: I haven't really tried the above, so it might contain parse errors hopefully this will give you a hint as to how to get it to work.

mekwall
  • 28,614
  • 6
  • 75
  • 77
  • 12
    Yeah, some modifications were necessary, but I like this better than introducing an extra new "Proxy" module dependency. A bit verbose, but at least I know exactly what's going on. Cheers. – user124114 May 03 '12 at 17:35
  • It seems like you need to do res.writeHead before data chink is written, otherwise you'll get error (headers cant be written after body). – setec Jun 04 '15 at 16:09
  • 5
    @user124114 - please put the full solution that you have used – Michal Tsadok Oct 10 '16 at 10:23
  • 1
    seems that you'll have problem setting headers this way. `Cannot render headers after they are sent to the client` – Shnd Jun 25 '19 at 19:16
  • replace it like this: ```var preparedWrite = ''; // wait for data cres.on('data', function(chunk){ //res.write(chunk); preparedWrite = preparedWrite+chunk; }); cres.on('close', function(){ // closed, let's end client request as well res.writeHead(cres.statusCode); res.end(); }); cres.on('end', function(){ // finished, let's finish client request as well res.writeHead(cres.statusCode); res.write(preparedWrite); res.end(); }); ``` – chrikrah Dec 06 '19 at 23:55
  • 1
    I've updated the answer to es6 syntax and fixed the writeHead issue – mekwall Dec 17 '19 at 15:57
51

To extend trigoman's answer (full credits to him) to work with POST (could also make work with PUT etc):

app.use('/api', function(req, res) {
  var url = 'YOUR_API_BASE_URL'+ req.url;
  var r = null;
  if(req.method === 'POST') {
     r = request.post({uri: url, json: req.body});
  } else {
     r = request(url);
  }

  req.pipe(r).pipe(res);
});
Community
  • 1
  • 1
Henrik Peinar
  • 2,181
  • 18
  • 22
  • 2
    Couldn't make it work with PUT. But works great for GET and POST. Thank you!! – Mariano Desanze Aug 15 '14 at 16:59
  • 6
    @Protron for PUT requests just use something like `if(req.method === 'PUT'){ r = request.put({uri: url, json: req.body}); }` – davnicwil Sep 26 '14 at 20:34
  • If you need to pass through headers as part of a PUT or POST request, make sure to delete the content-length header so request can calculate it. Otherwise, the receiving server could truncate the data, which will lead to an error. – Carlos Rymer Oct 21 '15 at 13:12
  • @Henrik Peinar, will this help when i do a login post request and expect to redirect from web.com/api/login to web.com/ – valik Apr 30 '20 at 09:24
29

I used the following setup to direct everything on /rest to my backend server (on port 8080), and all other requests to the frontend server (a webpack server on port 3001). It supports all HTTP-methods, doesn't lose any request meta-info and supports websockets (which I need for hot reloading)

var express  = require('express');
var app      = express();
var httpProxy = require('http-proxy');
var apiProxy = httpProxy.createProxyServer();
var backend = 'http://localhost:8080',
    frontend = 'http://localhost:3001';

app.all("/rest/*", function(req, res) {
  apiProxy.web(req, res, {target: backend});
});

app.all("/*", function(req, res) {
    apiProxy.web(req, res, {target: frontend});
});

var server = require('http').createServer(app);
server.on('upgrade', function (req, socket, head) {
  apiProxy.ws(req, socket, head, {target: frontend});
});
server.listen(3000);
Anthony De Smet
  • 2,265
  • 3
  • 17
  • 24
19

First install express and http-proxy-middleware

npm install express http-proxy-middleware --save

Then in your server.js

const express = require('express');
const proxy = require('http-proxy-middleware');

const app = express();
app.use(express.static('client'));

// Add middleware for http proxying 
const apiProxy = proxy('/api', { target: 'http://localhost:8080' });
app.use('/api', apiProxy);

// Render your site
const renderIndex = (req, res) => {
  res.sendFile(path.resolve(__dirname, 'client/index.html'));
}
app.get('/*', renderIndex);

app.listen(3000, () => {
  console.log('Listening on: http://localhost:3000');
});

In this example we serve the site on port 3000, but when a request end with /api we redirect it to localhost:8080.

http://localhost:3000/api/login redirect to http://localhost:8080/api/login

C. Dupetit
  • 191
  • 1
  • 4
7

Ok, here's a ready-to-copy-paste answer using the require('request') npm module and an environment variable *instead of an hardcoded proxy):

coffeescript

app.use (req, res, next) ->                                                 
  r = false
  method = req.method.toLowerCase().replace(/delete/, 'del')
  switch method
    when 'get', 'post', 'del', 'put'
      r = request[method](
        uri: process.env.PROXY_URL + req.url
        json: req.body)
    else
      return res.send('invalid method')
  req.pipe(r).pipe res

javascript:

app.use(function(req, res, next) {
  var method, r;
  method = req.method.toLowerCase().replace(/delete/,"del");
  switch (method) {
    case "get":
    case "post":
    case "del":
    case "put":
      r = request[method]({
        uri: process.env.PROXY_URL + req.url,
        json: req.body
      });
      break;
    default:
      return res.send("invalid method");
  }
  return req.pipe(r).pipe(res);
});
coderofsalvation
  • 1,764
  • 16
  • 13
  • 3
    Rather than a case statement all of which do the same thing except use a different request function) you could sanitize first (e.g. an if statement that calls your default if the method isn't in the list of approved methods), and then just do r = request[method](/* the rest */); – Paul Oct 12 '15 at 18:38
3

I found a shorter solution that does exactly what I want https://github.com/http-party/node-http-proxy

After installing http-proxy

npm install http-proxy --save

Use it like below in your server/index/app.js

var proxyServer = require('http-route-proxy');
app.use('/api/BLABLA/', proxyServer.connect({
  to: 'other_domain.com:3000/BLABLA',
  https: true,
  route: ['/']
}));

I really have spent days looking everywhere to avoid this issue, tried plenty of solutions and none of them worked but this one.

Hope it is going to help someone else too :)

tanguy_k
  • 11,307
  • 6
  • 54
  • 58
hzitoun
  • 5,492
  • 1
  • 36
  • 43
1

I figure out a way to have more control on the proxy request.

Using axios, the response of request made by proxy server is pipelined to the client in the form of stream.

More header and data body controls can be conducted by client to pass more information through proxy server.

// proxy.js

import { Router } from "express";
var router = Router();
import axios from "axios";

// Proxy the request
router.all("/", async function (req, res, next) {
  let url = req.body["url"];
  let method = req.body["method"] ? req.body["method"] : "GET";
  let headers = req.body["headers"] ? req.body["headers"] : null;
  let params = req.body["params"] ? req.body["params"] : null;
  let data = req.body["data"] ? req.body["data"] : null;

  const response = await axios({
    method: method,
    url: url,
    headers: headers,
    params: params,
    data: data,
    responseType: "stream",
  });

  // Set the header of response as the header of the proxy response
  for (let key in response.headers) {
    res.setHeader(key, response.headers[key]);
  }

  // Pipe the response to the client
  const stream = response.data;
  stream.pipe(res);
});

export default router;
// client.js

fetch("http://localhost:3000/proxy", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        url: picURL,
        method: "GET",
        headers: { Host: "wx2.sinaimg.cn" },
      }),
    }).then(async (res) => {
      const blobURL = URL.createObjectURL(await res.blob());
    });
yang piao
  • 144
  • 1
  • 7
0

I don't have have an express sample, but one with plain http-proxy package. A very strip down version of the proxy I used for my blog.

In short, all nodejs http proxy packages work at the http protocol level, not tcp(socket) level. This is also true for express and all express middleware. None of them can do transparent proxy, nor NAT, which means keeping incoming traffic source IP in the packet sent to backend web server.

However, web server can pickup original IP from http x-forwarded headers and add it into the log.

The xfwd: true in proxyOption enable x-forward header feature for http-proxy.

const url = require('url');
const proxy = require('http-proxy');

proxyConfig = {
    httpPort: 8888,
    proxyOptions: {
        target: {
            host: 'example.com',
            port: 80
        },
        xfwd: true // <--- This is what you are looking for.
    }
};

function startProxy() {

    proxy
        .createServer(proxyConfig.proxyOptions)
        .listen(proxyConfig.httpPort, '0.0.0.0');

}

startProxy();

Reference for X-Forwarded Header: https://en.wikipedia.org/wiki/X-Forwarded-For

Full version of my proxy: https://github.com/J-Siu/ghost-https-nodejs-proxy

John Siu
  • 5,056
  • 2
  • 26
  • 47
0

For Node 18+, you can use the native Fetch API:

import bodyParser from 'body-parser'

const pathsToProxy = /\/users/
const proxyApiUrl = 'https://yourapiproxy.com/users'

app.all(pathsToProxy, bodyParser.json(), (req, res) => {
    const url = req.url.replace(pathsToProxy, proxyApiUrl);
    
    fetch(url, {
        method: req.method,
        headers: {
            'Content-Type': req.headers['content-type'],
        },
        body: ['PUT', 'POST'].includes(req.method) ? JSON.stringify(req.body) : undefined
    }).then(response => {
        response.body.pipeTo(
            new WritableStream({
                start() {
                    res.statusCode = response.status;
                    response.headers.forEach((v, n) => res.setHeader(n, v));
                },
                write(chunk) {
                    res.write(chunk);
                },
                close() {
                    res.end();
                },
            })
            );
        }).catch(e => {
            res.status(500).send(e);
        })
    });
}

As observed, this implementation assumes that the proxy returns json, thus the json body parser

Avram Tudor
  • 1,468
  • 12
  • 18
-2

I think you should use cors npm

const app = express();
const cors = require('cors');
var corsOptions = {
    origin: 'http://localhost:3000',
    optionsSuccessStatus: 200 // some legacy browsers (IE11, various SmartTVs) choke on 204
}
app.use(cors(corsOptions));

https://www.npmjs.com/package/cors

Loc Tran
  • 13
  • 1