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.
request has been deprecated as of February 2020, I'll leave the answer below for historical reasons, but please consider moving to an alternative listed in this issue.
Archive
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);
});
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
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.
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);
});
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);
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
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);
});
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 :)
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
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
Related
I'm trying to setup a route for downlading videos for my Vue app backed by an Express server. For some reason, first request that is sent to backend is working as expected and it results in successful file download; however, the subsequent requests fail with Network Error, and I only get a brief error message that looks like this http://localhost:8080/download/videos/1667163624289.mp4 net::ERR_FAILED 200 (OK).
What could be the issue here?
I have an Express.js server (localhost:8000) setup with cors like below:
const express = require("express");
const app = express();
const port = 8000;
const cors = require("cors");
app.use(cors());
app.get("/download/:kind/:fileName",
async (req, res, next) => {
const file = `${__dirname}/public/files/${req.params.kind}/${req.params.fileName}`;
res.download(file);
});
app.listen(port, () => {
});
And my Vue (localhost:8080) component sends that request looks like this:
downloadVideo(fileName) {
const fileName = fileDir.split('/').pop();
const downloadUrl = `/download/videos/${fileName}`;
axios({
method: "get",
url: downloadUrl,
responseType: 'blob',
})
.then((response)=> {
// create file link in browser's memory
const href = URL.createObjectURL(response.data); // data is already a blob
// create "a" element with href to file & click
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', 'my_video.mp4');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(href);
})
.catch((err) => {
// HANDLE ERROR HERE
})
},
I also have a vue config setup to proxy the requests to 8000:
// vue.config.js
module.exports = {
devServer: {
proxy: 'http://localhost:8000',
disableHostCheck: true
},
outputDir: '../backend/public', // build will output to this folder
assetsDir: '' // relative to the output folder
}
Instead of manually setting up the route for downloading files, you can directly set the static files directory as a controller and remove the app.get controller for downloading.
app.use(express.static("public/files"))
Then, on the client side, instead of downloading the file using JS, converting it into a data url, and then downloading it, you can do the following:
downloadVideo(fileName) {
// whatever you want to do to the file name
const parsedFileName = fileName
const link = document.createElement('a');
link.href = parsedFileName;
link.setAttribute('download', 'my-video.mp4'); // or pdf
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
Here is a sample working example
Im not really sure why the first request is going through. But the error looks like a CORS problem.
Basically, your frontend and backend run on different ports, which are treated like a different server altogether by the CORS checks.
I took the following config from the cors package docs
var express = require('express')
var cors = require('cors')
var app = express()
var corsOptions = {
origin: 'http://localhost:8080',
optionsSuccessStatus: 200 // some legacy browsers (IE11, various SmartTVs) choke on 204
}
/*
Or use this, if you want to bypass cors checks - not a safe practice
var corsOptions = {
origin: '*',
optionsSuccessStatus: 200
}
*/
app.get('/products/:id', cors(corsOptions), function (req, res, next) {
res.json({msg: 'This is CORS-enabled for only localhost:8080'})
})
app.listen(80, function () {
console.log('CORS-enabled web server listening on port 80')
})
The 200 ERR is funny, because normally 200 means OK. But in this case 200 refers to the Preflight request and ERR to the fact that the Ports are different.
There is a good video about cors on youtube
I am trying to set up a proxy using node, express, and an instance of cors-anywhere for my arcgis-js-api app. My server file looks like this:
import express from 'express';
import cors from 'cors';
import corsAnywhere from 'cors-anywhere';
const { PORT } = process.env;
const port = PORT || 3030;
var app = express();
let proxy = corsAnywhere.createServer({
originWhitelist: [], // Allow all origins
requireHeaders: [], // Do not require any headers.
removeHeaders: [], // Do not remove any headers.
});
app.use(cors());
app.get('/proxy/:proxyUrl*', (req, res) => {
req.url = req.url.replace('/proxy/', '/');
proxy.emit('request', req, res);
});
app.get('*', (req, res) => {
res.sendFile(path.resolve(__dirname, 'index.html'));
});
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
When I go to http://localhost:3030/proxy/https://maps.disasters.nasa.gov/ags04/rest/services/ca_fires_202008/sentinel2/MapServer?f=json, I get to my target json no problem, with access-control-allow-origin: * correctly tacked on.
In my front end html (an arcgis-js-api app), I am calling that same url:
var layer = new MapImageLayer({
url: 'http://localhost:3030/proxy/https://maps.disasters.nasa.gov/ags04/rest/services/ca_fires_202008/sentinel2/MapServer?f=json',
});
My network tab shows a response not of the expected JSON, but of the text of the cors-anywhere proxy:
For those familiar with the arcgis-js-api, you can also preconfigure use of a proxy:
urlUtils.addProxyRule({
urlPrefix: 'maps.disasters.nasa.gov',
proxyUrl: 'http://localhost:3030/proxy/',
});
If I do it this way, the network tab shows that the call to the localhost:3030/proxy/<url> is returning the index.html page, not the desired json.
Why is the proxy giving the expected/required result when I access the url directly through the browser, but not when being called from my front end file? Thanks for reading.
I checked the browser console and noticed that the url being sent to the proxy instead of this
http://localhost:3030/proxy/https://maps.disasters.nasa.gov/ags04/rest/services/ca_fires_202008/sentinel2/MapServer?f=json
looks like this
http://localhost:3030/proxy/https:/maps.disasters.nasa.gov/ags04/rest/services/ca_fires_202008/sentinel2/MapServer?f=json
Not sure why it's happening, but as a quick fix you can replace
req.url = req.url.replace('/proxy/', '/');
with
req.url = req.url.replace('/proxy/https:/', '/https://');
I am building a proxy service that will forward all but one kind of a POST request to another server.
I was planning on using express-http-proxy to do this but I can't find a way to modify the POST request on the fly.
For Example:
I want to catch all POST requests to /getdata and check if they have a field called username,
if they do I want to replace it with a custom username and then forward the request to another server and then forward the response from it back to the user.
Any help is appreciated. Any package or resource would help. Thanks.
I was facing a similar issue recently and ended up using http-proxy-middleware with the following config (based on this recipe):
const express = require('express');
const {createProxyMiddleware} = require('http-proxy-middleware');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
const options = {
target: '<your-target>',
changeOrigin: true,
onProxyReq: (proxyReq, req, res) => {
if (req.path === '/getdata' && req.body && req.body.userName) {
req.body.userName = "someOtherUser";
const bodyData = JSON.stringify(req.body);
proxyReq.setHeader('Content-Type', 'application/json');
proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData));
proxyReq.write(bodyData);
}
},
};
app.use(createProxyMiddleware(options));
app.listen(4001, () => console.log("listening ..."));
What did the trick for me was recalculating the Content-Length using this line:
proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData));
I have built the following express app to serve multiple sites with HTTPS from a single server:
// strict
'use strict';
// vars
const tls = require('tls');
var fs = require('fs');
var http = require('http');
var https = require('https');
var express = require('express');
var vhost = require('vhost');
var forceSSL = require('express-force-ssl');
var cors = require('cors');
var app = express();
var server;
var secureServer;
// always force HTTP -> HTTPS redirect
app.use(forceSSL);
// allow cross origin
app.use(cors());
// Express settings (main switch logic - each host, and each static directory)
var siteOne = vhost('siteone.com', express.static('../siteone/dist'));
var wwwSiteOne = vhost('www.siteone.com', express.static('../siteone/dist'));
var siteTwo = vhost('sitetwo.com', express.static('../sitetwo/dist'));
var wwwSiteTwo = vhost('www.sitetwo.com', express.static('../sitetwo/dist'));
app.use(cors()); // allow cross origin
app.use(siteOne); // all hosts defined above
app.use(wwwSiteOne);
app.use(siteTwo);
app.use(wwwSiteTwo);
app.use(function(req, res, next) { // 404 page as last "use" call
res.status(404).send('404 page :(');
});
// HTTP Server - http.createServer is enough for our HTTP needs
server = http.createServer(app);
server.listen(8080); // router is set to forward port 80 (http requests) to port 8080
// HTTPS Server - use virtual host to redirect with secureserver
var secureContext = {
'siteone.com': tls.createSecureContext({
key: fs.readFileSync('../../sslcert/siteone/privkey.pem', 'utf8'),
cert: fs.readFileSync('../../sslcert/siteone/fullchain.pem', 'utf8'),
ca: fs.readFileSync('../../sslcert/siteone/chain.pem', 'utf8'),
}),
'www.siteone.com': tls.createSecureContext({
key: fs.readFileSync('../../sslcert/siteone/privkey.pem', 'utf8'),
cert: fs.readFileSync('../../sslcert/siteone/fullchain.pem', 'utf8'),
ca: fs.readFileSync('../../sslcert/siteone/chain.pem', 'utf8'),
}),
'sitetwo.com': tls.createSecureContext({
key: fs.readFileSync('../../sslcert/sitetwo/privkey.pem', 'utf8'),
cert: fs.readFileSync('../../sslcert/sitetwo/fullchain.pem', 'utf8'),
ca: fs.readFileSync('../../sslcert/sitetwo/chain.pem', 'utf8'),
}),
'www.sitetwo.com': tls.createSecureContext({
key: fs.readFileSync('../../sslcert/sitetwo/privkey.pem', 'utf8'),
cert: fs.readFileSync('../../sslcert/sitetwo/fullchain.pem', 'utf8'),
ca: fs.readFileSync('../../sslcert/sitetwo/chain.pem', 'utf8'),
}),
}
try {
var options = {
SNICallback: function (domain, cb) {
if (secureContext[domain]) {
if (cb) {
cb(null, secureContext[domain]);
} else {
// compatibility for older versions of node
return secureContext[domain];
}
} else {
console.log('Doing nothing. Domain requsted: ' + domain);
}
},
// must list a default key and cert because required by tls.createServer()
key: fs.readFileSync('../../sslcert/siteone/privkey.pem'),
cert: fs.readFileSync('../../sslcert/siteone/fullchain.pem'),
}
secureServer = https.createServer(options, app);
secureServer.listen(8043); // router is set to forward port 443 (https requests) to port 8043
} catch (err){
console.error(err.message);
console.error(err.stack);
}
So far, I have run into issues with POST, GET, and the react router (v4) paths. For example, a single page site with react router works to every link as long as the the user starts from the homepage, but if the user provides a link directly in the url bar (ex. siteone.com/somecoolpath) i get the 404 page from the website switch.
Similiarly I have a POST from a site for adding a user's email to a DB that is on url path /add_email... and I always get a 404 when I send the email...
All these paths work on their own apps, but not when I serve through this SNICallback switch.
Things I have tried but didn't work:
wildcards in the vhost:
var reactRouterSite = vhost('siteone.com*', express.static('../siteone/dist'));
rewriting the domain in the SNI callback:
SNICallback: function (domain, cb) {
if (domain.includes('siteone')) { // any request from siteone
domain = 'siteone.com'
}
...
a final switch() statement in the last app.use() statement:
app.use(function(req, res, next) {
console.log(req);
switch (req.url) {
case '/somecoolpath':
return siteOne;
break;
default:
res.status(404).send('404 page :(');
}
});
I've spent hours searching around for possible solutions, but haven't even found an example of a case with react router with SNICallback... any ideas what I could try? Or is there a much easier solution?
all the sites are hosted in other node instances, express apps on ports 8081, 8082, etc. For all sites the root pages load as expected through this 'switch', it's just those GET/POST urls and the copy/pasting router urls that don't work.
One thing you can do is get rid of the vhosts middleware and just create a app.get('*') route and do the logic yourself checking req.hostname and instead of using static directories, render a template. I've done this with multiple domains and multiple react apps.
From dzm's response, I ended up indeed taking out the vhost middleware, and instead used an app.get('*' ...) route, but instead of rendering a template, I found that a proxy would work best for my needs, and used the http-proxy package. After some tests, the react router paths can be copy/pasted directly in the browser and work as expected, and the GET/POST methods work as well, as long as the app.get() and app.post() paths exist also in the website switch.
Replacing the vhosts / app.use() section, while leaving the other parts of the code i.e. the SNICallback, and secureContext untouched, the solutions looks like this:
// set up proxy server
var httpProxy = require('http-proxy');
var proxy = httpProxy.createProxyServer({}); // leave options empty --> custom logic below in app.get('*' ...)
// GET paths
app.get('*', function(req, res) {
// custom logic to handle and proxy the request
console.log(req.hostname);
switch (req.hostname) {
case 'siteone.com':
proxy.web(req, res, { target: 'http://127.0.0.1:8081' }); // different node server running on local port 8081
break;
case 'sitetwo.com':
proxy.web(req, res, { target: 'http://127.0.0.1:8082' }); // different node server running on local port 8082
break;
default:
res.status(404).send('404 page :(');
break;
}
});
// POST paths (for siteone)
app.post('/new_email', function(req, res) {
proxy.web(req, res, { target: 'http://127.0.0.1:8081' }); // this post path must also be written on the index.js of siteone.com
});
In my scenario I need forward get request to another end point. In my machine there are two servers php and node.js server. Node.js is like a "man in the middle", PHP server must work in the same way.
Node.js server code
var express = require('express');
var fs = require('fs');
var path = require('path');
var http = require('http');
var https = require('https');
var app = express();
var HTTP_PORT = 3000;
// Create an HTTP service
http.createServer(app).listen(HTTP_PORT,function() {
console.log('Listening HTTP on port ' + HTTP_PORT);
});
//endpoint for tracking
app.get('/track', function(req, res) {
sendRequestToOtherEndPoint(req);
processRequest(req);
res.setHeader('Content-Type', 'application/json');
res.send('Req OK');
});
function processRequest(req){
console.log("request processed");
}
function sendRequestToOtherEndPoint(req){
//magic here :)
}
When this server receive a get request in port 3000, it process request information and it must forward the same requesto to another end point.
For example:
Get localhost:3000/track?param1=1¶m2=2
Server process get request
Server forward get request to localhost/final-endpoint?param1=1¶m2=2
Depending on what you're trying to do, you can create a new request to the end-point:
//endpoint for tracking
app.get('/track', function(req, res) {
req.get({url: 'http://end-point', headers: req.headers});
processRequest(req);
res.setHeader('Content-Type', 'application/json');
res.send('Req OK');
});
More info: https://github.com/request/request
There are a couple of useful libraries that one could use:
http-proxy-middleware:
const proxy = require('http-proxy-middleware')
var apiProxy = proxy('/track', {target: 'http://end-point'});
app.use(apiProxy)
axios-express-proxy
import express from 'express';
import { Proxy } from 'axios-express-proxy';
const app = express();
const port = 3000;
app.get('/track', (req, res) => Proxy('http://end-point', req, res));
In you case res.redirect might help.
app.get('/track', function(req, res) {
// process the request
// then redirect
res.redirect('/final-endpoint');
});
Then catch the redirected request in final endpont.
app.get('/final-endpoint', function(req, res) {
// proceess redirected request here.
});
See the Express docs
If your second endpoint is on a different server, (e.g. PHP) then you're going to need to either redirect the client (as in sohel's answer), or spoof a request from Node to the PHP server and then send the response back to the client. This latter option is definitely non-trivial so I would question whether it's crucial not to use a client redirect.
If you're talking about two express endpoints, then I think the simplest answer might be not to actually forward at all, but just use the endpoint callback directly instead:
app.get('/track', trackCallback);
app.get('/otherendpoint', otherendpointCallback);
function otherendpointCallback(req, res) {
// do your thing
}
function trackCallback(req, res) {
otherendpointCallback(req, res);
processRequest(req);
res.setHeader('Content-Type', 'application/json');
res.send('Req OK');
};
Depending on exactly what you want to do at the other end point, you might need to spoof some of req's fields (e.g. req.url)