UNPKG

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