diff --git a/request.js b/request.js index 6857890d8..724cf7a5d 100644 --- a/request.js +++ b/request.js @@ -253,6 +253,13 @@ function responseToJSON() { } } +// encode rfc3986 characters +function rfc3986 (str) { + return str.replace(/[!'()*]/g, function(c) { + return '%' + c.charCodeAt(0).toString(16).toUpperCase() + }) +} + function Request (options) { // if tunnel property of options was not given default to false // if given the method property in options, set property explicitMethod to true @@ -1402,7 +1409,9 @@ Request.prototype.qs = function (q, clobber) { return self } - self.uri = url.parse(self.uri.href.split('?')[0] + '?' + self.qsLib.stringify(base)) + var qs = self.qsLib.stringify(base) + + self.uri = url.parse(self.uri.href.split('?')[0] + '?' + rfc3986(qs)) self.url = self.uri self.path = self.uri.path @@ -1413,6 +1422,7 @@ Request.prototype.form = function (form) { if (form) { self.setHeader('content-type', 'application/x-www-form-urlencoded') self.body = (typeof form === 'string') ? form.toString('utf8') : self.qsLib.stringify(form).toString('utf8') + self.body = rfc3986(self.body) return self } // create form-data object @@ -1482,14 +1492,18 @@ Request.prototype.json = function (val) { self._json = true if (typeof val === 'boolean') { - if (self.body !== undefined && !/^application\/x-www-form-urlencoded\b/.test(self.getHeader('content-type'))) { - self.body = safeStringify(self.body) + if (self.body !== undefined) { + if (!/^application\/x-www-form-urlencoded\b/.test(self.getHeader('content-type'))) { + self.body = safeStringify(self.body) + } + self.body = rfc3986(self.body) if (!self.hasHeader('content-type')) { self.setHeader('content-type', 'application/json') } } } else { self.body = safeStringify(val) + self.body = rfc3986(self.body) if (!self.hasHeader('content-type')) { self.setHeader('content-type', 'application/json') } diff --git a/tests/test-rfc3986.js b/tests/test-rfc3986.js new file mode 100644 index 000000000..d346b3efd --- /dev/null +++ b/tests/test-rfc3986.js @@ -0,0 +1,77 @@ +'use strict' + +var http = require('http') + , request = require('../index') + , tape = require('tape') + + +function runTest (t, options) { + + var server = http.createServer(function(req, res) { + + var data = '' + req.setEncoding('utf8') + + req.on('data', function(d) { + data += d + }) + + req.on('end', function() { + if (options.qs) { + t.equal(req.url, '/?rfc3986=%21%2A%28%29%27') + } + if (options.form) { + t.equal(data, 'rfc3986=%21%2A%28%29%27') + } + if (options.body) { + if (options.headers) { + t.equal(data, 'rfc3986=%21%2A%28%29%27') + } + else { + t.equal(data, '{"rfc3986":"%21%2A%28%29%27"}') + } + } + if (typeof options.json === 'object') { + t.equal(data, '{"rfc3986":"%21%2A%28%29%27"}') + } + + res.writeHead(200) + res.end('done') + }) + }) + + server.listen(8080, function() { + + request.post('http://localhost:8080', options, function(err, res, body) { + t.equal(err, null) + server.close() + t.end() + }) + }) +} + +var cases = [ + {qs: {rfc3986: '!*()\''}}, + {qs: {rfc3986: '!*()\''}, json: true}, + {form: {rfc3986: '!*()\''}}, + {form: {rfc3986: '!*()\''}, json: true}, + {qs: {rfc3986: '!*()\''}, form: {rfc3986: '!*()\''}}, + {qs: {rfc3986: '!*()\''}, form: {rfc3986: '!*()\''}, json: true}, + { + headers: {'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'}, + body: 'rfc3986=!*()\'', + json: true + }, + { + body: {rfc3986: '!*()\''}, json: true + }, + { + json: {rfc3986: '!*()\''} + } +] + +cases.forEach(function (options, index) { + tape('rfc3986 ' + index, function(t) { + runTest(t, options) + }) +})