Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP Archive 1.2 support #1501

Merged
merged 6 commits into from Mar 24, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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