@jsforce/jsforce-node
Version:
Salesforce API Library for JavaScript
321 lines (319 loc) • 11.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HttpApi = void 0;
/**
*
*/
const events_1 = require("events");
const xml2js_1 = __importDefault(require("xml2js"));
const logger_1 = require("./util/logger");
const promise_1 = require("./util/promise");
const csv_1 = require("./csv");
const stream_1 = require("./util/stream");
const get_body_size_1 = require("./util/get-body-size");
/** @private */
function parseJSON(str) {
return JSON.parse(str);
}
/** @private */
async function parseXML(str) {
return xml2js_1.default.parseStringPromise(str, { explicitArray: false });
}
/** @private */
function parseText(str) {
return str;
}
/**
* HTTP based API class with authorization hook
*/
class HttpApi extends events_1.EventEmitter {
static _logger = (0, logger_1.getLogger)('http-api');
_conn;
_logger;
_transport;
_responseType;
_noContentResponse;
_options;
constructor(conn, options) {
super();
this._conn = conn;
this._logger = conn._logLevel
? HttpApi._logger.createInstance(conn._logLevel)
: HttpApi._logger;
this._responseType = options.responseType;
this._transport = options.transport || conn._transport;
this._noContentResponse = options.noContentResponse;
this._options = options;
}
/**
* Callout to API endpoint using http
*/
request(request) {
return promise_1.StreamPromise.create(() => {
const { stream, setStream } = (0, stream_1.createLazyStream)();
const promise = (async () => {
const refreshDelegate = this.getRefreshDelegate();
/* TODO decide remove or not this section */
/*
// remember previous instance url in case it changes after a refresh
const lastInstanceUrl = conn.instanceUrl;
// check to see if the token refresh has changed the instance url
if(lastInstanceUrl !== conn.instanceUrl){
// if the instance url has changed
// then replace the current request urls instance url fragment
// with the updated instance url
request.url = request.url.replace(lastInstanceUrl,conn.instanceUrl);
}
*/
if (refreshDelegate && refreshDelegate.isRefreshing()) {
await refreshDelegate.waitRefresh();
const bodyPromise = this.request(request);
setStream(bodyPromise.stream());
const body = await bodyPromise;
return body;
}
// hook before sending
this.beforeSend(request);
this.emit('request', request);
this._logger.debug(`<request> method=${request.method}, url=${request.url}`);
const requestTime = Date.now();
const requestPromise = this._transport.httpRequest(request, this._options);
setStream(requestPromise.stream());
let response;
try {
response = await requestPromise;
}
catch (err) {
this._logger.error(err);
throw err;
}
finally {
const responseTime = Date.now();
this._logger.debug(`elapsed time: ${responseTime - requestTime} msec`);
}
if (!response) {
return;
}
this._logger.debug(`<response> status=${String(response.statusCode)}, url=${request.url}`);
this.emit('response', response);
// Refresh token if session has been expired and requires authentication
// when session refresh delegate is available
if (this.isSessionExpired(response) && refreshDelegate) {
await refreshDelegate.refresh(requestTime);
/* remove the `content-length` header after token refresh
*
* SOAP requests include the access token their the body,
* if the first req had an invalid token and jsforce successfully
* refreshed it we need to remove the `content-length` header
* so that it get's re-calculated again with the new body.
*
* REST request aren't affected by this because the access token
* is sent via HTTP headers
*
* `_message` is only present in SOAP requests
*/
if ('_message' in request &&
request.headers &&
'content-length' in request.headers) {
delete request.headers['content-length'];
}
return this.request(request);
}
if (this.isErrorResponse(response)) {
const err = await this.getError(response);
throw err;
}
const body = await this.getResponseBody(response);
return body;
})();
return { stream, promise };
});
}
/**
* @protected
*/
getRefreshDelegate() {
return this._conn._refreshDelegate;
}
/**
* @protected
*/
beforeSend(request) {
/* eslint-disable no-param-reassign */
const headers = request.headers || {};
if (this._conn.accessToken) {
headers.Authorization = `Bearer ${this._conn.accessToken}`;
}
if (this._conn._callOptions) {
const callOptions = [];
for (const name of Object.keys(this._conn._callOptions)) {
callOptions.push(`${name}=${this._conn._callOptions[name]}`);
}
headers['Sforce-Call-Options'] = callOptions.join(', ');
}
const bodySize = (0, get_body_size_1.getBodySize)(request.body, headers);
const cannotHaveBody = ['GET', 'HEAD', 'OPTIONS'].includes(request.method);
if (!cannotHaveBody &&
!!request.body &&
!('transfer-encoding' in headers) &&
!('content-length' in headers) &&
!!bodySize) {
this._logger.debug(`missing 'content-length' header, setting it to: ${bodySize}`);
headers['content-length'] = String(bodySize);
}
request.headers = headers;
}
/**
* Detect response content mime-type
* @protected
*/
getResponseContentType(response) {
return (this._responseType ||
(response.headers && response.headers['content-type']));
}
/**
* @private
*/
// eslint-disable-next-line @typescript-eslint/require-await
async parseResponseBody(response) {
const contentType = this.getResponseContentType(response) || '';
const parseBody = /^(text|application)\/xml(;|$)/.test(contentType)
? parseXML
: /^application\/json(;|$)/.test(contentType)
? parseJSON
: /^text\/csv(;|$)/.test(contentType)
? csv_1.parseCSV
: parseText;
try {
return parseBody(response.body);
}
catch (e) {
// TODO(next major): we could throw a new "invalid response body" error instead.
this._logger.debug(`Failed to parse body of content-type: ${contentType}. Error: ${e.message}`);
return response.body;
}
}
/**
* Get response body
* @protected
*/
async getResponseBody(response) {
if (response.statusCode === 204) {
// No Content
return this._noContentResponse;
}
const body = await this.parseResponseBody(response);
let err;
if (this.hasErrorInResponseBody(body)) {
err = await this.getError(response, body);
throw err;
}
if (response.statusCode === 300) {
// Multiple Choices
throw new HttpApiError('Multiple records found', 'MULTIPLE_CHOICES', body);
}
return body;
}
/**
* Detect session expiry
* @protected
*/
isSessionExpired(response) {
// TODO:
// The connected app msg only applies to Agent API requests, we should move this to a separate SFAP/Agent API class later.
return response.statusCode === 401 && !response.body.includes('Connected app is not attached to Agent');
}
/**
* Detect error response
* @protected
*/
isErrorResponse(response) {
return response.statusCode >= 400;
}
/**
* Detect error in response body
* @protected
*/
hasErrorInResponseBody(_body) {
return false;
}
/**
* Parsing error message in response
* @protected
*/
parseError(body) {
const errors = body;
// XML response
if (errors.Errors) {
return errors.Errors.Error;
}
return errors;
}
/**
* Get error message in response
* @protected
*/
async getError(response, body) {
let error;
try {
error = this.parseError(body || (await this.parseResponseBody(response)));
}
catch (e) {
// eslint-disable no-empty
}
if (Array.isArray(error)) {
if (error.length === 1) {
error = error[0];
}
else {
return new HttpApiError(`Multiple errors returned.
Check \`error.data\` for the error details`, 'MULTIPLE_API_ERRORS', error);
}
}
error =
typeof error === 'object' &&
error !== null &&
typeof error.message === 'string'
? error
: {
errorCode: `ERROR_HTTP_${response.statusCode}`,
message: response.body,
};
if (response.headers['content-type'] === 'text/html') {
this._logger.debug(`html response.body: ${response.body}`);
return new HttpApiError(`HTTP response contains html content.
Check that the org exists and can be reached.
See \`error.data\` for the full html response.`, error.errorCode, error.message);
}
return error instanceof HttpApiError ? error : new HttpApiError(error.message, error.errorCode, error);
}
}
exports.HttpApi = HttpApi;
/**
*
*/
class HttpApiError extends Error {
/**
* This contains error-specific details, usually returned from the API.
*/
data;
errorCode;
constructor(message, errorCode, data) {
super(message);
this.name = errorCode || this.name;
this.errorCode = this.name;
this.data = data;
}
/**
* This will be removed in the next major (v4)
*
* @deprecated use `error.data` instead
*/
get content() {
return this.data;
}
}
exports.default = HttpApi;