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:
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.- 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:
- Edit the response so it pointed to
localhost
instead of storage - 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);