UNPKG

@datocms/rest-client-utils

Version:
225 lines 10.7 kB
"use strict"; /// <reference lib="dom" /> var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.request = exports.getFetchFn = exports.LogLevel = void 0; const buildNormalizedParams_1 = require("./buildNormalizedParams"); const errors_1 = require("./errors"); const makeCancelablePromise_1 = require("./makeCancelablePromise"); const wait_1 = require("./wait"); const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'; const MAX_RETRY_COUNT_ON_TIMEOUT_ERROR = 5; var LogLevel; (function (LogLevel) { /** No logging */ LogLevel[LogLevel["NONE"] = 0] = "NONE"; /** Logs HTTP requests (method, URL) and responses (status) */ LogLevel[LogLevel["BASIC"] = 1] = "BASIC"; /** Logs HTTP requests (method, URL, body) and responses (status, body) */ LogLevel[LogLevel["BODY"] = 2] = "BODY"; /** Logs HTTP requests (method, URL, headers, body) and responses (status, headers, body) */ LogLevel[LogLevel["BODY_AND_HEADERS"] = 3] = "BODY_AND_HEADERS"; })(LogLevel = exports.LogLevel || (exports.LogLevel = {})); function headersToObject(headers) { const result = {}; headers.forEach((value, key) => { result[key] = value; }); return result; } function buildApiErrorInitObject(method, url, requestHeaders, requestBody, response, responseBody, preCallStack) { return { request: { url, method, headers: requestHeaders, body: requestBody, }, response: { status: response.status, statusText: response.statusText, headers: headersToObject(response.headers), body: responseBody, }, preCallStack, }; } function buildTimeoutErrorInitObject(method, url, requestHeaders, requestBody, preCallStack) { return { request: { url, method, headers: requestHeaders, body: requestBody, }, preCallStack, }; } function buildApiErrorInitObjectFromJobResult(method, url, requestHeaders, requestBody, responseStatus, responseBody, preCallStack) { return { request: { url, method, headers: requestHeaders, body: requestBody, }, response: { status: responseStatus, statusText: 'N/A', headers: {}, body: responseBody, }, preCallStack, }; } function isErrorWithCode(error) { return typeof error === 'object' && !!error && 'code' in error; } function lowercaseKeys(obj) { return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k.toLowerCase(), v])); } let requestCount = 1; function getFetchFn(customFetchFn) { const fetchFn = customFetchFn || (typeof fetch === 'undefined' ? undefined : fetch) || (typeof globalThis === 'undefined' ? undefined : globalThis.fetch); if (typeof fetchFn === 'undefined') { throw new Error('fetch() is not available: either polyfill it globally, or provide it as fetchFn option.'); } return fetchFn; } exports.getFetchFn = getFetchFn; function request(options) { var _a; return __awaiter(this, void 0, void 0, function* () { const requestId = requestCount; requestCount += 1; const fetchFn = getFetchFn(options.fetchFn); const preCallStack = options.preCallStack; const userAgent = options.userAgent || '@datocms/rest-client-utils'; const retryCount = options.retryCount || 1; const logLevel = options.logLevel || LogLevel.NONE; const autoRetry = 'autoRetry' in options ? options.autoRetry : true; const log = options.logFn || (() => true); const headers = Object.assign({ 'content-type': 'application/json', accept: 'application/json', authorization: `Bearer ${options.apiToken}`, 'user-agent': userAgent }, (options.extraHeaders ? lowercaseKeys(options.extraHeaders) : {})); if (isBrowser) { // user agent cannot be set on browser delete headers['user-agent']; } const baseUrl = options.baseUrl.replace(/\/$/, ''); const body = options.body ? JSON.stringify(options.body, null, 2) : undefined; const queryString = options.queryParams && Object.keys(options.queryParams).length > 0 ? `?${new URLSearchParams((0, buildNormalizedParams_1.buildNormalizedParams)(options.queryParams)).toString()}` : ''; const url = `${baseUrl}${options.url}${queryString}`; if (logLevel >= LogLevel.BASIC) { log(`[${requestId}] ${options.method} ${url}`); if (logLevel >= LogLevel.BODY_AND_HEADERS) { for (const [key, value] of Object.entries(headers || {})) { log(`[${requestId}] ${key}: ${value}`); } } if (logLevel >= LogLevel.BODY && body) { log(`[${requestId}] ${body}`); } } try { const requestPromise = (0, makeCancelablePromise_1.makeCancelablePromise)(fetchFn(url, { method: options.method, headers, body, })); const timeoutId = setTimeout(() => { requestPromise.cancel(); }, options.requestTimeout || 30000); const response = yield requestPromise; clearTimeout(timeoutId); const responseContentType = response.headers.get('Content-Type'); const invalidContentType = responseContentType && !responseContentType.includes('application/json'); if (response.status === 429 || invalidContentType) { if (!autoRetry || (invalidContentType && options.method !== 'GET')) { throw new errors_1.ApiError(buildApiErrorInitObject(options.method, url, headers, options.body, response, undefined, preCallStack)); } const waitTimeInSecs = response.headers.has('X-RateLimit-Reset') ? Number.parseInt(response.headers.get('X-RateLimit-Reset'), 10) : retryCount; if (logLevel >= LogLevel.BASIC) { if (response.status === 429) { log(`[${requestId}] Rate limit exceeded, wait ${waitTimeInSecs} seconds then retry...`); } else { log(`[${requestId}] Invalid response content type "${responseContentType}" (status ${response.status}). Wait ${waitTimeInSecs} seconds then retry...`); } } yield (0, wait_1.wait)(waitTimeInSecs * 1000); return request(Object.assign(Object.assign({}, options), { retryCount: retryCount + 1 })); } if (logLevel >= LogLevel.BASIC) { log(`[${requestId}] Status: ${response.status} (${response.statusText})`); if (logLevel >= LogLevel.BODY_AND_HEADERS) { for (const key of [ 'x-api-version', 'x-environment', 'x-queue-time', 'x-ratelimit-remaining', 'x-request-id', 'cf-ray', ]) { const value = response.headers.get(key); if (value) { log(`[${requestId}] ${key}: ${value}`); } } } } let responseBody = response.status === 204 ? undefined : yield response.json(); if (logLevel >= LogLevel.BODY && responseBody) { log(`[${requestId}] ${JSON.stringify(responseBody, null, 2)}`); } if (response.status === 202) { const jobResult = yield options.fetchJobResult(responseBody.data.id); if (jobResult.status < 200 || jobResult.status >= 300) { throw new errors_1.ApiError(buildApiErrorInitObjectFromJobResult(options.method, url, headers, options.body, jobResult.status, jobResult.payload, preCallStack)); } responseBody = jobResult.payload; } if (response.status >= 200 && response.status < 300) { return responseBody; } const error = new errors_1.ApiError(buildApiErrorInitObject(options.method, url, headers, options.body, response, responseBody, preCallStack)); const transientErrorCode = (_a = error.errors.find((e) => e.attributes.transient)) === null || _a === void 0 ? void 0 : _a.attributes.code; if (autoRetry && transientErrorCode) { if (logLevel >= LogLevel.BASIC) { log(`[${requestId}] ${transientErrorCode}, wait ${retryCount} seconds then retry...`); } yield (0, wait_1.wait)(retryCount * 1000); return request(Object.assign(Object.assign({}, options), { retryCount: retryCount + 1 })); } throw error; } catch (error) { if (error instanceof makeCancelablePromise_1.CanceledPromiseError || (isErrorWithCode(error) && error.code.includes('ETIMEDOUT'))) { if (autoRetry && retryCount < MAX_RETRY_COUNT_ON_TIMEOUT_ERROR) { if (logLevel >= LogLevel.BASIC) { log(`[${requestId}] Timeout error, wait ${retryCount} seconds then retry...`); } yield (0, wait_1.wait)(retryCount * 1000); return request(Object.assign(Object.assign({}, options), { retryCount: retryCount + 1 })); } throw new errors_1.TimeoutError(buildTimeoutErrorInitObject(options.method, url, headers, options.body, preCallStack)); } throw error; } }); } exports.request = request; //# sourceMappingURL=request.js.map