We're working on an application which needs to access a remote storage API to load images - in this case, the image files were saved on Azure, and our local API gave us the Azure URL to load the images. When running on our local machines for development, the local compiled Javascript files are served by an Express server:

server.use('/', express.static(process.cwd() + '/bin/build'));

Our API calls are coming from another URL, set in config files and loaded dynamically for different environments (eg development, staging, production etc) - this is where the problems start! Due to browser security policies, namely cross-origin resource sharing (CORS), we were running into problems with the API being on a different URL; browsers return errors if you don't set up the correct headers. Since we have a number of environments, all loaded dynamically, we didn't want to update every server and every environment with the correct headers to allow local development machines access, so we needed another way to fix this.

Since we were already using Node to run our code locally, we decided to proxy all the API requests through our local server. This means that our Javascript code makes a request to our own development server which is already serving the code locally, and the server forwards on the request. When the response comes back, it is routed through our local server, so our Javascript code thinks that the API is running on the same domain and doesn't complain about CORS errors.

Here's a diagram of what happens without proxying:

localhost:3000 ---> example.com/api --//--> CORS error! (different domains)

And with a proxy:

localhost:3000 ---> localhost:3000/api           here's your info!
                    |                               ^
                    |                               |
                    |                               |
                    ----> server fetches example.com/api

Set up the proxy

We're using [express-http-proxy](https://github.com/villadora/express-http-proxy) to proxy requests to the api. There's a couple of steps involved here:

  1. Replace calls which go directly to your api (eg example.com/api) to go through your local server - localhost:3000 in this case. This means you'll need to update your code - the neatest way is to use a config setting so you can change this per environment.

  2. Add proxy code to the local Express server

Proxy code:

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

const proxyService = express();

// serve static code (compiled JS)
proxyService.use('/', express.static(process.cwd() + '/bin/build'));

// this is the proxy - it will request the external api when you hit /api
// http://localhost:3000/api -> http://example.com/api
proxyService.use('/api', proxy('example.com/', {
  // this passes any URL params on to the external api
  // eg /api/user/1234 -> example.com/api/user/1234
  forwardPath: (req, res) => '/api' + (url.parse(req.url).path === '/' ? '' : url.parse(req.url).path)

// tell it to use port 3000 - localhost:3000
})).listen(3000);

Now we can request the external api via our local server without any errors!

Part 2: Doing horrible things with proxies for IE9

With our new proxy in place, everything was working brilliantly, except in IE9. No images were loading in IE9, since they lived on an external server (Azure, as mentioned earlier), and IE9 didn't like that. Our usual proxy method wouldn't work in this case, since we got the URL from the API and then tried to call that directly - to be able to proxy, we needed the image call to come through our local development server (localhost) so we could request the correct URL without our code knowing about it. Luckily, express-http-proxy provides an intercept method, which allows us to mess around with the response from the external API before we send it through to our code.

You can see where this is going: to get images displaying in IE9, we needed to read the response we got back from the external API and then edit it so that the image URL pointed to localhost instead of Azure storage. There were a couple of steps to this as well:

  1. Edit the response so it pointed to localhost instead of storage
  2. Add another proxy route to read images

Edit response

The response back originally looked a bit like this:

{
  "name": "lovely picture",
  "url": "https://azurestorage.example.com/verylongpictureid"
}

so we updated the proxy to use intercept to read the response back:

// this is the proxy - it will request the external api when you hit /api
// http://localhost:3000/api -> http://example.com/api
proxyService.use('/api', proxy('example.com/', {
  // this passes any URL params on to the external api
  // eg /api/user/1234 -> example.com/api/user/1234
  forwardPath: (req, res) => '/api' + (url.parse(req.url).path === '/' ? '' : url.parse(req.url).path),
  intercept: (rsp, data, req, res, callback) => {
    // change data here then pass it to callback
  }
})).listen(3000);

Seems straightforward, just call String.replace on the response! Well, no - data in the intercept function is actually a Buffer, not a string, so we need to transform it to a string, replace the URL, then pass it on to our local server.

intercept: (rsp, data, req, res, callback) => {

  // check if the browser is IE9 - we only want to alter image URLs for IE9 since
  // everything else is working ok
  req.headers['user-agent'].indexOf('MSIE 9.0') > -1) {

    // convert Buffer to JSON (our API always sends a JSON response)
    let response = data.toString('utf-8');

    // we need a string to just globally replace the storage URL - you could just change
    // the object members individually above, but here we're converting to a string and
    // replacing every instance of the storage URL
    response = JSON.stringify(response);

    // replace the URL with a string we can easily proxy later
    response = response.replace(/https:\/\/azurestorage.example.com/gi, '/imagestorage');


    // convert the string back to JSON and pass it in to the callback (ie back to our server)
    // the first param is an error response, so set to null
    callback(null, JSON.parse(response));

  } else {
    // if browser is not IE9, just pass the data through
    callback(null, data);
  }
}

Now the image URLs are pointing to our local server, we need to set up another proxy route so we can get the images from the correct place:

// local images -> get from Azure storage
proxyService.use('/imagestorage', proxy('azurestorage.example.com'));

All done!

Great, images are now working in IE9! Final code:

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

const proxyService = express();

// serve static code (compiled JS)
proxyService.use('/', express.static(process.cwd() + '/bin/build'));

// local images -> get from Azure storage
proxyService.use('/imagestorage', proxy('azurestorage.example.com'));

// this is the proxy - it will request the external api when you hit /api
// http://localhost:3000/api -> http://example.com/api
proxyService.use('/api', proxy('example.com/', {

  // this passes any URL params on to the external api
  // eg /api/user/1234 -> example.com/api/user/1234
  forwardPath: (req, res) => '/api' + (url.parse(req.url).path === '/' ? '' : url.parse(req.url).path),

  intercept: (rsp, data, req, res, callback) => {

      // check if the browser is IE9 - we only want to alter image URLs for IE9 since
      // everything else is working ok
      req.headers['user-agent'].indexOf('MSIE 9.0') > -1) {

        // convert Buffer to JSON (our API always sends a JSON response)
        let response = data.toString('utf-8');

        // we need a string to just globally replace the storage URL - you could just change
        // the object members individually above, but here we're converting to a string and
        // replacing every instance of the storage URL
        response = JSON.stringify(response);

        // replace the URL with a string we can easily proxy later
        response = response.replace(/https:\/\/azurestorage.example.com/gi, '/imagestorage');


        // convert the string back to JSON and pass it in to the callback (ie back to our server)
        // the first param is an error response, so set to null
        callback(null, JSON.parse(response));

      } else {
        // if browser is not IE9, just pass the data through
        callback(null, data);
      }
  }
})).listen(3000);