UNPKG

25.7 kBJavaScriptView Raw
1"use strict";
2/*!
3 * Copyright 2022 Google LLC. All Rights Reserved.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17Object.defineProperty(exports, "__esModule", { value: true });
18exports.util = exports.Util = exports.PartialFailureError = exports.ApiError = void 0;
19/*!
20 * @module common/util
21 */
22const projectify_1 = require("@google-cloud/projectify");
23const ent = require("ent");
24const extend = require("extend");
25const google_auth_library_1 = require("google-auth-library");
26const retryRequest = require("retry-request");
27const stream_1 = require("stream");
28const teeny_request_1 = require("teeny-request");
29// eslint-disable-next-line @typescript-eslint/no-var-requires
30const duplexify = require('duplexify');
31const requestDefaults = {
32 timeout: 60000,
33 gzip: true,
34 forever: true,
35 pool: {
36 maxSockets: Infinity,
37 },
38};
39/**
40 * Default behavior: Automatically retry retriable server errors.
41 *
42 * @const {boolean}
43 * @private
44 */
45const AUTO_RETRY_DEFAULT = true;
46/**
47 * Default behavior: Only attempt to retry retriable errors 3 times.
48 *
49 * @const {number}
50 * @private
51 */
52const MAX_RETRY_DEFAULT = 3;
53/**
54 * Custom error type for API errors.
55 *
56 * @param {object} errorBody - Error object.
57 */
58class ApiError extends Error {
59 constructor(errorBodyOrMessage) {
60 super();
61 if (typeof errorBodyOrMessage !== 'object') {
62 this.message = errorBodyOrMessage || '';
63 return;
64 }
65 const errorBody = errorBodyOrMessage;
66 this.code = errorBody.code;
67 this.errors = errorBody.errors;
68 this.response = errorBody.response;
69 try {
70 this.errors = JSON.parse(this.response.body).error.errors;
71 }
72 catch (e) {
73 this.errors = errorBody.errors;
74 }
75 this.message = ApiError.createMultiErrorMessage(errorBody, this.errors);
76 Error.captureStackTrace(this);
77 }
78 /**
79 * Pieces together an error message by combining all unique error messages
80 * returned from a single GoogleError
81 *
82 * @private
83 *
84 * @param {GoogleErrorBody} err The original error.
85 * @param {GoogleInnerError[]} [errors] Inner errors, if any.
86 * @returns {string}
87 */
88 static createMultiErrorMessage(err, errors) {
89 const messages = new Set();
90 if (err.message) {
91 messages.add(err.message);
92 }
93 if (errors && errors.length) {
94 errors.forEach(({ message }) => messages.add(message));
95 }
96 else if (err.response && err.response.body) {
97 messages.add(ent.decode(err.response.body.toString()));
98 }
99 else if (!err.message) {
100 messages.add('A failure occurred during this request.');
101 }
102 let messageArr = Array.from(messages);
103 if (messageArr.length > 1) {
104 messageArr = messageArr.map((message, i) => ` ${i + 1}. ${message}`);
105 messageArr.unshift('Multiple errors occurred during the request. Please see the `errors` array for complete details.\n');
106 messageArr.push('\n');
107 }
108 return messageArr.join('\n');
109 }
110}
111exports.ApiError = ApiError;
112/**
113 * Custom error type for partial errors returned from the API.
114 *
115 * @param {object} b - Error object.
116 */
117class PartialFailureError extends Error {
118 constructor(b) {
119 super();
120 const errorObject = b;
121 this.errors = errorObject.errors;
122 this.name = 'PartialFailureError';
123 this.response = errorObject.response;
124 this.message = ApiError.createMultiErrorMessage(errorObject, this.errors);
125 }
126}
127exports.PartialFailureError = PartialFailureError;
128class Util {
129 constructor() {
130 this.ApiError = ApiError;
131 this.PartialFailureError = PartialFailureError;
132 }
133 /**
134 * No op.
135 *
136 * @example
137 * function doSomething(callback) {
138 * callback = callback || noop;
139 * }
140 */
141 noop() { }
142 /**
143 * Uniformly process an API response.
144 *
145 * @param {*} err - Error value.
146 * @param {*} resp - Response value.
147 * @param {*} body - Body value.
148 * @param {function} callback - The callback function.
149 */
150 handleResp(err, resp, body, callback) {
151 callback = callback || util.noop;
152 const parsedResp = extend(true, { err: err || null }, resp && util.parseHttpRespMessage(resp), body && util.parseHttpRespBody(body));
153 // Assign the parsed body to resp.body, even if { json: false } was passed
154 // as a request option.
155 // We assume that nobody uses the previously unparsed value of resp.body.
156 if (!parsedResp.err && resp && typeof parsedResp.body === 'object') {
157 parsedResp.resp.body = parsedResp.body;
158 }
159 if (parsedResp.err && resp) {
160 parsedResp.err.response = resp;
161 }
162 callback(parsedResp.err, parsedResp.body, parsedResp.resp);
163 }
164 /**
165 * Sniff an incoming HTTP response message for errors.
166 *
167 * @param {object} httpRespMessage - An incoming HTTP response message from `request`.
168 * @return {object} parsedHttpRespMessage - The parsed response.
169 * @param {?error} parsedHttpRespMessage.err - An error detected.
170 * @param {object} parsedHttpRespMessage.resp - The original response object.
171 */
172 parseHttpRespMessage(httpRespMessage) {
173 const parsedHttpRespMessage = {
174 resp: httpRespMessage,
175 };
176 if (httpRespMessage.statusCode < 200 || httpRespMessage.statusCode > 299) {
177 // Unknown error. Format according to ApiError standard.
178 parsedHttpRespMessage.err = new ApiError({
179 errors: new Array(),
180 code: httpRespMessage.statusCode,
181 message: httpRespMessage.statusMessage,
182 response: httpRespMessage,
183 });
184 }
185 return parsedHttpRespMessage;
186 }
187 /**
188 * Parse the response body from an HTTP request.
189 *
190 * @param {object} body - The response body.
191 * @return {object} parsedHttpRespMessage - The parsed response.
192 * @param {?error} parsedHttpRespMessage.err - An error detected.
193 * @param {object} parsedHttpRespMessage.body - The original body value provided
194 * will try to be JSON.parse'd. If it's successful, the parsed value will
195 * be returned here, otherwise the original value and an error will be returned.
196 */
197 parseHttpRespBody(body) {
198 const parsedHttpRespBody = {
199 body,
200 };
201 if (typeof body === 'string') {
202 try {
203 parsedHttpRespBody.body = JSON.parse(body);
204 }
205 catch (err) {
206 parsedHttpRespBody.body = body;
207 }
208 }
209 if (parsedHttpRespBody.body && parsedHttpRespBody.body.error) {
210 // Error from JSON API.
211 parsedHttpRespBody.err = new ApiError(parsedHttpRespBody.body.error);
212 }
213 return parsedHttpRespBody;
214 }
215 /**
216 * Take a Duplexify stream, fetch an authenticated connection header, and
217 * create an outgoing writable stream.
218 *
219 * @param {Duplexify} dup - Duplexify stream.
220 * @param {object} options - Configuration object.
221 * @param {module:common/connection} options.connection - A connection instance used to get a token with and send the request through.
222 * @param {object} options.metadata - Metadata to send at the head of the request.
223 * @param {object} options.request - Request object, in the format of a standard Node.js http.request() object.
224 * @param {string=} options.request.method - Default: "POST".
225 * @param {string=} options.request.qs.uploadType - Default: "multipart".
226 * @param {string=} options.streamContentType - Default: "application/octet-stream".
227 * @param {function} onComplete - Callback, executed after the writable Request stream has completed.
228 */
229 makeWritableStream(dup, options, onComplete) {
230 onComplete = onComplete || util.noop;
231 const writeStream = new ProgressStream();
232 writeStream.on('progress', evt => dup.emit('progress', evt));
233 dup.setWritable(writeStream);
234 const defaultReqOpts = {
235 method: 'POST',
236 qs: {
237 uploadType: 'multipart',
238 },
239 timeout: 0,
240 maxRetries: 0,
241 };
242 const metadata = options.metadata || {};
243 const reqOpts = extend(true, defaultReqOpts, options.request, {
244 multipart: [
245 {
246 'Content-Type': 'application/json',
247 body: JSON.stringify(metadata),
248 },
249 {
250 'Content-Type': metadata.contentType || 'application/octet-stream',
251 body: writeStream,
252 },
253 ],
254 });
255 options.makeAuthenticatedRequest(reqOpts, {
256 onAuthenticated(err, authenticatedReqOpts) {
257 if (err) {
258 dup.destroy(err);
259 return;
260 }
261 const request = teeny_request_1.teenyRequest.defaults(requestDefaults);
262 request(authenticatedReqOpts, (err, resp, body) => {
263 util.handleResp(err, resp, body, (err, data) => {
264 if (err) {
265 dup.destroy(err);
266 return;
267 }
268 dup.emit('response', resp);
269 onComplete(data);
270 });
271 });
272 },
273 });
274 }
275 /**
276 * Returns true if the API request should be retried, given the error that was
277 * given the first time the request was attempted. This is used for rate limit
278 * related errors as well as intermittent server errors.
279 *
280 * @param {error} err - The API error to check if it is appropriate to retry.
281 * @return {boolean} True if the API request should be retried, false otherwise.
282 */
283 shouldRetryRequest(err) {
284 if (err) {
285 if ([408, 429, 500, 502, 503, 504].indexOf(err.code) !== -1) {
286 return true;
287 }
288 if (err.errors) {
289 for (const e of err.errors) {
290 const reason = e.reason;
291 if (reason === 'rateLimitExceeded') {
292 return true;
293 }
294 if (reason === 'userRateLimitExceeded') {
295 return true;
296 }
297 if (reason && reason.includes('EAI_AGAIN')) {
298 return true;
299 }
300 }
301 }
302 }
303 return false;
304 }
305 /**
306 * Get a function for making authenticated requests.
307 *
308 * @param {object} config - Configuration object.
309 * @param {boolean=} config.autoRetry - Automatically retry requests if the
310 * response is related to rate limits or certain intermittent server
311 * errors. We will exponentially backoff subsequent requests by default.
312 * (default: true)
313 * @param {object=} config.credentials - Credentials object.
314 * @param {boolean=} config.customEndpoint - If true, just return the provided request options. Default: false.
315 * @param {boolean=} config.useAuthWithCustomEndpoint - If true, will authenticate when using a custom endpoint. Default: false.
316 * @param {string=} config.email - Account email address, required for PEM/P12 usage.
317 * @param {number=} config.maxRetries - Maximum number of automatic retries attempted before returning the error. (default: 3)
318 * @param {string=} config.keyFile - Path to a .json, .pem, or .p12 keyfile.
319 * @param {array} config.scopes - Array of scopes required for the API.
320 */
321 makeAuthenticatedRequestFactory(config) {
322 const googleAutoAuthConfig = extend({}, config);
323 if (googleAutoAuthConfig.projectId === '{{projectId}}') {
324 delete googleAutoAuthConfig.projectId;
325 }
326 let authClient;
327 if (googleAutoAuthConfig.authClient instanceof google_auth_library_1.GoogleAuth) {
328 // Use an existing `GoogleAuth`
329 authClient = googleAutoAuthConfig.authClient;
330 }
331 else {
332 // Pass an `AuthClient` to `GoogleAuth`, if available
333 const config = {
334 ...googleAutoAuthConfig,
335 authClient: googleAutoAuthConfig.authClient,
336 };
337 authClient = new google_auth_library_1.GoogleAuth(config);
338 }
339 function makeAuthenticatedRequest(reqOpts, optionsOrCallback) {
340 let stream;
341 let projectId;
342 const reqConfig = extend({}, config);
343 let activeRequest_;
344 if (!optionsOrCallback) {
345 stream = duplexify();
346 reqConfig.stream = stream;
347 }
348 const options = typeof optionsOrCallback === 'object' ? optionsOrCallback : undefined;
349 const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : undefined;
350 const onAuthenticated = (err, authenticatedReqOpts) => {
351 const authLibraryError = err;
352 const autoAuthFailed = err &&
353 err.message.indexOf('Could not load the default credentials') > -1;
354 if (autoAuthFailed) {
355 // Even though authentication failed, the API might not actually
356 // care.
357 authenticatedReqOpts = reqOpts;
358 }
359 if (!err || autoAuthFailed) {
360 try {
361 authenticatedReqOpts = util.decorateRequest(authenticatedReqOpts, projectId);
362 err = null;
363 }
364 catch (e) {
365 // A projectId was required, but we don't have one.
366 // Re-use the "Could not load the default credentials error" if
367 // auto auth failed.
368 err = err || e;
369 }
370 }
371 if (err) {
372 if (stream) {
373 stream.destroy(err);
374 }
375 else {
376 const fn = options && options.onAuthenticated
377 ? options.onAuthenticated
378 : callback;
379 fn(err);
380 }
381 return;
382 }
383 if (options && options.onAuthenticated) {
384 options.onAuthenticated(null, authenticatedReqOpts);
385 }
386 else {
387 activeRequest_ = util.makeRequest(authenticatedReqOpts, reqConfig, (apiResponseError, ...params) => {
388 if (apiResponseError &&
389 apiResponseError.code === 401 &&
390 authLibraryError) {
391 // Re-use the "Could not load the default credentials error" if
392 // the API request failed due to missing credentials.
393 apiResponseError = authLibraryError;
394 }
395 callback(apiResponseError, ...params);
396 });
397 }
398 };
399 Promise.all([
400 config.projectId && config.projectId !== '{{projectId}}'
401 ? // The user provided a project ID. We don't need to check with the
402 // auth client, it could be incorrect.
403 new Promise(resolve => resolve(config.projectId))
404 : authClient.getProjectId(),
405 reqConfig.customEndpoint && reqConfig.useAuthWithCustomEndpoint !== true
406 ? // Using a custom API override. Do not use `google-auth-library` for
407 // authentication. (ex: connecting to a local Datastore server)
408 new Promise(resolve => resolve(reqOpts))
409 : authClient.authorizeRequest(reqOpts),
410 ])
411 .then(([_projectId, authorizedReqOpts]) => {
412 projectId = _projectId;
413 onAuthenticated(null, authorizedReqOpts);
414 })
415 .catch(onAuthenticated);
416 if (stream) {
417 return stream;
418 }
419 return {
420 abort() {
421 setImmediate(() => {
422 if (activeRequest_) {
423 activeRequest_.abort();
424 activeRequest_ = null;
425 }
426 });
427 },
428 };
429 }
430 const mar = makeAuthenticatedRequest;
431 mar.getCredentials = authClient.getCredentials.bind(authClient);
432 mar.authClient = authClient;
433 return mar;
434 }
435 /**
436 * Make a request through the `retryRequest` module with built-in error
437 * handling and exponential back off.
438 *
439 * @param {object} reqOpts - Request options in the format `request` expects.
440 * @param {object=} config - Configuration object.
441 * @param {boolean=} config.autoRetry - Automatically retry requests if the
442 * response is related to rate limits or certain intermittent server
443 * errors. We will exponentially backoff subsequent requests by default.
444 * (default: true)
445 * @param {number=} config.maxRetries - Maximum number of automatic retries
446 * attempted before returning the error. (default: 3)
447 * @param {object=} config.request - HTTP module for request calls.
448 * @param {function} callback - The callback function.
449 */
450 makeRequest(reqOpts, config, callback) {
451 var _a, _b, _c, _d, _e, _f, _g;
452 let autoRetryValue = AUTO_RETRY_DEFAULT;
453 if (config.autoRetry !== undefined &&
454 ((_a = config.retryOptions) === null || _a === void 0 ? void 0 : _a.autoRetry) !== undefined) {
455 throw new ApiError('autoRetry is deprecated. Use retryOptions.autoRetry instead.');
456 }
457 else if (config.autoRetry !== undefined) {
458 autoRetryValue = config.autoRetry;
459 }
460 else if (((_b = config.retryOptions) === null || _b === void 0 ? void 0 : _b.autoRetry) !== undefined) {
461 autoRetryValue = config.retryOptions.autoRetry;
462 }
463 let maxRetryValue = MAX_RETRY_DEFAULT;
464 if (config.maxRetries && ((_c = config.retryOptions) === null || _c === void 0 ? void 0 : _c.maxRetries)) {
465 throw new ApiError('maxRetries is deprecated. Use retryOptions.maxRetries instead.');
466 }
467 else if (config.maxRetries) {
468 maxRetryValue = config.maxRetries;
469 }
470 else if ((_d = config.retryOptions) === null || _d === void 0 ? void 0 : _d.maxRetries) {
471 maxRetryValue = config.retryOptions.maxRetries;
472 }
473 const options = {
474 request: teeny_request_1.teenyRequest.defaults(requestDefaults),
475 retries: autoRetryValue !== false ? maxRetryValue : 0,
476 noResponseRetries: autoRetryValue !== false ? maxRetryValue : 0,
477 shouldRetryFn(httpRespMessage) {
478 var _a, _b;
479 const err = util.parseHttpRespMessage(httpRespMessage).err;
480 if ((_a = config.retryOptions) === null || _a === void 0 ? void 0 : _a.retryableErrorFn) {
481 return err && ((_b = config.retryOptions) === null || _b === void 0 ? void 0 : _b.retryableErrorFn(err));
482 }
483 return err && util.shouldRetryRequest(err);
484 },
485 maxRetryDelay: (_e = config.retryOptions) === null || _e === void 0 ? void 0 : _e.maxRetryDelay,
486 retryDelayMultiplier: (_f = config.retryOptions) === null || _f === void 0 ? void 0 : _f.retryDelayMultiplier,
487 totalTimeout: (_g = config.retryOptions) === null || _g === void 0 ? void 0 : _g.totalTimeout,
488 };
489 if (typeof reqOpts.maxRetries === 'number') {
490 options.retries = reqOpts.maxRetries;
491 }
492 if (!config.stream) {
493 return retryRequest(reqOpts, options,
494 // eslint-disable-next-line @typescript-eslint/no-explicit-any
495 (err, response, body) => {
496 util.handleResp(err, response, body, callback);
497 });
498 }
499 const dup = config.stream;
500 // eslint-disable-next-line @typescript-eslint/no-explicit-any
501 let requestStream;
502 const isGetRequest = (reqOpts.method || 'GET').toUpperCase() === 'GET';
503 if (isGetRequest) {
504 requestStream = retryRequest(reqOpts, options);
505 dup.setReadable(requestStream);
506 }
507 else {
508 // Streaming writable HTTP requests cannot be retried.
509 requestStream = options.request(reqOpts);
510 dup.setWritable(requestStream);
511 }
512 // Replay the Request events back to the stream.
513 requestStream
514 .on('error', dup.destroy.bind(dup))
515 .on('response', dup.emit.bind(dup, 'response'))
516 .on('complete', dup.emit.bind(dup, 'complete'));
517 dup.abort = requestStream.abort;
518 return dup;
519 }
520 /**
521 * Decorate the options about to be made in a request.
522 *
523 * @param {object} reqOpts - The options to be passed to `request`.
524 * @param {string} projectId - The project ID.
525 * @return {object} reqOpts - The decorated reqOpts.
526 */
527 decorateRequest(reqOpts, projectId) {
528 delete reqOpts.autoPaginate;
529 delete reqOpts.autoPaginateVal;
530 delete reqOpts.objectMode;
531 if (reqOpts.qs !== null && typeof reqOpts.qs === 'object') {
532 delete reqOpts.qs.autoPaginate;
533 delete reqOpts.qs.autoPaginateVal;
534 reqOpts.qs = projectify_1.replaceProjectIdToken(reqOpts.qs, projectId);
535 }
536 if (Array.isArray(reqOpts.multipart)) {
537 reqOpts.multipart = reqOpts.multipart.map(part => {
538 return projectify_1.replaceProjectIdToken(part, projectId);
539 });
540 }
541 if (reqOpts.json !== null && typeof reqOpts.json === 'object') {
542 delete reqOpts.json.autoPaginate;
543 delete reqOpts.json.autoPaginateVal;
544 reqOpts.json = projectify_1.replaceProjectIdToken(reqOpts.json, projectId);
545 }
546 reqOpts.uri = projectify_1.replaceProjectIdToken(reqOpts.uri, projectId);
547 return reqOpts;
548 }
549 // eslint-disable-next-line @typescript-eslint/no-explicit-any
550 isCustomType(unknown, module) {
551 function getConstructorName(obj) {
552 return obj.constructor && obj.constructor.name.toLowerCase();
553 }
554 const moduleNameParts = module.split('/');
555 const parentModuleName = moduleNameParts[0] && moduleNameParts[0].toLowerCase();
556 const subModuleName = moduleNameParts[1] && moduleNameParts[1].toLowerCase();
557 if (subModuleName && getConstructorName(unknown) !== subModuleName) {
558 return false;
559 }
560 let walkingModule = unknown;
561 // eslint-disable-next-line no-constant-condition
562 while (true) {
563 if (getConstructorName(walkingModule) === parentModuleName) {
564 return true;
565 }
566 walkingModule = walkingModule.parent;
567 if (!walkingModule) {
568 return false;
569 }
570 }
571 }
572 /**
573 * Create a properly-formatted User-Agent string from a package.json file.
574 *
575 * @param {object} packageJson - A module's package.json file.
576 * @return {string} userAgent - The formatted User-Agent string.
577 */
578 getUserAgentFromPackageJson(packageJson) {
579 const hyphenatedPackageName = packageJson.name
580 .replace('@google-cloud', 'gcloud-node') // For legacy purposes.
581 .replace('/', '-'); // For UA spec-compliance purposes.
582 return hyphenatedPackageName + '/' + packageJson.version;
583 }
584 /**
585 * Given two parameters, figure out if this is either:
586 * - Just a callback function
587 * - An options object, and then a callback function
588 * @param optionsOrCallback An options object or callback.
589 * @param cb A potentially undefined callback.
590 */
591 maybeOptionsOrCallback(optionsOrCallback, cb) {
592 return typeof optionsOrCallback === 'function'
593 ? [{}, optionsOrCallback]
594 : [optionsOrCallback, cb];
595 }
596}
597exports.Util = Util;
598/**
599 * Basic Passthrough Stream that records the number of bytes read
600 * every time the cursor is moved.
601 */
602class ProgressStream extends stream_1.Transform {
603 constructor() {
604 super(...arguments);
605 this.bytesRead = 0;
606 }
607 // eslint-disable-next-line @typescript-eslint/no-explicit-any
608 _transform(chunk, encoding, callback) {
609 this.bytesRead += chunk.length;
610 this.emit('progress', { bytesWritten: this.bytesRead, contentLength: '*' });
611 this.push(chunk);
612 callback();
613 }
614}
615const util = new Util();
616exports.util = util;
617//# sourceMappingURL=util.js.map
\No newline at end of file