UNPKG

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