Skip to content

Commit

Permalink
Merge pull request #1501 from ahmadnassri/master
Browse files Browse the repository at this point in the history
HTTP Archive 1.2 support
  • Loading branch information
simov committed Mar 24, 2015
2 parents ed6d53e + a970d68 commit ddab5e8
Show file tree
Hide file tree
Showing 7 changed files with 619 additions and 2 deletions.
53 changes: 52 additions & 1 deletion README.md
Expand Up @@ -32,6 +32,7 @@ request('http://www.google.com', function (error, response, body) {
- [Proxies](#proxies)
- [Unix Domain Sockets](#unix-domain-sockets)
- [TLS/SSL Protocol](#tlsssl-protocol)
- [Support for HAR 1.2](#support-for-har-12)
- [**All Available Options**](#requestoptions-callback)

Request also offers [convenience methods](#convenience-methods) like
Expand Down Expand Up @@ -633,6 +634,54 @@ request.get({
---
## Support for HAR 1.2
The `options.har` property will override the values: `url`, `method`, `qs`, `headers`, `form`, `formData`, `body`, `json`, as well as construct multipart data and read files from disk when `request.postData.params[].fileName` is present without a matching `value`.
a validation step will check if the HAR Request format matches the latest spec (v1.2) and will skip parsing if not matching.
```js
var request = require('request')
request({
// will be ignored
method: 'GET'
uri: 'http://www.google.com',

// HTTP Archive Request Object
har: {
url: 'http://www.mockbin.com/har'
method: 'POST',
headers: [
{
name: 'content-type',
value: 'application/x-www-form-urlencoded'
}
],
postData: {
mimeType: 'application/x-www-form-urlencoded',
params: [
{
name: 'foo',
value: 'bar'
},
{
name: 'hello',
value: 'world'
}
]
}
}
})

// a POST request will be sent to http://www.mockbin.com
// with body an application/x-www-form-urlencoded body:
// foo=bar&hello=world
```
[back to top](#table-of-contents)
---
## request(options, callback)
Expand Down Expand Up @@ -726,6 +775,9 @@ The first argument can be either a `url` or an `options` object. The only requir
- `time` - If `true`, the request-response cycle (including all redirects) is timed at millisecond resolution, and the result provided on the response's `elapsedTime` property.
---
- `har` - A [HAR 1.2 Request Object](http://www.softwareishard.com/blog/har-12-spec/#request), will be processed from HAR format into options overwriting matching values *(see the [HAR 1.2 section](#support-for-har-1.2) for details)*
The callback argument gets 3 arguments:
Expand All @@ -738,7 +790,6 @@ The callback argument gets 3 arguments:
---
## Convenience methods
There are also shorthand methods for different HTTP METHODs and some other conveniences.
Expand Down
205 changes: 205 additions & 0 deletions lib/har.js
@@ -0,0 +1,205 @@
'use strict'

var fs = require('fs')
var qs = require('querystring')
var validate = require('har-validator')
var util = require('util')

function Har (request) {
this.request = request
}

Har.prototype.reducer = function (obj, pair) {
// new property ?
if (obj[pair.name] === undefined) {
obj[pair.name] = pair.value
return obj
}

// existing? convert to array
var arr = [
obj[pair.name],
pair.value
]

obj[pair.name] = arr

return obj
}

Har.prototype.prep = function (data) {
// construct utility properties
data.queryObj = {}
data.headersObj = {}
data.postData.jsonObj = false
data.postData.paramsObj = false

// construct query objects
if (data.queryString && data.queryString.length) {
data.queryObj = data.queryString.reduce(this.reducer, {})
}

// construct headers objects
if (data.headers && data.headers.length) {
// loweCase header keys
data.headersObj = data.headers.reduceRight(function (headers, header) {
headers[header.name] = header.value
return headers
}, {})
}

// construct Cookie header
if (data.cookies && data.cookies.length) {
var cookies = data.cookies.map(function (cookie) {
return cookie.name + '=' + cookie.value
})

if (cookies.length) {
data.headersObj.cookie = cookies.join('; ')
}
}

// prep body
switch (data.postData.mimeType) {
case 'multipart/mixed':
case 'multipart/related':
case 'multipart/form-data':
case 'multipart/alternative':
// reset values
data.postData.mimeType = 'multipart/form-data'
break

case 'application/x-www-form-urlencoded':
if (!data.postData.params) {
data.postData.text = ''
} else {
data.postData.paramsObj = data.postData.params.reduce(this.reducer, {})

// always overwrite
data.postData.text = qs.stringify(data.postData.paramsObj)
}
break

case 'text/json':
case 'text/x-json':
case 'application/json':
case 'application/x-json':
data.postData.mimeType = 'application/json'

if (data.postData.text) {
try {
data.postData.jsonObj = JSON.parse(data.postData.text)
} catch (e) {
this.request.debug(e)

// force back to text/plain
data.postData.mimeType = 'text/plain'
}
}
break
}

return data
}

Har.prototype.options = function (options) {
// skip if no har property defined
if (!options.har) {
return options
}

var har = util._extend({}, options.har)

// only process the first entry
if (har.log && har.log.entries) {
har = har.log.entries[0]
}

// add optional properties to make validation successful
har.url = har.url || options.url || options.uri || options.baseUrl || '/'
har.httpVersion = har.httpVersion || 'HTTP/1.1'
har.queryString = har.queryString || []
har.headers = har.headers || []
har.cookies = har.cookies || []
har.postData = har.postData || {}
har.postData.mimeType = har.postData.mimeType || 'application/octet-stream'

har.bodySize = 0
har.headersSize = 0
har.postData.size = 0

if (!validate.request(har)) {
return options
}

// clean up and get some utility properties
var req = this.prep(har)

// construct new options
if (req.url) {
options.url = req.url
}

if (req.method) {
options.method = req.method
}

if (Object.keys(req.queryObj).length) {
options.qs = req.queryObj
}

if (Object.keys(req.headersObj).length) {
options.headers = req.headersObj
}

switch (req.postData.mimeType) {
case 'application/x-www-form-urlencoded':
options.form = req.postData.paramsObj
break

case 'application/json':
if (req.postData.jsonObj) {
options.body = req.postData.jsonObj
options.json = true
}
break

case 'multipart/form-data':
options.formData = {}

req.postData.params.forEach(function (param) {
var attachment = {}

if (!param.fileName && !param.fileName && !param.contentType) {
options.formData[param.name] = param.value
return
}

// attempt to read from disk!
if (param.fileName && !param.value) {
attachment.value = fs.createReadStream(param.fileName)
} else if (param.value) {
attachment.value = param.value
}

if (param.fileName) {
attachment.options = {
filename: param.fileName,
contentType: param.contentType ? param.contentType : null
}
}

options.formData[param.name] = attachment
})
break

default:
if (req.postData.text) {
options.body = req.postData.text
}
}

return options
}

exports.Har = Har
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -38,7 +38,8 @@
"aws-sign2": "~0.5.0",
"stringstream": "~0.0.4",
"combined-stream": "~0.0.5",
"isstream": "~0.1.1"
"isstream": "~0.1.1",
"har-validator": "^1.4.0"
},
"scripts": {
"test": "npm run lint && node node_modules/.bin/taper tests/test-*.js && npm run test-browser",
Expand Down
8 changes: 8 additions & 0 deletions request.js
Expand Up @@ -22,6 +22,7 @@ var http = require('http')
, cookies = require('./lib/cookies')
, copy = require('./lib/copy')
, getProxyFromURI = require('./lib/getProxyFromURI')
, Har = require('./lib/har').Har
, Auth = require('./lib/auth').Auth
, OAuth = require('./lib/oauth').OAuth
, Multipart = require('./lib/multipart').Multipart
Expand Down Expand Up @@ -244,6 +245,13 @@ function Request (options) {
// call init

var self = this

// start with HAR, then override with additional options
if (options.har) {
self._har = new Har(self)
options = self._har.options(options)
}

stream.Stream.call(self)
var reserved = Object.keys(Request.prototype)
var nonReserved = filterForNonReserved(reserved, options)
Expand Down

0 comments on commit ddab5e8

Please sign in to comment.