@jsforce/jsforce-node 3.0.0-next.1 → 3.0.0-next.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,7 +19,8 @@ import QuickAction from './quick-action';
19
19
  import Process from './process';
20
20
  import Analytics from './api/analytics';
21
21
  import Apex from './api/apex';
22
- import { Bulk, BulkV2 } from './api/bulk';
22
+ import { Bulk } from './api/bulk';
23
+ import { BulkV2 } from './api/bulk2';
23
24
  import Chatter from './api/chatter';
24
25
  import Metadata from './api/metadata';
25
26
  import SoapApi from './api/soap';
package/lib/http-api.js CHANGED
@@ -13,6 +13,7 @@ const logger_1 = require("./util/logger");
13
13
  const promise_1 = require("./util/promise");
14
14
  const csv_1 = require("./csv");
15
15
  const stream_1 = require("./util/stream");
16
+ const get_body_size_1 = require("./util/get-body-size");
16
17
  /** @private */
17
18
  function parseJSON(str) {
18
19
  return JSON.parse(str);
@@ -103,6 +104,23 @@ class HttpApi extends events_1.EventEmitter {
103
104
  // when session refresh delegate is available
104
105
  if (this.isSessionExpired(response) && refreshDelegate) {
105
106
  await refreshDelegate.refresh(requestTime);
107
+ /* remove the `content-length` header after token refresh
108
+ *
109
+ * SOAP requests include the access token their the body,
110
+ * if the first req had an invalid token and jsforce successfully
111
+ * refreshed it we need to remove the `content-length` header
112
+ * so that it get's re-calculated again with the new body.
113
+ *
114
+ * REST request aren't affected by this because the access token
115
+ * is sent via HTTP headers
116
+ *
117
+ * `_message` is only present in SOAP requests
118
+ */
119
+ if ('_message' in request &&
120
+ request.headers &&
121
+ 'content-length' in request.headers) {
122
+ delete request.headers['content-length'];
123
+ }
106
124
  return this.request(request);
107
125
  }
108
126
  if (this.isErrorResponse(response)) {
@@ -137,6 +155,16 @@ class HttpApi extends events_1.EventEmitter {
137
155
  }
138
156
  headers['Sforce-Call-Options'] = callOptions.join(', ');
139
157
  }
158
+ const bodySize = (0, get_body_size_1.getBodySize)(request.body, headers);
159
+ const cannotHaveBody = ['GET', 'HEAD', 'OPTIONS'].includes(request.method);
160
+ if (!cannotHaveBody &&
161
+ !!request.body &&
162
+ !('transfer-encoding' in headers) &&
163
+ !('content-length' in headers) &&
164
+ !!bodySize) {
165
+ this._logger.debug(`missing 'content-length' header, setting it to: ${bodySize}`);
166
+ headers['content-length'] = String(bodySize);
167
+ }
140
168
  request.headers = headers;
141
169
  }
142
170
  /**
@@ -214,6 +242,10 @@ class HttpApi extends events_1.EventEmitter {
214
242
  */
215
243
  parseError(body) {
216
244
  const errors = body;
245
+ // XML response
246
+ if (errors.Errors) {
247
+ return errors.Errors.Error;
248
+ }
217
249
  return Array.isArray(errors) ? errors[0] : errors;
218
250
  }
219
251
  /**
@@ -237,6 +269,12 @@ class HttpApi extends events_1.EventEmitter {
237
269
  errorCode: `ERROR_HTTP_${response.statusCode}`,
238
270
  message: response.body,
239
271
  };
272
+ if (response.headers['content-type'] === 'text/html') {
273
+ this._logger.debug(`html response.body: ${response.body}`);
274
+ return new HttpApiError(`HTTP response contains html content.
275
+ Check that the org exists and can be reached.
276
+ See error.content for the full html response.`, error.errorCode, error.message);
277
+ }
240
278
  return new HttpApiError(error.message, error.errorCode);
241
279
  }
242
280
  }
package/lib/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import jsforce from './jsforce';
2
2
  import './api/analytics';
3
3
  import './api/apex';
4
4
  import './api/bulk';
5
+ import './api/bulk2';
5
6
  import './api/chatter';
6
7
  import './api/metadata';
7
8
  import './api/soap';
package/lib/index.js CHANGED
@@ -21,6 +21,7 @@ const jsforce_1 = __importDefault(require("./jsforce"));
21
21
  require("./api/analytics");
22
22
  require("./api/apex");
23
23
  require("./api/bulk");
24
+ require("./api/bulk2");
24
25
  require("./api/chatter");
25
26
  require("./api/metadata");
26
27
  require("./api/soap");
package/lib/request.js CHANGED
@@ -4,10 +4,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.setDefaults = void 0;
7
+ const stream_1 = require("stream");
7
8
  const node_fetch_1 = __importDefault(require("node-fetch"));
8
9
  const abort_controller_1 = __importDefault(require("abort-controller"));
9
10
  const https_proxy_agent_1 = __importDefault(require("https-proxy-agent"));
10
11
  const request_helper_1 = require("./request-helper");
12
+ const logger_1 = require("./util/logger");
13
+ const is_1 = __importDefault(require("@sindresorhus/is"));
11
14
  /**
12
15
  *
13
16
  */
@@ -23,13 +26,35 @@ exports.setDefaults = setDefaults;
23
26
  *
24
27
  */
25
28
  async function startFetchRequest(request, options, input, output, emitter, counter = 0) {
29
+ const logger = (0, logger_1.getLogger)('fetch');
26
30
  const { httpProxy, followRedirect } = options;
27
31
  const agent = httpProxy ? (0, https_proxy_agent_1.default)(httpProxy) : undefined;
28
32
  const { url, body, ...rrequest } = request;
29
33
  const controller = new abort_controller_1.default();
30
- let res;
31
- try {
32
- res = await (0, request_helper_1.executeWithTimeout)(() => (0, node_fetch_1.default)(url, {
34
+ let retryCount = 0;
35
+ const retryOpts = {
36
+ maxRetries: options.retry?.maxRetries ?? 5,
37
+ errorCodes: options.retry?.errorCodes ?? [
38
+ 'ECONNRESET',
39
+ 'ECONNREFUSED',
40
+ 'ENOTFOUND',
41
+ 'ENETDOWN',
42
+ 'ENETUNREACH',
43
+ 'EHOSTDOWN',
44
+ 'UND_ERR_SOCKET',
45
+ 'ETIMEDOUT',
46
+ 'EPIPE',
47
+ ],
48
+ methods: options.retry?.methods ?? [
49
+ 'GET',
50
+ 'PUT',
51
+ 'HEAD',
52
+ 'OPTIONS',
53
+ 'DELETE',
54
+ ],
55
+ };
56
+ const fetchWithRetries = async (maxRetry = retryOpts?.maxRetries) => {
57
+ const fetchOpts = {
33
58
  ...rrequest,
34
59
  ...(input && /^(post|put|patch)$/i.test(request.method)
35
60
  ? { body: input }
@@ -37,7 +62,51 @@ async function startFetchRequest(request, options, input, output, emitter, count
37
62
  redirect: 'manual',
38
63
  signal: controller.signal,
39
64
  agent,
40
- }), options.timeout, () => controller.abort());
65
+ };
66
+ try {
67
+ return await (0, node_fetch_1.default)(url, fetchOpts);
68
+ }
69
+ catch (err) {
70
+ logger.debug(`Request failed`);
71
+ const error = err;
72
+ // request was canceled by consumer (AbortController), skip retry and rethrow.
73
+ if (error.name === 'AbortError') {
74
+ throw error;
75
+ }
76
+ const shouldRetry = () => {
77
+ // only retry on operational errors
78
+ if (error.name != 'FetchError')
79
+ return false;
80
+ if (retryCount === maxRetry)
81
+ return false;
82
+ if (!retryOpts?.methods?.includes(request.method))
83
+ return false;
84
+ if (is_1.default.nodeStream(body) && stream_1.Readable.isDisturbed(body)) {
85
+ logger.debug('Body of type stream was read, unable to retry request.');
86
+ return false;
87
+ }
88
+ if ('code' in error &&
89
+ error.code &&
90
+ retryOpts?.errorCodes?.includes(error.code))
91
+ return true;
92
+ return false;
93
+ };
94
+ if (shouldRetry()) {
95
+ logger.debug(`retrying for the ${retryCount + 1} time`);
96
+ logger.debug(`Error: ${error}`);
97
+ // NOTE: this event is only used by tests and will be removed at any time.
98
+ // jsforce may switch to node's fetch which doesn't emit this event on retries.
99
+ emitter.emit('retry', retryCount);
100
+ retryCount++;
101
+ return await fetchWithRetries(maxRetry);
102
+ }
103
+ logger.debug('Skipping retry...');
104
+ throw err;
105
+ }
106
+ };
107
+ let res;
108
+ try {
109
+ res = await (0, request_helper_1.executeWithTimeout)(fetchWithRetries, options.timeout, () => controller.abort());
41
110
  }
42
111
  catch (err) {
43
112
  emitter.emit('error', err);
package/lib/soap.js CHANGED
@@ -10,6 +10,7 @@ exports.SOAP = exports.castTypeUsingSchema = void 0;
10
10
  */
11
11
  const http_api_1 = __importDefault(require("./http-api"));
12
12
  const function_1 = require("./util/function");
13
+ const get_body_size_1 = require("./util/get-body-size");
13
14
  /**
14
15
  *
15
16
  */
@@ -196,6 +197,16 @@ class SOAP extends http_api_1.default {
196
197
  /** @override */
197
198
  beforeSend(request) {
198
199
  request.body = this._createEnvelope(request._message);
200
+ const headers = request.headers || {};
201
+ const bodySize = (0, get_body_size_1.getBodySize)(request.body, request.headers);
202
+ if (request.method === 'POST' &&
203
+ !('transfer-encoding' in headers) &&
204
+ !('content-length' in headers) &&
205
+ !!bodySize) {
206
+ this._logger.debug(`missing 'content-length' header, setting it to: ${bodySize}`);
207
+ headers['content-length'] = String(bodySize);
208
+ }
209
+ request.headers = headers;
199
210
  }
200
211
  /** @override **/
201
212
  isSessionExpired(response) {
@@ -9,7 +9,7 @@ import * as FormData from 'form-data';
9
9
  */
10
10
  export type Callback<T, T2 = undefined> = (err: Error | null | undefined, ret?: T, ret2?: T2) => any;
11
11
  export type HttpBody = Buffer | URLSearchParams | NodeJS.ReadableStream | string | FormData | null;
12
- export type HttpMethods = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS';
12
+ export type HttpMethods = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';
13
13
  export type HttpRequest = {
14
14
  url: string;
15
15
  method: HttpMethods;
@@ -19,6 +19,11 @@ export type HttpRequest = {
19
19
  body?: HttpBody;
20
20
  };
21
21
  export type HttpRequestOptions = {
22
+ retry?: {
23
+ maxRetries?: number;
24
+ errorCodes?: string[];
25
+ methods?: HttpMethods[];
26
+ };
22
27
  httpProxy?: string;
23
28
  timeout?: number;
24
29
  followRedirect?: boolean | ((redirectUrl: string) => HttpRequest | null);
@@ -0,0 +1,4 @@
1
+ import { HttpBody } from '../types';
2
+ export declare function getBodySize(body: HttpBody | undefined, headers?: {
3
+ [name: string]: string;
4
+ }): number | undefined;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getBodySize = void 0;
7
+ const is_1 = __importDefault(require("@sindresorhus/is"));
8
+ function getBodySize(body, headers = {}) {
9
+ function isFormData(body) {
10
+ return is_1.default.nodeStream(body) && is_1.default.function_(body.getBoundary);
11
+ }
12
+ if (headers && 'content-length' in headers) {
13
+ return Number(headers['content-length']);
14
+ }
15
+ if (!body) {
16
+ return 0;
17
+ }
18
+ if (is_1.default.string(body)) {
19
+ return Buffer.byteLength(body);
20
+ }
21
+ if (is_1.default.urlSearchParams(body)) {
22
+ return Buffer.byteLength(body.toString());
23
+ }
24
+ if (is_1.default.buffer(body)) {
25
+ return body.length;
26
+ }
27
+ try {
28
+ // `getLengthSync` will throw if body has a stream:
29
+ // https://github.com/form-data/form-data#integer-getlengthsync
30
+ if (isFormData(body)) {
31
+ return body.getLengthSync();
32
+ }
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ return undefined;
38
+ }
39
+ exports.getBodySize = getBodySize;
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "database.com"
11
11
  ],
12
12
  "homepage": "http://github.com/jsforce/jsforce",
13
- "version": "3.0.0-next.1",
13
+ "version": "3.0.0-next.3",
14
14
  "repository": {
15
15
  "type": "git",
16
16
  "url": "git://github.com/jsforce/jsforce.git"
@@ -191,6 +191,7 @@
191
191
  "node": ">=18"
192
192
  },
193
193
  "dependencies": {
194
+ "@sindresorhus/is": "^4",
194
195
  "@types/node": "^18.15.3",
195
196
  "abort-controller": "^3.0.0",
196
197
  "base64url": "^3.0.1",
@@ -240,6 +241,7 @@
240
241
  "karma-jasmine-html-reporter": "^2.0.0",
241
242
  "karma-sourcemap-loader": "^0.4.0",
242
243
  "karma-webpack": "^5.0.0",
244
+ "nock": "^13.4.0",
243
245
  "path-browserify": "^1.0.1",
244
246
  "power-assert": "^1.6.1",
245
247
  "prettier": "^2.2.1",