deepl-node
Version:
deepl-node is the official DeepL Node.js client library
224 lines (223 loc) • 8.99 kB
JavaScript
;
// 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;