Skip to content

Commit

Permalink
fix: increase req timeout for export and import (#59)
Browse files Browse the repository at this point in the history
users were having troubles applying large endpoints as the backend took
more than the default timeout to respond
  • Loading branch information
cyyynthia authored Oct 30, 2023
1 parent e764004 commit ca30ac3
Show file tree
Hide file tree
Showing 8 changed files with 55 additions and 38 deletions.
32 changes: 21 additions & 11 deletions src/client/errors.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,50 @@
import type { Request, Response } from 'undici';
import type { RequestData } from './internal/requester.js';
import type { Dispatcher } from 'undici';
import { STATUS_CODES } from 'http';

export class HttpError extends Error {
constructor(
public request: Request,
public response: Response,
public request: RequestData,
public response: Dispatcher.ResponseData,
options?: ErrorOptions
) {
super(response.statusText, options);
super(
`HTTP Error ${response.statusCode} ${STATUS_CODES[response.statusCode]}`,
options
);
}

getErrorText() {
// Unauthorized
if (this.response.status === 400) {
if (this.response.statusCode === 400) {
return 'Invalid request data.';
}

// Unauthorized
if (this.response.status === 401) {
if (this.response.statusCode === 401) {
return 'Missing or invalid authentication token.';
}

// Forbidden
if (this.response.status === 403) {
if (this.response.statusCode === 403) {
return 'You are not allowed to perform this operation.';
}

// Rate limited
if (this.response.statusCode === 429) {
return "You've been rate limited. Please try again later.";
}

// Service Unavailable
if (this.response.status === 503) {
if (this.response.statusCode === 503) {
return 'API is temporarily unavailable. Please try again later.';
}

// Server error
if (this.response.status >= 500) {
return `API reported a server error (${this.response.status}). Please try again later.`;
if (this.response.statusCode >= 500) {
return `API reported a server error (${this.response.statusCode}). Please try again later.`;
}

return `Unknown error (HTTP ${this.response.status})`;
return `Unknown error (HTTP ${this.response.statusCode})`;
}
}
4 changes: 4 additions & 0 deletions src/client/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export default class ExportClient {
method: 'POST',
path: `${this.requester.projectUrl}/export`,
body: { ...req, zip: true },
headersTimeout: 300,
bodyTimeout: 300,
});
}

Expand All @@ -30,6 +32,8 @@ export default class ExportClient {
method: 'POST',
path: `${this.requester.projectUrl}/export`,
body: { ...req, zip: false },
headersTimeout: 300,
bodyTimeout: 300,
});
}
}
4 changes: 3 additions & 1 deletion src/client/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export default class ImportClient {
method: 'PUT',
path: `${this.requester.projectUrl}/import/apply`,
query: { forceMode: req?.forceMode },
headersTimeout: 300,
bodyTimeout: 300,
});
}

Expand All @@ -71,7 +73,7 @@ export default class ImportClient {
try {
await this.deleteImport();
} catch (e) {
if (e instanceof HttpError && e.response.status === 404) return;
if (e instanceof HttpError && e.response.statusCode === 404) return;
throw e;
}
}
Expand Down
39 changes: 20 additions & 19 deletions src/client/internal/requester.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Request, Response } from 'undici';
import type { Dispatcher } from 'undici';
import type { Blob } from 'buffer';
import type { components } from './schema.generated.js';

import { fetch } from 'undici';
import { STATUS_CODES } from 'http';
import { request } from 'undici';
import FormData from 'form-data';

import { HttpError } from '../errors.js';
Expand All @@ -23,6 +24,8 @@ export type RequestData = {
body?: any;
query?: Record<string, Primitive | Primitive[] | undefined>;
headers?: Record<string, string>;
headersTimeout?: number;
bodyTimeout?: number;
};

export type PaginatedView<T> = {
Expand Down Expand Up @@ -56,7 +59,7 @@ export default class Requester {
* @param req Request data
* @returns The response
*/
async request(req: RequestData): Promise<Response> {
async request(req: RequestData): Promise<Dispatcher.ResponseData> {
const url = new URL(req.path, this.params.apiUrl);

if (req.query) {
Expand Down Expand Up @@ -95,19 +98,23 @@ export default class Requester {
}
}

const request = new Request(url, {
debug(`[HTTP] Requesting: ${req.method} ${url}`);

const response = await request(url, {
method: req.method,
headers: headers,
body: body,
headersTimeout: req.headersTimeout,
bodyTimeout: req.bodyTimeout,
});

debug(`[HTTP] Requesting: ${request.method} ${request.url}`);
const response = await fetch(request);

debug(
`[HTTP] ${request.method} ${request.url} -> ${response.status} ${response.statusText}`
`[HTTP] ${req.method} ${url} -> ${response.statusCode} ${
STATUS_CODES[response.statusCode]
}`
);
if (!response.ok) throw new HttpError(request, response);

if (response.statusCode >= 400) throw new HttpError(req, response);
return response;
}

Expand All @@ -118,7 +125,7 @@ export default class Requester {
* @returns The response data
*/
async requestJson<T = unknown>(req: RequestData): Promise<T> {
return <Promise<T>>this.request(req).then((r) => r.json());
return <Promise<T>>this.request(req).then((r) => r.body.json());
}

/**
Expand All @@ -128,20 +135,14 @@ export default class Requester {
* @returns The response blob
*/
async requestBlob(req: RequestData): Promise<Blob> {
return this.request(req).then((r) => r.blob());
return this.request(req).then((r) => r.body.blob());
}

/**
* Performs an HTTP request to the API and forces the consumption of the body, according to recommendations by Undici
*
* @see https://github.com/nodejs/undici#garbage-collection
* @param req Request data
* Performs an HTTP request to the API.
*/
async requestVoid(req: RequestData): Promise<void> {
const res = await this.request(req);
if (res.body) {
for await (const _chunk of res.body);
}
await this.request(req);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/commands/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ async function loginHandler(this: Command, key: string) {
try {
keyInfo = await RestClient.getApiKeyInformation(opts.apiUrl, key);
} catch (e) {
if (e instanceof HttpError && e.response.status === 401) {
if (e instanceof HttpError && e.response.statusCode === 401) {
error("Couldn't log in: the API key you provided is invalid.");
process.exit(1);
}
Expand Down
4 changes: 2 additions & 2 deletions src/commands/pull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ async function pullHandler(this: Command, path: string) {
await loading('Extracting strings...', unzipBuffer(zipBlob, path));
success('Done!');
} catch (e) {
if (e instanceof HttpError && e.response.status === 400) {
const res: any = await e.response.json();
if (e instanceof HttpError && e.response.statusCode === 400) {
const res: any = await e.response.body.json();
error(
`Please check if your parameters, including namespaces, are configured correctly. Tolgee responded with: ${res.code}`
);
Expand Down
2 changes: 1 addition & 1 deletion src/commands/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ async function applyImport(client: Client) {
try {
await loading('Applying changes...', client.import.applyImport());
} catch (e) {
if (e instanceof HttpError && e.response.status === 400) {
if (e instanceof HttpError && e.response.statusCode === 400) {
error(
"Some of the imported languages weren't recognized. Please create a language with corresponding tag in the Tolgee Platform."
);
Expand Down
6 changes: 3 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,11 @@ async function loadConfig() {

async function handleHttpError(e: HttpError) {
error('An error occurred while requesting the API.');
error(`${e.request.method} ${e.request.url}`);
error(`${e.request.method} ${e.request.path}`);
error(e.getErrorText());

// Remove token from store if necessary
if (e.response.status === 401) {
if (e.response.statusCode === 401) {
const removeFn = program.getOptionValue('_removeApiKeyFromStore');
if (removeFn) {
info('Removing the API key from the authentication store.');
Expand All @@ -177,7 +177,7 @@ async function handleHttpError(e: HttpError) {
// catastrophic failure) which means the output is completely unpredictable. While some errors are
// formatted by the Tolgee server, reality is there's a huge chance the 5xx error hasn't been raised
// by Tolgee's error handler.
const res = await e.response.text();
const res = await e.response.body.text();
debug(`Server response:\n\n---\n${res}\n---`);
}
}
Expand Down

0 comments on commit ca30ac3

Please sign in to comment.