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 4 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
46 changes: 46 additions & 0 deletions README.md
Expand Up @@ -726,6 +726,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 example below for details)*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's cross-link to the HAR section instead (syntax may not be quite right):

- *(see example below for details)*
+ *(see the [HAR 1.2 section](#support-for-har-1.2) for details)*

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, was going to do this first, then I followed the examples from encoding ...


The callback argument gets 3 arguments:

Expand All @@ -738,6 +741,49 @@ The callback argument gets 3 arguments:

---

## Support for HAR 1.2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say put the new doc section right after the TLS/SSL Protocol section instead, and add it to the table of contents too.


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
```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To match the style of the other doc sections, add a link back to the top and a --- line (I vote for removing the --- lines but for now let's be consistent.)


## Convenience methods

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 heade
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo, should be 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 attachement = {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In English this should be attachment instead


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

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

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

options.formData[param.name] = attachement
})
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