UNPKG

deepl-node

Version:

deepl-node is the official DeepL Node.js client library

224 lines (223 loc) 8.99 kB
"use strict"; // Copyright 2022 DeepL SE (https://www.deepl.com) // Use of this source code is governed by an MIT // license that can be found in the LICENSE file. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.HttpClient = void 0; const errors_1 = require("./errors"); const utils_1 = require("./utils"); const axios_1 = __importDefault(require("axios")); const form_data_1 = __importDefault(require("form-data")); const https = __importStar(require("https")); const http = __importStar(require("http")); const axiosInstance = axios_1.default.create({ httpAgent: new http.Agent({ keepAlive: true }), httpsAgent: new https.Agent({ keepAlive: true }), }); /** * Internal class implementing exponential-backoff timer. * @private */ class BackoffTimer { constructor() { this.backoffInitial = 1.0; this.backoffMax = 120.0; this.backoffJitter = 0.23; this.backoffMultiplier = 1.6; this.numRetries = 0; this.backoff = this.backoffInitial * 1000.0; this.deadline = Date.now() + this.backoff; } getNumRetries() { return this.numRetries; } getTimeout() { return this.getTimeUntilDeadline(); } getTimeUntilDeadline() { return Math.max(this.deadline - Date.now(), 0.0); } async sleepUntilDeadline() { await (0, utils_1.timeout)(this.getTimeUntilDeadline()); // Apply multiplier to current backoff time this.backoff = Math.min(this.backoff * this.backoffMultiplier, this.backoffMax * 1000.0); // Get deadline by applying jitter as a proportion of backoff: // if jitter is 0.1, then multiply backoff by random value in [0.9, 1.1] this.deadline = Date.now() + this.backoff * (1 + this.backoffJitter * (2 * Math.random() - 1)); this.numRetries++; } } /** * Internal class implementing HTTP requests. * @private */ class HttpClient { constructor(serverUrl, headers, maxRetries, minTimeout, proxy) { this.serverUrl = serverUrl; this.headers = headers; this.maxRetries = maxRetries; this.minTimeout = minTimeout; this.proxy = proxy; } prepareRequest(method, url, timeoutMs, responseAsStream, options) { const headers = Object.assign({}, this.headers, options.headers); const axiosRequestConfig = { url, method, baseURL: this.serverUrl, headers, responseType: responseAsStream ? 'stream' : 'text', timeout: timeoutMs, validateStatus: null, // do not throw errors for any status codes }; if (options.fileBuffer) { const form = new form_data_1.default(); form.append('file', options.fileBuffer, { filename: options.filename }); if (options.data) { for (const [key, value] of options.data.entries()) { form.append(key, value); } } axiosRequestConfig.data = form; if (axiosRequestConfig.headers === undefined) { axiosRequestConfig.headers = {}; } Object.assign(axiosRequestConfig.headers, form.getHeaders()); } else if (options.data) { if (method === 'GET') { axiosRequestConfig.params = options.data; } else { axiosRequestConfig.data = options.data; } } axiosRequestConfig.proxy = this.proxy; return axiosRequestConfig; } /** * Makes API request retrying if necessary, and returns (as Promise) response. * @param method HTTP method, for example 'GET' * @param url Path to endpoint, excluding base server URL. * @param options Additional options controlling request. * @param responseAsStream Set to true if the return type is IncomingMessage. * @return Fulfills with status code and response (as text or stream). */ async sendRequestWithBackoff(method, url, options, responseAsStream = false) { options = options === undefined ? {} : options; (0, utils_1.logInfo)(`Request to DeepL API ${method} ${url}`); (0, utils_1.logDebug)(`Request details: ${options.data}`); const backoff = new BackoffTimer(); let response, error; while (backoff.getNumRetries() <= this.maxRetries) { const timeoutMs = Math.max(this.minTimeout, backoff.getTimeout()); const axiosRequestConfig = this.prepareRequest(method, url, timeoutMs, responseAsStream, options); try { response = await HttpClient.sendAxiosRequest(axiosRequestConfig); error = undefined; } catch (e) { response = undefined; error = e; } if (!HttpClient.shouldRetry(response === null || response === void 0 ? void 0 : response.statusCode, error) || backoff.getNumRetries() + 1 >= this.maxRetries) { break; } if (error !== undefined) { (0, utils_1.logDebug)(`Encountered a retryable-error: ${error.message}`); } (0, utils_1.logInfo)(`Starting retry ${backoff.getNumRetries() + 1} for request ${method}` + ` ${url} after sleeping for ${backoff.getTimeUntilDeadline()} seconds.`); await backoff.sleepUntilDeadline(); } if (response !== undefined) { const { statusCode, content } = response; (0, utils_1.logInfo)(`DeepL API response ${method} ${url} ${statusCode}`); if (!responseAsStream) { (0, utils_1.logDebug)('Response details:', { content: content }); } return response; } else { throw error; } } /** * Performs given HTTP request and returns status code and response content (text or stream). * @param axiosRequestConfig * @private */ static async sendAxiosRequest(axiosRequestConfig) { try { const response = await axiosInstance.request(axiosRequestConfig); if (response.headers !== undefined) { (0, utils_1.logDebug)('Trace details:', { xTraceId: response.headers['x-trace-id'], }); } if (axiosRequestConfig.responseType === 'text') { // Workaround for axios-bug: https://github.com/axios/axios/issues/907 if (typeof response.data === 'object') { response.data = JSON.stringify(response.data); } } return { statusCode: response.status, content: response.data, }; } catch (axios_error_raw) { const axiosError = axios_error_raw; const message = axiosError.message || ''; const error = new errors_1.ConnectionError(`Connection failure: ${message}`); error.error = axiosError; if (axiosError.code === 'ETIMEDOUT') { error.shouldRetry = true; } else if (axiosError.code === 'ECONNABORTED') { error.shouldRetry = true; } else { (0, utils_1.logDebug)('Unrecognized axios error', axiosError); error.shouldRetry = false; } throw error; } } static shouldRetry(statusCode, error) { if (statusCode === undefined) { return error.shouldRetry; } // Retry on Too-Many-Requests error and internal errors return statusCode === 429 || statusCode >= 500; } } exports.HttpClient = HttpClient;