Node & Express application to proxy HTTP requests – simply forwarding the response to the original caller image 19

Node & Express application to proxy HTTP requests – simply forwarding the response to the original caller

The requirement is simple: a Node JS application that receives HTTP requests and forwards (some of) them to other hosts and subsequently the returns the responses it receives to the original caller.

image

This can be used in many situations – to ensure all resources loaded in a web application come from the same host (one way to handle CORS), to have content in IFRAMEs loaded from the same host as the surrounding application or to allow connection between systems that cannot directly reach each other. Of course, the proxy component does not have to be the dumb and mute intermediary – it can add headers, handle faults, perform validation and keep track of the traffic. Before you know it, it becomes an API Gateway…

In this article a very simple example of a proxy that I want to use for the following purpose: I create a Rich Web Client application (Angular, React, Oracle JET) – and some of the components used are owned and maintained by an external party. Instead of adding the sources to the server that serves the static sources of the web application, I use the proxy to retrieve these specific sources from their real origin (either a live application, a web server or even a Git repository). This allows me to have the latets sources of these components at any time, without redeploying my own application.

The proxy component is of course very simple and straightforward. And I am sure it can be much improved upon. For my current purposes, it is good enough.

The Node application consists of file www that is initialized with npm start through package.json. This file does some generic initialization of Express (such as defining the port on which the listen). Then it defers to app.js for all request handling. In app.js, a static file server is configured to serve files from the local /public subdirectory (using express.static).

www:


var app = require('../app');
var debug = require('debug')(' :server');
var http = require('http');

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
var server = http.createServer(app);
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

function normalizePort(val) {
var port = parseInt(val, 10);

if (isNaN(port)) {
// named pipe
return val;
}

if (port >= 0) {
// port number
return port;
}

return false;
}

function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}

var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;

// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}

function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}

package.json:


{
"name": "jet-on-node",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"body-parser": "~1.18.2",
"cookie-parser": "~1.4.3",
"debug": "~2.6.9",
"express": "~4.15.5",
"morgan": "~1.9.0",
"pug": "2.0.0-beta11",
"request": "^2.85.0",
"serve-favicon": "~2.4.5"
}
}

app.js:


var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

const http = require('http');
const url = require('url');
const fs = require('fs');
const request = require('request');

var app = express();
// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());

// define static resource server from local directory public (for any request not otherwise handled)
app.use(express.static(path.join(__dirname, 'public')));

app.use(function (req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});

// catch 404 and forward to error handler
app.use(function (req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});

// error handler
app.use(function (err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};

// render the error page
res.status(err.status || 500);
res.json({
message: err.message,
error: err
});
});

module.exports = app;

Then the interesting bit: requests for URL /js/jet-composites/* are intercepted: instead of having those requests also handle by serving local resources (from directory public/js/jet-composites/*), the requests are interpreted and routed to an external host. The responses from that host are returned to the requester. To the requesting browser, there is no distinction between resources served locally as static artifacts from the local file system and resources retrieved through these redirected requests.


// any request at /js/jet-composites (for resouces in that folder)
// should be intercepted and redirected
var compositeBasePath = '/js/jet-composites/'
app.get(compositeBasePath + '*', function (req, res) {
var requestedResource = req.url.substr(compositeBasePath.length)
// parse URL
const parsedUrl = url.parse(requestedResource);
// extract URL path
let pathname = `${parsedUrl.pathname}`;
// maps file extention to MIME types
const mimeType = {
'.ico': 'image/x-icon',
'.html': 'text/html',
'.js': 'text/javascript',
'.json': 'application/json',
'.css': 'text/css',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.wav': 'audio/wav',
'.mp3': 'audio/mpeg',
'.svg': 'image/svg+xml',
'.pdf': 'application/pdf',
'.doc': 'application/msword',
'.eot': 'appliaction/vnd.ms-fontobject',
'.ttf': 'aplication/font-sfnt'
};

handleResourceFromCompositesServer(res, mimeType, pathname)
})

async function handleResourceFromCompositesServer(res, mimeType, requestedResource) {
var reqUrl = "http://yourhost:theport/applicationURL/" + requestedResource
// fetch resource and return
var options = url.parse(reqUrl);
options.method = "GET";
options.agent = false;

// options.headers['host'] = options.host;
http.get(reqUrl, function (serverResponse) {
console.log('<== Received res for', serverResponse.statusCode, reqUrl); console.log('\t-> Request Headers: ', options);
console.log(' ');
console.log('\t-> Response Headers: ', serverResponse.headers);

serverResponse.pause();

serverResponse.headers['access-control-allow-origin'] = '*';

switch (serverResponse.statusCode) {
// pass through. we're not too smart here...
case 200: case 201: case 202: case 203: case 204: case 205: case 206:
case 304:
case 400: case 401: case 402: case 403: case 404: case 405:
case 406: case 407: case 408: case 409: case 410: case 411:
case 412: case 413: case 414: case 415: case 416: case 417: case 418:
res.writeHeader(serverResponse.statusCode, serverResponse.headers);
serverResponse.pipe(res, { end: true });
serverResponse.resume();
break;

// fix host and pass through.
case 301:
case 302:
case 303:
serverResponse.statusCode = 303;
serverResponse.headers['location'] = 'http://localhost:' + PORT + '/' + serverResponse.headers['location'];
console.log('\t-> Redirecting to ', serverResponse.headers['location']);
res.writeHeader(serverResponse.statusCode, serverResponse.headers);
serverResponse.pipe(res, { end: true });
serverResponse.resume();
break;

// error everything else
default:
var stringifiedHeaders = JSON.stringify(serverResponse.headers, null, 4);
serverResponse.resume();
res.writeHeader(500, {
'content-type': 'text/plain'
});
res.end(process.argv.join(' ') + ':\n\nError ' + serverResponse.statusCode + '\n' + stringifiedHeaders);
break;
}

console.log('\n\n');
});
}

Resources

Express Tutorial Part 2: Creating a skeleton website - https://developer.mozilla.org/en-US/docs/Learn/Server-side/Express_Nodejs/skeleton_website

Building a Node.js static file server (files over HTTP) using ES6+ - http://adrianmejia.com/blog/2016/08/24/Building-a-Node-js-static-file-server-files-over-HTTP-using-ES6/

How To Combine REST API calls with JavaScript Promises in node.js or OpenWhisk - https://medium.com/adobe-io/how-to-combine-rest-api-calls-with-javascript-promises-in-node-js-or-openwhisk-d96cbc10f299

Node script to forward all http requests to another server and return the response with an access-control-allow-origin header. Follows redirects. - https://gist.github.com/cmawhorter/a527a2350d5982559bb6

5 Ways to Make HTTP Requests in Node.js - https://www.twilio.com/blog/2017/08/http-requests-in-node-js.html

2 Comments

  1. Bradley Baysinger June 5, 2018