├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '8' 5 | - '7' 6 | - '6' 7 | - '5' 8 | - '4' 9 | - '0.12' 10 | - '0.10' 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2017 Thomas Watson Steen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # http-headers 2 | 3 | [![Build status](https://travis-ci.org/watson/http-headers.svg?branch=master)](https://travis-ci.org/watson/http-headers) 4 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://github.com/feross/standard) 5 | 6 | Parse the start-line and headers from an HTTP request or reponse. 7 | 8 | Converts: 9 | 10 | ```http 11 | HTTP/1.1 200 OK 12 | Date: Tue, 10 Jun 2014 07:19:27 GMT 13 | Connection: keep-alive 14 | Transfer-Encoding: chunked 15 | 16 | Hello World 17 | ``` 18 | 19 | To this: 20 | 21 | ```js 22 | { 23 | version: { major: 1, minor: 1 }, 24 | statusCode: 200, 25 | statusMessage: 'OK', 26 | headers: { 27 | date: 'Tue, 10 Jun 2014 07:19:27 GMT', 28 | connection: 'keep-alive', 29 | 'transfer-encoding': 'chunked' 30 | } 31 | } 32 | ``` 33 | 34 | **Features:** 35 | 36 | - Auto-detects and ignores body if present 37 | - Fully [RFC 2068](http://www.rfc-base.org/txt/rfc-2068.txt) compliant 38 | (please [open an issue](https://github.com/watson/http-headers/issues) 39 | if you find a discrepancy) 40 | - Support multi-line headers (lines will be joined with a space) 41 | - Support repeating headers 42 | 43 | ## Installation 44 | 45 | ``` 46 | npm install http-headers --save 47 | ``` 48 | 49 | ## Usage 50 | 51 | ```js 52 | var net = require('net') 53 | var httpHeaders = require('http-headers') 54 | 55 | // create TCP server 56 | net.createServer(function (c) { 57 | var buffers = [] 58 | c.on('data', buffers.push.bind(buffers)) 59 | c.on('end', function () { 60 | var data = Buffer.concat(buffers) 61 | 62 | // parse incoming data as an HTTP request and extra HTTP headers 63 | console.log(httpHeaders(data)) 64 | }) 65 | }).listen(8080) 66 | ``` 67 | 68 | ### `http.ServerReponse` support 69 | 70 | If given an instance of `http.ServerResponse`, the reponse headers is 71 | automatically extracted, parsed and returned: 72 | 73 | ```js 74 | var http = require('http') 75 | var httpHeaders = require('http-headers') 76 | 77 | http.createServer(function (req, res) { 78 | res.end('Hello World') 79 | console.log(httpHeaders(res)) 80 | }).listen(8080) 81 | ``` 82 | 83 | #### Why? 84 | 85 | If you've ever needed to log or in another way access the headers sent 86 | to the client on a `http.ServerResponse` in Node.js, you know it's not 87 | as easy as with the `http.IncomingMessage` headers (which you just 88 | access via `request.headers['content-type']`). 89 | 90 | Response headers are not directly available on the `response` object. 91 | Instead all headers are preprocessed as a string on the private 92 | `response._header` property and needs to be processed in order to be 93 | available as an object. 94 | 95 | This module makes the task super simple. 96 | 97 | ## API 98 | 99 | The http-headers module exposes a single parser function: 100 | 101 | ```js 102 | httpHeaders(data[, onlyHeaders]) 103 | ``` 104 | 105 | Arguments: 106 | 107 | - `data` - A string, buffer or instance of `http.ServerReponse` 108 | - `onlyHeaders` - An optional boolean. If `true`, only the headers 109 | object will be returned. Defaults to `false` 110 | 111 | ### Request example 112 | 113 | If given a request as input: 114 | 115 | ```http 116 | GET /foo HTTP/1.1 117 | Date: Tue, 10 Jun 2014 07:19:27 GMT 118 | Connection: keep-alive 119 | Transfer-Encoding: chunked 120 | 121 | Hello World 122 | ``` 123 | 124 | Returns: 125 | 126 | ```js 127 | { 128 | method: 'GET', 129 | url: '/foo', 130 | version: { major: 1, minor: 1 }, 131 | headers: { 132 | date: 'Tue, 10 Jun 2014 07:19:27 GMT', 133 | connection: 'keep-alive', 134 | 'transfer-encoding': 'chunked' 135 | } 136 | } 137 | ``` 138 | 139 | ### Response example 140 | 141 | If given a request as input: 142 | 143 | ```http 144 | HTTP/1.1 200 OK 145 | Date: Tue, 10 Jun 2014 07:19:27 GMT 146 | Connection: keep-alive 147 | Transfer-Encoding: chunked 148 | 149 | Hello World 150 | ``` 151 | 152 | Returns: 153 | 154 | ```js 155 | { 156 | version: { major: 1, minor: 1 }, 157 | statusCode: 200, 158 | statusMessage: 'OK', 159 | headers: { 160 | date: 'Tue, 10 Jun 2014 07:19:27 GMT', 161 | connection: 'keep-alive', 162 | 'transfer-encoding': 'chunked' 163 | } 164 | } 165 | ``` 166 | 167 | ### `onlyHeaders` example 168 | 169 | If the optional second argument is set to `true`, only headers are 170 | returned no matter the type of input: 171 | 172 | ```js 173 | { 174 | date: 'Tue, 10 Jun 2014 07:19:27 GMT', 175 | connection: 'keep-alive', 176 | 'transfer-encoding': 'chunked' 177 | } 178 | ``` 179 | 180 | ### No Start-Line 181 | 182 | If the `data` given does not contain an HTTP Start-Line, only the 183 | headers are returned, even if the `onlyHeaders` argument is `false`: 184 | 185 | ```http 186 | Date: Tue, 10 Jun 2014 07:19:27 GMT 187 | Connection: keep-alive 188 | Transfer-Encoding: chunked 189 | 190 | Hello World 191 | ``` 192 | 193 | Returns: 194 | 195 | ```js 196 | { 197 | date: 'Tue, 10 Jun 2014 07:19:27 GMT', 198 | connection: 'keep-alive', 199 | 'transfer-encoding': 'chunked' 200 | } 201 | ``` 202 | 203 | ## License 204 | 205 | MIT 206 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var nextLine = require('next-line') 4 | 5 | // RFC-2068 Start-Line definitions: 6 | // Request-Line: Method SP Request-URI SP HTTP-Version CRLF 7 | // Status-Line: HTTP-Version SP Status-Code SP Reason-Phrase CRLF 8 | var startLine = /^[A-Z_]+(\/\d\.\d)? / 9 | var requestLine = /^([A-Z_]+) (.+) [A-Z]+\/(\d)\.(\d)$/ 10 | var statusLine = /^[A-Z]+\/(\d)\.(\d) (\d{3}) (.*)$/ 11 | 12 | module.exports = function (data, onlyHeaders) { 13 | return parse(normalize(data), onlyHeaders) 14 | } 15 | 16 | function parse (str, onlyHeaders) { 17 | var line = firstLine(str) 18 | var match 19 | 20 | if (onlyHeaders && startLine.test(line)) { 21 | return parseHeaders(str) 22 | } else if ((match = line.match(requestLine)) !== null) { 23 | return { 24 | method: match[1], 25 | url: match[2], 26 | version: { major: parseInt(match[3], 10), minor: parseInt(match[4], 10) }, 27 | headers: parseHeaders(str) 28 | } 29 | } else if ((match = line.match(statusLine)) !== null) { 30 | return { 31 | version: { major: parseInt(match[1], 10), minor: parseInt(match[2], 10) }, 32 | statusCode: parseInt(match[3], 10), 33 | statusMessage: match[4], 34 | headers: parseHeaders(str) 35 | } 36 | } else { 37 | return parseHeaders(str) 38 | } 39 | } 40 | 41 | function parseHeaders (str) { 42 | var headers = {} 43 | var next = nextLine(str) 44 | var line = next() 45 | var index, name, value 46 | 47 | if (startLine.test(line)) line = next() 48 | 49 | while (line) { 50 | // subsequent lines in multi-line headers start with whitespace 51 | if (line[0] === ' ' || line[0] === '\t') { 52 | value += ' ' + line.trim() 53 | line = next() 54 | continue 55 | } 56 | 57 | if (name) addHeaderLine(name, value, headers) 58 | 59 | index = line.indexOf(':') 60 | name = line.substr(0, index) 61 | value = line.substr(index + 1).trim() 62 | 63 | line = next() 64 | } 65 | 66 | if (name) addHeaderLine(name, value, headers) 67 | 68 | return headers 69 | } 70 | 71 | function normalize (str) { 72 | if (str && str._header) str = str._header // extra headers from http.ServerResponse object 73 | if (!str || typeof str.toString !== 'function') return '' 74 | return str.toString().trim() 75 | } 76 | 77 | function firstLine (str) { 78 | var nl = str.indexOf('\r\n') 79 | if (nl === -1) return str 80 | else return str.slice(0, nl) 81 | } 82 | 83 | // The following function is lifted from: 84 | // https://github.com/nodejs/node/blob/f1294f5bfd7f02bce8029818be9c92de59749137/lib/_http_incoming.js#L116-L170 85 | // 86 | // Add the given (field, value) pair to the message 87 | // 88 | // Per RFC2616, section 4.2 it is acceptable to join multiple instances of the 89 | // same header with a ', ' if the header in question supports specification of 90 | // multiple values this way. If not, we declare the first instance the winner 91 | // and drop the second. Extended header fields (those beginning with 'x-') are 92 | // always joined. 93 | function addHeaderLine (field, value, dest) { 94 | field = field.toLowerCase() 95 | switch (field) { 96 | // Array headers: 97 | case 'set-cookie': 98 | if (dest[field] !== undefined) { 99 | dest[field].push(value) 100 | } else { 101 | dest[field] = [value] 102 | } 103 | break 104 | 105 | // list is taken from: 106 | // https://mxr.mozilla.org/mozilla/source/netwerk/protocol/http/src/nsHttpHeaderArray.cpp 107 | case 'content-type': 108 | case 'content-length': 109 | case 'user-agent': 110 | case 'referer': 111 | case 'host': 112 | case 'authorization': 113 | case 'proxy-authorization': 114 | case 'if-modified-since': 115 | case 'if-unmodified-since': 116 | case 'from': 117 | case 'location': 118 | case 'max-forwards': 119 | case 'retry-after': 120 | case 'etag': 121 | case 'last-modified': 122 | case 'server': 123 | case 'age': 124 | case 'expires': 125 | // drop duplicates 126 | if (dest[field] === undefined) dest[field] = value 127 | break 128 | 129 | default: 130 | // make comma-separated list 131 | if (typeof dest[field] === 'string') { 132 | dest[field] += ', ' + value 133 | } else { 134 | dest[field] = value 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "http-headers", 3 | "version": "3.0.2", 4 | "description": "Parse http headers", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard && tape test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/watson/http-headers.git" 12 | }, 13 | "dependencies": { 14 | "next-line": "^1.1.0" 15 | }, 16 | "devDependencies": { 17 | "safe-buffer": "^5.1.1", 18 | "standard": "^10.0.2", 19 | "tape": "^4.7.0" 20 | }, 21 | "keywords": [ 22 | "http", 23 | "https", 24 | "header", 25 | "headers", 26 | "parse", 27 | "parsing", 28 | "ServerResponse", 29 | "response" 30 | ], 31 | "author": "Thomas Watson Steen (https://twitter.com/wa7son)", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/watson/http-headers/issues" 35 | }, 36 | "homepage": "https://github.com/watson/http-headers", 37 | "coordinates": [ 38 | 55.6757062, 39 | 12.5774478 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var test = require('tape') 4 | var http = require('http') 5 | var Buffer = require('safe-buffer').Buffer 6 | var httpHeaders = require('./') 7 | 8 | var requestLine = 'GET /foo HTTP/1.1\r\n' 9 | var statusLine = 'HTTP/1.1 200 OK\r\n' 10 | var msgHeaders = 'Date: Tue, 10 Jun 2014 07:29:20 GMT\r\n' + 11 | 'Connection: keep-alive\r\n' + 12 | 'Transfer-Encoding: chunked\r\n' + 13 | 'Age: foo\r\n' + 14 | 'Age: bar\r\n' + 15 | 'Set-Cookie: cookie\r\n' + 16 | 'X-List: A\r\n' + 17 | 'X-Multi-Line-Header: Foo\r\n' + 18 | ' Bar\r\n' + 19 | 'X-List: B\r\n' + 20 | '\r\n' 21 | var requestMsg = requestLine + msgHeaders + 'Hello: World' 22 | var responseMsg = statusLine + msgHeaders + 'Hello: World' 23 | 24 | var headerResult = { 25 | date: 'Tue, 10 Jun 2014 07:29:20 GMT', 26 | connection: 'keep-alive', 27 | 'transfer-encoding': 'chunked', 28 | age: 'foo', 29 | 'set-cookie': ['cookie'], 30 | 'x-list': 'A, B', 31 | 'x-multi-line-header': 'Foo Bar' 32 | } 33 | var responseResult = { 34 | version: { major: 1, minor: 1 }, 35 | statusCode: 200, 36 | statusMessage: 'OK', 37 | headers: headerResult 38 | } 39 | var requestResult = { 40 | method: 'GET', 41 | url: '/foo', 42 | version: { major: 1, minor: 1 }, 43 | headers: headerResult 44 | } 45 | 46 | test('no argument', function (t) { 47 | t.deepEqual(httpHeaders(), {}) 48 | t.deepEqual(httpHeaders(undefined, true), {}) 49 | t.end() 50 | }) 51 | 52 | test('empty string', function (t) { 53 | t.deepEqual(httpHeaders(''), {}) 54 | t.deepEqual(httpHeaders('', true), {}) 55 | t.end() 56 | }) 57 | 58 | test('empty object', function (t) { 59 | t.deepEqual(httpHeaders({}), {}) 60 | t.deepEqual(httpHeaders({}, true), {}) 61 | t.end() 62 | }) 63 | 64 | test('empty buffer', function (t) { 65 | t.deepEqual(httpHeaders(new Buffer('')), {}) 66 | t.deepEqual(httpHeaders(new Buffer(''), true), {}) 67 | t.end() 68 | }) 69 | 70 | test('start-line + header', function (t) { 71 | t.deepEqual(httpHeaders(requestLine + msgHeaders), requestResult) 72 | t.deepEqual(httpHeaders(statusLine + msgHeaders), responseResult) 73 | t.deepEqual(httpHeaders(new Buffer(requestLine + msgHeaders)), requestResult) 74 | t.deepEqual(httpHeaders(new Buffer(statusLine + msgHeaders)), responseResult) 75 | t.deepEqual(httpHeaders(requestLine + msgHeaders, true), headerResult) 76 | t.deepEqual(httpHeaders(statusLine + msgHeaders, true), headerResult) 77 | t.deepEqual(httpHeaders(new Buffer(requestLine + msgHeaders), true), headerResult) 78 | t.deepEqual(httpHeaders(new Buffer(statusLine + msgHeaders), true), headerResult) 79 | t.end() 80 | }) 81 | 82 | test('request-line only', function (t) { 83 | var requestResult = { 84 | method: 'GET', 85 | url: '/foo', 86 | version: { major: 1, minor: 1 }, 87 | headers: {} 88 | } 89 | 90 | t.deepEqual(httpHeaders(requestLine + '\r\n'), requestResult) 91 | t.deepEqual(httpHeaders(new Buffer(requestLine + '\r\n')), requestResult) 92 | t.deepEqual(httpHeaders(requestLine + '\r\n', true), {}) 93 | t.deepEqual(httpHeaders(new Buffer(requestLine + '\r\n'), true), {}) 94 | t.end() 95 | }) 96 | 97 | test('status-line only', function (t) { 98 | var responseResult = { 99 | version: { major: 1, minor: 1 }, 100 | statusCode: 200, 101 | statusMessage: 'OK', 102 | headers: {} 103 | } 104 | 105 | t.deepEqual(httpHeaders(statusLine + '\r\n'), responseResult) 106 | t.deepEqual(httpHeaders(new Buffer(statusLine + '\r\n')), responseResult) 107 | t.deepEqual(httpHeaders(statusLine + '\r\n', true), {}) 108 | t.deepEqual(httpHeaders(new Buffer(statusLine + '\r\n'), true), {}) 109 | t.end() 110 | }) 111 | 112 | test('headers only', function (t) { 113 | t.deepEqual(httpHeaders(msgHeaders), headerResult) 114 | t.deepEqual(httpHeaders(new Buffer(msgHeaders)), headerResult) 115 | t.deepEqual(httpHeaders(msgHeaders, true), headerResult) 116 | t.deepEqual(httpHeaders(new Buffer(msgHeaders), true), headerResult) 117 | t.end() 118 | }) 119 | 120 | test('full http response', function (t) { 121 | t.deepEqual(httpHeaders(requestMsg), requestResult) 122 | t.deepEqual(httpHeaders(responseMsg), responseResult) 123 | t.deepEqual(httpHeaders(new Buffer(requestMsg)), requestResult) 124 | t.deepEqual(httpHeaders(new Buffer(responseMsg)), responseResult) 125 | t.deepEqual(httpHeaders(requestMsg, true), headerResult) 126 | t.deepEqual(httpHeaders(responseMsg, true), headerResult) 127 | t.deepEqual(httpHeaders(new Buffer(requestMsg), true), headerResult) 128 | t.deepEqual(httpHeaders(new Buffer(responseMsg), true), headerResult) 129 | t.end() 130 | }) 131 | 132 | test('http.ServerResponse', function (t) { 133 | t.test('real http.ServerResponse object', function (t) { 134 | var res = new http.ServerResponse({}) 135 | t.deepEqual(httpHeaders(res), {}) 136 | t.deepEqual(httpHeaders(res, true), {}) 137 | t.end() 138 | }) 139 | 140 | t.test('no _header property', function (t) { 141 | t.deepEqual(httpHeaders({ _header: undefined }), {}) 142 | t.deepEqual(httpHeaders({ _header: undefined }, true), {}) 143 | t.end() 144 | }) 145 | 146 | t.test('empty string as _header', function (t) { 147 | t.deepEqual(httpHeaders({ _header: '' }), {}) 148 | t.deepEqual(httpHeaders({ _header: '' }, true), {}) 149 | t.end() 150 | }) 151 | 152 | t.test('normal _header property', function (t) { 153 | t.deepEqual(httpHeaders({ _header: statusLine + msgHeaders }), responseResult) 154 | t.deepEqual(httpHeaders({ _header: statusLine + msgHeaders }, true), headerResult) 155 | t.end() 156 | }) 157 | }) 158 | 159 | test('set-cookie', function (t) { 160 | t.deepEqual(httpHeaders('Set-Cookie: foo'), { 'set-cookie': ['foo'] }) 161 | t.deepEqual(httpHeaders('Set-Cookie: foo\r\nSet-Cookie: bar'), { 'set-cookie': ['foo', 'bar'] }) 162 | t.end() 163 | }) 164 | --------------------------------------------------------------------------------