From 8300dd8a47efec1fcd29c2e16aa05d80c37a9e45 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Mon, 7 Mar 2016 11:06:56 -0500 Subject: [PATCH 1/5] Encodes the body by default to application/json if is object --- spec/HTTPRequest.spec.js | 49 +++++++++++++++++++++++++---------- src/cloud-code/httpRequest.js | 25 +++++++++++++----- 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/spec/HTTPRequest.spec.js b/spec/HTTPRequest.spec.js index abc5decf62..0de220cb0d 100644 --- a/spec/HTTPRequest.spec.js +++ b/spec/HTTPRequest.spec.js @@ -1,3 +1,5 @@ +'use strict'; + var httpRequest = require("../src/cloud-code/httpRequest"), bodyParser = require('body-parser'), express = require("express"); @@ -158,31 +160,50 @@ describe("httpRequest", () => { done(); }) }); - it("should encode a JSON body", (done) => { + it("should encode a JSON body by default", (done) => { - var result = httpRequest.encodeBody({"foo": "bar"}, {'Content-Type': 'application/json'}); - expect(result).toEqual('{"foo":"bar"}'); + let options = { + body: {"foo": "bar"}, + } + + var result = httpRequest.encodeBody(options); + expect(result.body).toEqual('{"foo":"bar"}'); + expect(result.headers['Content-Type']).toEqual('application/json'); done(); }) - it("should encode a www-form body", (done) => { + + it("should encode a JSON body", (done) => { + + let options = { + body: {"foo": "bar"}, + headers: {'Content-Type': 'application/json'} + } - var result = httpRequest.encodeBody({"foo": "bar", "bar": "baz"}, {'cOntent-tYpe': 'application/x-www-form-urlencoded'}); - expect(result).toEqual("foo=bar&bar=baz"); + var result = httpRequest.encodeBody(options); + expect(result.body).toEqual('{"foo":"bar"}'); done(); - }); - it("should not encode a wrong content type", (done) => { - var result = httpRequest.encodeBody({"foo": "bar", "bar": "baz"}, {'cOntent-tYpe': 'mime/jpeg'}); - expect(result).toEqual({"foo": "bar", "bar": "baz"}); + }) + it("should encode a www-form body", (done) => { + let options = { + body: {"foo": "bar", "bar": "baz"}, + headers: {'cOntent-tYpe': 'application/x-www-form-urlencoded'} + } + var result = httpRequest.encodeBody(options); + expect(result.body).toEqual("foo=bar&bar=baz"); done(); }); - it("should not encode when missing content type", (done) => { - var result = httpRequest.encodeBody({"foo": "bar", "bar": "baz"}, {'X-Custom-Header': 'my-header'}); - expect(result).toEqual({"foo": "bar", "bar": "baz"}); + it("should not encode a wrong content type", (done) => { + let options = { + body:{"foo": "bar", "bar": "baz"}, + headers: {'cOntent-tYpe': 'mime/jpeg'} + } + var result = httpRequest.encodeBody(options); + expect(result.body).toEqual({"foo": "bar", "bar": "baz"}); done(); }); - + it("should fail gracefully", (done) => { httpRequest({ url: "http://not a good url", diff --git a/src/cloud-code/httpRequest.js b/src/cloud-code/httpRequest.js index 35af79ca3e..2dc5f979cf 100644 --- a/src/cloud-code/httpRequest.js +++ b/src/cloud-code/httpRequest.js @@ -2,25 +2,36 @@ var request = require("request"), querystring = require('querystring'), Parse = require('parse/node').Parse; -var encodeBody = function(body, headers = {}) { +var encodeBody = function(options = {}) { + let body = options.body; + let headers = options.headers || {}; if (typeof body !== 'object') { - return body; + return options; } var contentTypeKeys = Object.keys(headers).filter((key) => { return key.match(/content-type/i) != null; }); - if (contentTypeKeys.length == 1) { + if (contentTypeKeys.length == 0) { + // no content type + try { + options.body = JSON.stringify(body); + options.headers = options.headers || {}; + options.headers['Content-Type'] = 'application/json'; + } catch(e) { + // do nothing; + } + } else if (contentTypeKeys.length == 1) { var contentType = contentTypeKeys[0]; if (headers[contentType].match(/application\/json/i)) { - body = JSON.stringify(body); + options.body = JSON.stringify(body); } else if(headers[contentType].match(/application\/x-www-form-urlencoded/i)) { - body = Object.keys(body).map(function(key){ + options.body = Object.keys(body).map(function(key){ return `${key}=${encodeURIComponent(body[key])}` }).join("&"); } } - return body; + return options; } module.exports = function(options) { @@ -32,7 +43,7 @@ module.exports = function(options) { delete options.success; delete options.error; delete options.uri; // not supported - options.body = encodeBody(options.body, options.headers); + options = encodeBody(options); // set follow redirects to false by default options.followRedirect = options.followRedirects == true; // support params options From f40efe39233d61e12449092e2e789d31f5b2aa16 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Mon, 7 Mar 2016 23:18:45 -0500 Subject: [PATCH 2/5] Sets encoding to null to preserve raw buffers --- spec/HTTPRequest.spec.js | 11 +++++++++++ src/cloud-code/httpRequest.js | 8 +++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/spec/HTTPRequest.spec.js b/spec/HTTPRequest.spec.js index 0de220cb0d..e573be1abb 100644 --- a/spec/HTTPRequest.spec.js +++ b/spec/HTTPRequest.spec.js @@ -217,6 +217,17 @@ describe("httpRequest", () => { done(); } }); + }); + + it('should get a cat image', (done) => { + httpRequest({ + url: 'http://thecatapi.com/api/images/get?format=src&type=jpg', + followRedirects: true + }).then((res) => { + expect(res.buffer).not.toBe(null); + expect(res.text).not.toBe(null); + done(); + }) }) it("should params object to query string", (done) => { diff --git a/src/cloud-code/httpRequest.js b/src/cloud-code/httpRequest.js index 2dc5f979cf..7c0b11f476 100644 --- a/src/cloud-code/httpRequest.js +++ b/src/cloud-code/httpRequest.js @@ -52,6 +52,8 @@ module.exports = function(options) { } else if (typeof options.params === 'string') { options.qs = querystring.parse(options.params); } + // force the response as a buffer + options.encoding = null; request(options, (error, response, body) => { if (error) { @@ -63,11 +65,11 @@ module.exports = function(options) { var httpResponse = {}; httpResponse.status = response.statusCode; httpResponse.headers = response.headers; - httpResponse.buffer = new Buffer(response.body); + httpResponse.buffer = response.body; httpResponse.cookies = response.headers["set-cookie"]; - httpResponse.text = response.body; + httpResponse.text = response.body.toString('utf-8'); try { - httpResponse.data = JSON.parse(response.body); + httpResponse.data = JSON.parse(httpResponse.text); } catch (e) {} // Consider <200 && >= 400 as errors if (httpResponse.status < 200 || httpResponse.status >= 400) { From 982fc723278e8bc34640acb27da5012909cc5a95 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Mon, 7 Mar 2016 23:53:23 -0500 Subject: [PATCH 3/5] Adds HTTPResponse object and lazy loading text and JSON --- src/cloud-code/HTTPResponse.js | 21 +++++++++++++++++++++ src/cloud-code/httpRequest.js | 12 +++--------- 2 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 src/cloud-code/HTTPResponse.js diff --git a/src/cloud-code/HTTPResponse.js b/src/cloud-code/HTTPResponse.js new file mode 100644 index 0000000000..c8630da7f8 --- /dev/null +++ b/src/cloud-code/HTTPResponse.js @@ -0,0 +1,21 @@ + +export default class HTTPResponse { + constructor(response) { + this.status = response.statusCode; + this.headers = response.headers; + this.buffer = response.body; + this.cookies = response.headers["set-cookie"]; + } + + get text() { + return this.buffer.toString('utf-8'); + } + get data() { + if (!this._data) { + try { + this._data = JSON.parse(this.text); + } catch (e) {} + } + return this._data; + } +} \ No newline at end of file diff --git a/src/cloud-code/httpRequest.js b/src/cloud-code/httpRequest.js index 7c0b11f476..d4391256a7 100644 --- a/src/cloud-code/httpRequest.js +++ b/src/cloud-code/httpRequest.js @@ -1,6 +1,7 @@ var request = require("request"), querystring = require('querystring'), Parse = require('parse/node').Parse; + HTTPResponse = require('./HTTPResponse').HTTPResponse; var encodeBody = function(options = {}) { let body = options.body; @@ -62,15 +63,8 @@ module.exports = function(options) { } return promise.reject(error); } - var httpResponse = {}; - httpResponse.status = response.statusCode; - httpResponse.headers = response.headers; - httpResponse.buffer = response.body; - httpResponse.cookies = response.headers["set-cookie"]; - httpResponse.text = response.body.toString('utf-8'); - try { - httpResponse.data = JSON.parse(httpResponse.text); - } catch (e) {} + let httpResponse = new HTTPResponse(response); + // Consider <200 && >= 400 as errors if (httpResponse.status < 200 || httpResponse.status >= 400) { if (callbacks.error) { From 6acb5cee808053720128a0834f3dd69f545fc9ec Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Tue, 8 Mar 2016 00:15:17 -0500 Subject: [PATCH 4/5] ES6-ify --- spec/HTTPRequest.spec.js | 13 +++++-------- src/cloud-code/HTTPResponse.js | 4 ++-- src/cloud-code/httpRequest.js | 34 ++++++++++++++++++---------------- 3 files changed, 25 insertions(+), 26 deletions(-) diff --git a/spec/HTTPRequest.spec.js b/spec/HTTPRequest.spec.js index e573be1abb..1936feee21 100644 --- a/spec/HTTPRequest.spec.js +++ b/spec/HTTPRequest.spec.js @@ -160,13 +160,12 @@ describe("httpRequest", () => { done(); }) }); + it("should encode a JSON body by default", (done) => { - let options = { body: {"foo": "bar"}, } - - var result = httpRequest.encodeBody(options); + let result = httpRequest.encodeBody(options); expect(result.body).toEqual('{"foo":"bar"}'); expect(result.headers['Content-Type']).toEqual('application/json'); done(); @@ -174,13 +173,11 @@ describe("httpRequest", () => { }) it("should encode a JSON body", (done) => { - let options = { body: {"foo": "bar"}, headers: {'Content-Type': 'application/json'} } - - var result = httpRequest.encodeBody(options); + let result = httpRequest.encodeBody(options); expect(result.body).toEqual('{"foo":"bar"}'); done(); @@ -190,7 +187,7 @@ describe("httpRequest", () => { body: {"foo": "bar", "bar": "baz"}, headers: {'cOntent-tYpe': 'application/x-www-form-urlencoded'} } - var result = httpRequest.encodeBody(options); + let result = httpRequest.encodeBody(options); expect(result.body).toEqual("foo=bar&bar=baz"); done(); }); @@ -199,7 +196,7 @@ describe("httpRequest", () => { body:{"foo": "bar", "bar": "baz"}, headers: {'cOntent-tYpe': 'mime/jpeg'} } - var result = httpRequest.encodeBody(options); + let result = httpRequest.encodeBody(options); expect(result.body).toEqual({"foo": "bar", "bar": "baz"}); done(); }); diff --git a/src/cloud-code/HTTPResponse.js b/src/cloud-code/HTTPResponse.js index c8630da7f8..f234f332db 100644 --- a/src/cloud-code/HTTPResponse.js +++ b/src/cloud-code/HTTPResponse.js @@ -13,9 +13,9 @@ export default class HTTPResponse { get data() { if (!this._data) { try { - this._data = JSON.parse(this.text); + this._data = JSON.parse(this.text); } catch (e) {} } return this._data; } -} \ No newline at end of file +} diff --git a/src/cloud-code/httpRequest.js b/src/cloud-code/httpRequest.js index d4391256a7..740ba9f801 100644 --- a/src/cloud-code/httpRequest.js +++ b/src/cloud-code/httpRequest.js @@ -1,13 +1,11 @@ -var request = require("request"), - querystring = require('querystring'), - Parse = require('parse/node').Parse; - HTTPResponse = require('./HTTPResponse').HTTPResponse; +import request from 'request'; +import Parse from 'parse/node'; +import HTTPResponse from './HTTPResponse'; +import querystring from 'querystring'; -var encodeBody = function(options = {}) { - let body = options.body; - let headers = options.headers || {}; +var encodeBody = function({body, headers = {}}) { if (typeof body !== 'object') { - return options; + return {body, headers}; } var contentTypeKeys = Object.keys(headers).filter((key) => { return key.match(/content-type/i) != null; @@ -16,23 +14,27 @@ var encodeBody = function(options = {}) { if (contentTypeKeys.length == 0) { // no content type try { - options.body = JSON.stringify(body); - options.headers = options.headers || {}; - options.headers['Content-Type'] = 'application/json'; + body = JSON.stringify(body); + headers['Content-Type'] = 'application/json'; } catch(e) { // do nothing; } - } else if (contentTypeKeys.length == 1) { + } else { + /* istanbul ignore next */ + if (contentTypeKeys.length > 1) { + console.error('multiple content-type headers are set.'); + } + // There maybe many, we'll just take the 1st one var contentType = contentTypeKeys[0]; if (headers[contentType].match(/application\/json/i)) { - options.body = JSON.stringify(body); + body = JSON.stringify(body); } else if(headers[contentType].match(/application\/x-www-form-urlencoded/i)) { - options.body = Object.keys(body).map(function(key){ + body = Object.keys(body).map(function(key){ return `${key}=${encodeURIComponent(body[key])}` }).join("&"); } } - return options; + return {body, headers}; } module.exports = function(options) { @@ -44,7 +46,7 @@ module.exports = function(options) { delete options.success; delete options.error; delete options.uri; // not supported - options = encodeBody(options); + options = Object.assign(options, encodeBody(options)); // set follow redirects to false by default options.followRedirect = options.followRedirects == true; // support params options From bd7d951ad6c7c8759239296f5385d421e33a0317 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Tue, 8 Mar 2016 08:08:48 -0500 Subject: [PATCH 5/5] default encoding is now querystring instead of JSON --- spec/HTTPRequest.spec.js | 6 +++--- src/cloud-code/httpRequest.js | 14 +++++--------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/spec/HTTPRequest.spec.js b/spec/HTTPRequest.spec.js index 1936feee21..4bacd0fe2c 100644 --- a/spec/HTTPRequest.spec.js +++ b/spec/HTTPRequest.spec.js @@ -161,13 +161,13 @@ describe("httpRequest", () => { }) }); - it("should encode a JSON body by default", (done) => { + it("should encode a query string body by default", (done) => { let options = { body: {"foo": "bar"}, } let result = httpRequest.encodeBody(options); - expect(result.body).toEqual('{"foo":"bar"}'); - expect(result.headers['Content-Type']).toEqual('application/json'); + expect(result.body).toEqual('foo=bar'); + expect(result.headers['Content-Type']).toEqual('application/x-www-form-urlencoded'); done(); }) diff --git a/src/cloud-code/httpRequest.js b/src/cloud-code/httpRequest.js index 740ba9f801..d78e67350f 100644 --- a/src/cloud-code/httpRequest.js +++ b/src/cloud-code/httpRequest.js @@ -13,12 +13,10 @@ var encodeBody = function({body, headers = {}}) { if (contentTypeKeys.length == 0) { // no content type - try { - body = JSON.stringify(body); - headers['Content-Type'] = 'application/json'; - } catch(e) { - // do nothing; - } + // As per https://parse.com/docs/cloudcode/guide#cloud-code-advanced-sending-a-post-request the default encoding is supposedly x-www-form-urlencoded + + body = querystring.stringify(body); + headers['Content-Type'] = 'application/x-www-form-urlencoded'; } else { /* istanbul ignore next */ if (contentTypeKeys.length > 1) { @@ -29,9 +27,7 @@ var encodeBody = function({body, headers = {}}) { if (headers[contentType].match(/application\/json/i)) { body = JSON.stringify(body); } else if(headers[contentType].match(/application\/x-www-form-urlencoded/i)) { - body = Object.keys(body).map(function(key){ - return `${key}=${encodeURIComponent(body[key])}` - }).join("&"); + body = querystring.stringify(body); } } return {body, headers};