From 2d79c0835b6a9acaf20d5c943d9b4619bb96831c Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 11 Nov 2022 04:28:49 +1100 Subject: [PATCH] feat: Remove deprecation `DEPPS4`: Remove convenience method for http request `Parse.Cloud.httpRequest` (#8287) BREAKING CHANGE: The convenience method for HTTP requests `Parse.Cloud.httpRequest` is removed; use your preferred 3rd party library for making HTTP requests --- DEPRECATIONS.md | 2 +- spec/CloudCode.spec.js | 19 ---- spec/HTTPRequest.spec.js | 4 +- spec/ParseGeoPoint.spec.js | 2 +- src/cloud-code/HTTPResponse.js | 61 ------------ src/cloud-code/Parse.Cloud.js | 9 -- src/cloud-code/httpRequest.js | 158 ----------------------------- src/request.js | 175 ++++++++++++++++++++++++++++++++- 8 files changed, 178 insertions(+), 252 deletions(-) delete mode 100644 src/cloud-code/HTTPResponse.js delete mode 100644 src/cloud-code/httpRequest.js diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md index 711796f087..e9a692a181 100644 --- a/DEPRECATIONS.md +++ b/DEPRECATIONS.md @@ -7,7 +7,7 @@ The following is a list of deprecations, according to the [Deprecation Policy](h | DEPPS1 | Native MongoDB syntax in aggregation pipeline | [#7338](https://github.com/parse-community/parse-server/issues/7338) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - | | DEPPS2 | Config option `directAccess` defaults to `true` | [#6636](https://github.com/parse-community/parse-server/pull/6636) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - | | DEPPS3 | Config option `enforcePrivateUsers` defaults to `true` | [#7319](https://github.com/parse-community/parse-server/pull/7319) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - | -| DEPPS4 | Remove convenience method for http request `Parse.Cloud.httpRequest` | [#7589](https://github.com/parse-community/parse-server/pull/7589) | 5.0.0 (2022) | 6.0.0 (2023) | deprecated | - | +| DEPPS4 | Remove convenience method for http request `Parse.Cloud.httpRequest` | [#7589](https://github.com/parse-community/parse-server/pull/7589) | 5.0.0 (2022) | 6.0.0 (2023) | removed | - | | DEPPS5 | Config option `allowClientClassCreation` defaults to `false` | [#7925](https://github.com/parse-community/parse-server/pull/7925) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - | | DEPPS6 | Auth providers disabled by default | [#7953](https://github.com/parse-community/parse-server/pull/7953) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - | | DEPPS7 | Remove file trigger syntax `Parse.Cloud.beforeSaveFile((request) => {})` | [#7966](https://github.com/parse-community/parse-server/pull/7966) | 5.3.0 (2022) | 7.0.0 (2024) | deprecated | - | diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index c522af0b5d..3cec9ac468 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1686,25 +1686,6 @@ describe('Cloud Code', () => { obj.save().then(done, done.fail); }); - it('can deprecate Parse.Cloud.httpRequest', async () => { - const logger = require('../lib/logger').logger; - spyOn(logger, 'warn').and.callFake(() => {}); - Parse.Cloud.define('hello', () => { - return 'Hello world!'; - }); - await Parse.Cloud.httpRequest({ - method: 'POST', - url: 'http://localhost:8378/1/functions/hello', - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest', - }, - }); - expect(logger.warn).toHaveBeenCalledWith( - 'DeprecationWarning: Parse.Cloud.httpRequest is deprecated and will be removed in a future version. Use a http request library instead.' - ); - }); - describe('cloud jobs', () => { it('should define a job', done => { expect(() => { diff --git a/spec/HTTPRequest.spec.js b/spec/HTTPRequest.spec.js index f218ff3c91..1321ca2627 100644 --- a/spec/HTTPRequest.spec.js +++ b/spec/HTTPRequest.spec.js @@ -1,7 +1,7 @@ 'use strict'; -const httpRequest = require('../lib/cloud-code/httpRequest'), - HTTPResponse = require('../lib/cloud-code/HTTPResponse').default, +const httpRequest = require('../lib/request'), + HTTPResponse = require('../lib/request').HTTPResponse, bodyParser = require('body-parser'), express = require('express'); diff --git a/spec/ParseGeoPoint.spec.js b/spec/ParseGeoPoint.spec.js index 83868e8d8d..8454e60c74 100644 --- a/spec/ParseGeoPoint.spec.js +++ b/spec/ParseGeoPoint.spec.js @@ -47,7 +47,7 @@ describe('Parse.GeoPoint testing', () => { obj.set('location', point); obj.set('name', 'Zhoul'); await obj.save(); - Parse.Cloud.httpRequest({ + request({ url: 'http://localhost:8378/1/classes/TestObject/' + obj.id, headers: { 'X-Parse-Application-Id': 'test', diff --git a/src/cloud-code/HTTPResponse.js b/src/cloud-code/HTTPResponse.js deleted file mode 100644 index 1c1de93ae2..0000000000 --- a/src/cloud-code/HTTPResponse.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @typedef Parse.Cloud.HTTPResponse - * @property {Buffer} buffer The raw byte representation of the response body. Use this to receive binary data. See Buffer for more details. - * @property {Object} cookies The cookies sent by the server. The keys in this object are the names of the cookies. The values are Parse.Cloud.Cookie objects. - * @property {Object} data The parsed response body as a JavaScript object. This is only available when the response Content-Type is application/x-www-form-urlencoded or application/json. - * @property {Object} headers The headers sent by the server. The keys in this object are the names of the headers. We do not support multiple response headers with the same name. In the common case of Set-Cookie headers, please use the cookies field instead. - * @property {Number} status The status code. - * @property {String} text The raw text representation of the response body. - */ -export default class HTTPResponse { - constructor(response, body) { - let _text, _data; - this.status = response.statusCode; - this.headers = response.headers || {}; - this.cookies = this.headers['set-cookie']; - - if (typeof body == 'string') { - _text = body; - } else if (Buffer.isBuffer(body)) { - this.buffer = body; - } else if (typeof body == 'object') { - _data = body; - } - - const getText = () => { - if (!_text && this.buffer) { - _text = this.buffer.toString('utf-8'); - } else if (!_text && _data) { - _text = JSON.stringify(_data); - } - return _text; - }; - - const getData = () => { - if (!_data) { - try { - _data = JSON.parse(getText()); - } catch (e) { - /* */ - } - } - return _data; - }; - - Object.defineProperty(this, 'body', { - get: () => { - return body; - }, - }); - - Object.defineProperty(this, 'text', { - enumerable: true, - get: getText, - }); - - Object.defineProperty(this, 'data', { - enumerable: true, - get: getData, - }); - } -} diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index b90ffe70f6..db12067ffe 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -713,15 +713,6 @@ ParseCloud.useMasterKey = () => { ); }; -const request = require('./httpRequest'); -ParseCloud.httpRequest = opts => { - Deprecator.logRuntimeDeprecation({ - usage: 'Parse.Cloud.httpRequest', - solution: 'Use a http request library instead.', - }); - return request(opts); -}; - module.exports = ParseCloud; /** diff --git a/src/cloud-code/httpRequest.js b/src/cloud-code/httpRequest.js deleted file mode 100644 index 0f70a8d6a6..0000000000 --- a/src/cloud-code/httpRequest.js +++ /dev/null @@ -1,158 +0,0 @@ -import HTTPResponse from './HTTPResponse'; -import querystring from 'querystring'; -import log from '../logger'; -import { http, https } from 'follow-redirects'; -import { parse } from 'url'; - -const clients = { - 'http:': http, - 'https:': https, -}; - -function makeCallback(resolve, reject) { - return function (response) { - const chunks = []; - response.on('data', chunk => { - chunks.push(chunk); - }); - response.on('end', () => { - const body = Buffer.concat(chunks); - const httpResponse = new HTTPResponse(response, body); - - // Consider <200 && >= 400 as errors - if (httpResponse.status < 200 || httpResponse.status >= 400) { - return reject(httpResponse); - } else { - return resolve(httpResponse); - } - }); - response.on('error', reject); - }; -} - -const encodeBody = function ({ body, headers = {} }) { - if (typeof body !== 'object') { - return { body, headers }; - } - var contentTypeKeys = Object.keys(headers).filter(key => { - return key.match(/content-type/i) != null; - }); - - if (contentTypeKeys.length == 0) { - // no content type - // 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) { - log.error('Parse.Cloud.httpRequest', '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)) { - body = JSON.stringify(body); - } else if (headers[contentType].match(/application\/x-www-form-urlencoded/i)) { - body = querystring.stringify(body); - } - } - return { body, headers }; -}; - -/** - * Makes an HTTP Request. - * - * **Available in Cloud Code only.** - * - * By default, Parse.Cloud.httpRequest does not follow redirects caused by HTTP 3xx response codes. You can use the followRedirects option in the {@link Parse.Cloud.HTTPOptions} object to change this behavior. - * - * Sample request: - * ``` - * Parse.Cloud.httpRequest({ - * url: 'http://www.parse.com/' - * }).then(function(httpResponse) { - * // success - * console.log(httpResponse.text); - * },function(httpResponse) { - * // error - * console.error('Request failed with response code ' + httpResponse.status); - * }); - * ``` - * - * @method httpRequest - * @name Parse.Cloud.httpRequest - * @param {Parse.Cloud.HTTPOptions} options The Parse.Cloud.HTTPOptions object that makes the request. - * @return {Promise} A promise that will be resolved with a {@link Parse.Cloud.HTTPResponse} object when the request completes. - */ -module.exports = function httpRequest(options) { - let url; - try { - url = parse(options.url); - } catch (e) { - return Promise.reject(e); - } - options = Object.assign(options, encodeBody(options)); - // support params options - if (typeof options.params === 'object') { - options.qs = options.params; - } else if (typeof options.params === 'string') { - options.qs = querystring.parse(options.params); - } - const client = clients[url.protocol]; - if (!client) { - return Promise.reject(`Unsupported protocol ${url.protocol}`); - } - const requestOptions = { - method: options.method, - port: Number(url.port), - path: url.pathname, - hostname: url.hostname, - headers: options.headers, - encoding: null, - followRedirects: options.followRedirects === true, - }; - if (requestOptions.headers) { - Object.keys(requestOptions.headers).forEach(key => { - if (typeof requestOptions.headers[key] === 'undefined') { - delete requestOptions.headers[key]; - } - }); - } - if (url.search) { - options.qs = Object.assign({}, options.qs, querystring.parse(url.query)); - } - if (url.auth) { - requestOptions.auth = url.auth; - } - if (options.qs) { - requestOptions.path += `?${querystring.stringify(options.qs)}`; - } - if (options.agent) { - requestOptions.agent = options.agent; - } - return new Promise((resolve, reject) => { - const req = client.request(requestOptions, makeCallback(resolve, reject, options)); - if (options.body) { - req.write(options.body); - } - req.on('error', error => { - reject(error); - }); - req.end(); - }); -}; - -/** - * @typedef Parse.Cloud.HTTPOptions - * @property {String|Object} body The body of the request. If it is a JSON object, then the Content-Type set in the headers must be application/x-www-form-urlencoded or application/json. You can also set this to a {@link Buffer} object to send raw bytes. If you use a Buffer, you should also set the Content-Type header explicitly to describe what these bytes represent. - * @property {function} error The function that is called when the request fails. It will be passed a Parse.Cloud.HTTPResponse object. - * @property {Boolean} followRedirects Whether to follow redirects caused by HTTP 3xx responses. Defaults to false. - * @property {Object} headers The headers for the request. - * @property {String} method The method of the request. GET, POST, PUT, DELETE, HEAD, and OPTIONS are supported. Will default to GET if not specified. - * @property {String|Object} params The query portion of the url. You can pass a JSON object of key value pairs like params: {q : 'Sean Plott'} or a raw string like params:q=Sean Plott. - * @property {function} success The function that is called when the request successfully completes. It will be passed a Parse.Cloud.HTTPResponse object. - * @property {string} url The url to send the request to. - */ - -module.exports.encodeBody = encodeBody; diff --git a/src/request.js b/src/request.js index 2f85c3c9cf..bc58ee40ac 100644 --- a/src/request.js +++ b/src/request.js @@ -1 +1,174 @@ -module.exports = require('./cloud-code/httpRequest'); +import querystring from 'querystring'; +import log from './logger'; +import { http, https } from 'follow-redirects'; +import { parse } from 'url'; + +class HTTPResponse { + constructor(response, body) { + let _text, _data; + this.status = response.statusCode; + this.headers = response.headers || {}; + this.cookies = this.headers['set-cookie']; + + if (typeof body == 'string') { + _text = body; + } else if (Buffer.isBuffer(body)) { + this.buffer = body; + } else if (typeof body == 'object') { + _data = body; + } + + const getText = () => { + if (!_text && this.buffer) { + _text = this.buffer.toString('utf-8'); + } else if (!_text && _data) { + _text = JSON.stringify(_data); + } + return _text; + }; + + const getData = () => { + if (!_data) { + try { + _data = JSON.parse(getText()); + } catch (e) { + /* */ + } + } + return _data; + }; + + Object.defineProperty(this, 'body', { + get: () => { + return body; + }, + }); + + Object.defineProperty(this, 'text', { + enumerable: true, + get: getText, + }); + + Object.defineProperty(this, 'data', { + enumerable: true, + get: getData, + }); + } +} + +const clients = { + 'http:': http, + 'https:': https, +}; + +function makeCallback(resolve, reject) { + return function (response) { + const chunks = []; + response.on('data', chunk => { + chunks.push(chunk); + }); + response.on('end', () => { + const body = Buffer.concat(chunks); + const httpResponse = new HTTPResponse(response, body); + + // Consider <200 && >= 400 as errors + if (httpResponse.status < 200 || httpResponse.status >= 400) { + return reject(httpResponse); + } else { + return resolve(httpResponse); + } + }); + response.on('error', reject); + }; +} + +const encodeBody = function ({ body, headers = {} }) { + if (typeof body !== 'object') { + return { body, headers }; + } + var contentTypeKeys = Object.keys(headers).filter(key => { + return key.match(/content-type/i) != null; + }); + + if (contentTypeKeys.length == 0) { + // no content type + // 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) { + log.error('Parse.Cloud.httpRequest', '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)) { + body = JSON.stringify(body); + } else if (headers[contentType].match(/application\/x-www-form-urlencoded/i)) { + body = querystring.stringify(body); + } + } + return { body, headers }; +}; + +function httpRequest(options) { + let url; + try { + url = parse(options.url); + } catch (e) { + return Promise.reject(e); + } + options = Object.assign(options, encodeBody(options)); + // support params options + if (typeof options.params === 'object') { + options.qs = options.params; + } else if (typeof options.params === 'string') { + options.qs = querystring.parse(options.params); + } + const client = clients[url.protocol]; + if (!client) { + return Promise.reject(`Unsupported protocol ${url.protocol}`); + } + const requestOptions = { + method: options.method, + port: Number(url.port), + path: url.pathname, + hostname: url.hostname, + headers: options.headers, + encoding: null, + followRedirects: options.followRedirects === true, + }; + if (requestOptions.headers) { + Object.keys(requestOptions.headers).forEach(key => { + if (typeof requestOptions.headers[key] === 'undefined') { + delete requestOptions.headers[key]; + } + }); + } + if (url.search) { + options.qs = Object.assign({}, options.qs, querystring.parse(url.query)); + } + if (url.auth) { + requestOptions.auth = url.auth; + } + if (options.qs) { + requestOptions.path += `?${querystring.stringify(options.qs)}`; + } + if (options.agent) { + requestOptions.agent = options.agent; + } + return new Promise((resolve, reject) => { + const req = client.request(requestOptions, makeCallback(resolve, reject, options)); + if (options.body) { + req.write(options.body); + } + req.on('error', error => { + reject(error); + }); + req.end(); + }); +} +module.exports = httpRequest; +module.exports.encodeBody = encodeBody; +module.exports.HTTPResponse = HTTPResponse;