This time, we are going to be creating a request header parser microservice in Node.js. Keep in mind that I’ll be using a set-up similar to that used by the previous two tutorials; for those who have not read them, that means that we’ll be using Express, and code our app using ECMAScript 6 thanks to Babel.
Feel free to go though the creation of a simple Express app post, as well as the set-up for using ECMAScript 6 within your node app.
Let’s get started, create a new directory for the project and run npm init within. Fill in the fields as usual and install the packages we’ll use:
npm install --save express babel-core babel-register babel-preset-es2015
Optional: Initialize a git repository and add a .gitignore file if you are planning to use source control.
For Babel to work properly, remember to add a .babelrc file to the project root:
{ "presets": ["es2015"] }
Now, create an index.js file and src directory at the project root, and an app.js file inside the src directory. The end result should be similar to the following::
/request-header-parser | |-- package.json (generated by npm after we run npm init) |-- .gitignore |-- .babelrc (babel configuration) |-- index.js (entry file that will import and initialize the app) |-- /node_modules (generated by npm) |-- /src | |-- app.js (main app file)
Finally, before we get to the interesting stuff, update index.js so it looks like so:
require('babel-register'); const app = require('./src/app').app, PORT = process.env.PORT || 3000; app.listen(PORT, function() { console.log('Request header parser microservice running on port', PORT); });
Optional: If you are planning to deploy your app to Heroku, remember to add a Procfile to the root directory. If this sounds like Chinese to you, go ahead and go through this post first.
The Request Header Parser
We’ll start by opening app.js and setting up our endpoints with Express. You can name your routes in any way that you want:
import express from 'express'; export conts app = express(); // We export app so index.js can make use of it app.get('/api/whoami', (req, res) => { res.status(200).send('Hello World'); });
Run your app and go to http://localhost:3000/api/whoami (or whatever port you defined in index.js) and you should be able to see the message. It doesn’t do anything yet, but we’ll get there.
Let’s do what every good request header parser does: parse request headers. Marvelling and unexpected, I know, it took me by surprise too.
We can take a few different paths at this point:
- Implement the parsing logic within the endpoint code like this:
app.get('/api/whoami', (req, res) => { // Do the magic here and send it back! });
- Create a function that does the parsing within the same file:
app.get('/api/whoami', (req, res) => { let parsedData = parseRequestAndReturnJSON(req); res.status(200).json(parsedData); }); function parseRequestAndReturnJSON(req) { // Do parsing magic and return it }
- Create a function or class in an external file that does the parsing, and import it into app.js:
export class Parser { static parseRequest(request) { // Do magic and return JSON } }
import { Parser } from './parser'; app.get('/api/whoami', (req, res) => { let parsedData = Parser.parseRequest(req); res.status(200).json(parsedData); });
If you haven’t figured out by now, we are using the latter. Some may think that’s it’s over-complicated, but it helps in separating concerns: parsing logic lives within the Parser class, and endpoint logic lives in the app.js file.
If you don’t know about ES6 classes, you can take a quick peak at ECMAScript 6 class syntax in my two post series: Part I & Part II.
The Parser class
Start by creating a new file under src/ and call it parser.js. Here, we’ll create a Parser class with a single static method I’ll call parseHeader. This method will take in the request object and return a simple JavaScript object of this shape:
{ "ipaddress": "IP address wil go here", "language": "Language will go here", "software": "Client OS will go here" }
Next, we’ll set up the bare-bones:
export class Parser { static parseRequest(req) { // Do magic here } }
Then import in into app.js:
import express from 'express'; import { Parser } from './parser'; // Import the Parser class export conts app = express(); app.get('/api/whoami', (req, res) => { // Now, send the output from parseRequest back res.status(200).send(Parser.parseRequest(req)); });
Method: parseRequest
Next, we’ll implement the parsing logic within parseRequest, we need to obtain the following 3:
- IP address: We can access this property through request.connection.remoteAddress
- Language: Available within the request headers.
- OS: Available within the request headers.
If you want to take a look at the request headers object, you can easily print it to the console by accessing them in Node (console.log(req.headers) ), this is what my output looks like for reference:
{ "host":"localhost:8000", "connection":"keep-alive", "cache-control":"max-age=0", "upgrade-insecure-requests":"1", "user-agent":"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36", "accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "accept-encoding":"gzip, deflate, sdch, br", "accept-language":"en-GB,en-US;q=0.8,en;q=0.6", "cookie":"ai_user=9cm3C|2016-05-25T13:18:52.822Z; eu-cookie-compliance-agreed=1; _ga=GA1.1.914834370.1464182342; __utma=111872281.914834370.1464182342.1476955194.1476960941.3; __utmz=111872281.1464944511.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none)", "if-none-match":"W/"360-UvOt5/Ilipk3+oZi2fJzLQ"" }
I’ve marked those that actually interest us:
- The user agent, where we’ll get the operating system:
"user-agent":"Mozilla/5.0 (<strong>Windows NT 10.0; WOW64</strong>) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36"
- The accept language field:
"accept-language":"<strong>en-GB</strong>,en-US;q=0.8,en;q=0.6"
We’ll keep it simple and only return the OS and first language, in other words, this is what we want to be the end result:
{ "ipaddress": "the.ip.address.from.remoteAddress", "language": "en-GB", "software": "Windows NT 10.0; WOW64" }
Let’s parse their respective fields and return the appropriate values, I’m going to create a method for each one of them so it’s easier on the eyes:
export class Parser { static parseRequest(req) { return { ipaddress: Parser.getIP(req.connection.remoteAddress), language: Parser.getLanguage(req.headers["accept-language"]), software: Parser.getOS(req.headers["user-agent"]) } } static getIP(remoteAddress) { // Parse IP } static getOS(userAgent) { // Parse user-agent header } static getLanguage(acceptLanguage) { // Parse accept-language header } }
Method: getIP
Getting the IP used to be easier, as one could simple return the string contained within request.connection.remoteAddress, but since now we might get a hybrid IPv6/IPv4 IP in this field. And regular IPv4 string will look like so:
- 10.40.0.200
While a IPv6 hybrid string will have something like ::ffff: at the beginning:
- ::ffff:10.40.0.200
To remove these extra characters, we’ll first check if the string has a colon in it, and act accordingly:
export class Parser { static parseRequest(req) { return { ipaddress: Parser.getIP(req.connection.remoteAddress), language: Parser.getLanguage(req.headers["accept-language"]), software: Parser.getOS(req.headers["user-agent"]) } } static getIP(remoteAddress) { let isV6 = remoteAddress.indexOf(':') >= 0; // We split the address every colon, then reverse the array // and grab the first item, which will be the actual IPv4 address return isV6 ? remoteAddress.split(':').reverse()[0] : remoteAddress; } static getOS(userAgent) { // Parse user-agent header } static getLanguage(acceptLanguage) { // Parse accept-language header } }
Method: getOS
We’ll start with the OS detection, the user-agent header uses the following format:
Mozilla/[version] ([system info]) [platform] ([platform details]) [extensions]
This is what each field means:
- Mozilla/[version]: Was used to detect Mozilla rendering engine compatibility.
- [platform], [platform details] and [extensions]: These Describe the web browser where the request originated
- [system info]: Provides system and OS information.
I think you already know what we are looking for. YES. [system info] is our guy, let’s extract it from the header, let’s update the getOS method:
export class Parser { static parseRequest(req) { return { ipaddress: Parser.getIP(req.connection.remoteAddress), language: Parser.getLanguage(req.headers["accept-language"]), software: Parser.getOS(req.headers["user-agent"]) } } static getIP(remoteAddress) { let isV6 = remoteAddress.indexOf(':') >= 0; return isV6 ? remoteAddress.split(':').reverse()[0] : remoteAddress; } static getOS(userAgent) { let osInfo = userAgent.split(/[()]/)[1]; // We grab the second field with [1] return osInfo.trim(); // Trim extra space } static getLanguage(acceptLanguage) { // Parse accept-language header } }
This method uses a regular expression to split the string whenever an opening or closing parenthesis is found, we then remote extra space by using trim.
Method: getLanguage
Next, we tackle the language, the accept-language header gives us the accepted languages as well as a possible list of preferences, here’s an example:
en-GB,en-US;q=0.8,en;q=0.6
That string means that it prefers British English, but will also accept American English, as well as other types of English. The q=0.8 and q=0.6 values indicate the quality, or preference of the last two options; no q value defaults to 1, the highest quality.
We only want the first one for this case:
export class Parser { static parseRequest(req) { return { ipaddress: Parser.getIP(req.connection.remoteAddress), language: Parser.getLanguage(req.headers["accept-language"]), software: Parser.getOS(req.headers["user-agent"]) } } static getIP(remoteAddress) { let isV6 = remoteAddress.indexOf(':') >= 0; return isV6 ? remoteAddress.split(':').reverse()[0] : remoteAddress; } static getOS(userAgent) { let osInfo = userAgent.split(/[()]/)[1]; return osInfo.trim(); } static getLanguage(acceptLanguage) { // We take the first language in the list and trim extra spaces return acceptLanguage.split(',')[0].trim(); } }
And we’re done! Out request header parser microservice is ready to go. Check it out by running app locally by executing node index.js or push up to Heroku.
Feel free to contact me with any queries!