From 7917db648763539d1c106219ac1a441479323089 Mon Sep 17 00:00:00 2001 From: Richard Lawrence Date: Mon, 29 Jun 2026 14:42:25 -0500 Subject: [PATCH] feat: add configurable HTTP request timeout (#2) - Add support for `OVERLEAF_TIMEOUT` environment variable. - Add global `--timeout ` CLI option. - Add `config set-timeout` and `config get-timeout` commands to persist timeout setting. - Update `OverleafClient` to support a configurable global timeout and per-request overrides for download operations. - Default timeout remains 10,000ms. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- src/cli.ts | 29 ++++++++++++++++++++++++++++- src/client.ts | 25 ++++++++++++++++--------- src/config.ts | 12 +++++++++++- 3 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 82102e0..2e1794d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -36,7 +36,9 @@ import { getBaseUrl, setBaseUrl, getSessionCookieName, - setSessionCookieName + setSessionCookieName, + getTimeout, + setTimeout } from './config.js'; const program = new Command(); @@ -47,6 +49,7 @@ program .version(VERSION) .option('--base-url ', 'Overleaf instance base URL (overrides OVERLEAF_BASE_URL and config)') .option('--cookie-name ', 'Session cookie name (default: overleaf_session2, use overleaf.sid for older instances)') + .option('--timeout ', 'HTTP request timeout in milliseconds', parseInt) .option('--verbose', 'Print every HTTP request, status, and error response body to stderr'); /** @@ -65,6 +68,10 @@ async function getClient(cookieOpt?: string, baseUrlOpt?: string): Promise') + .description('Set the default HTTP request timeout in milliseconds') + .action((ms: string) => { + const timeout = parseInt(ms, 10); + if (isNaN(timeout)) { + console.error(chalk.red('Invalid timeout value. Must be a number.')); + process.exit(1); + } + setTimeout(timeout); + console.log(chalk.green(`Default timeout set to: ${timeout}ms`)); + }); + +configCmd + .command('get-timeout') + .description('Get the current default HTTP request timeout') + .action(() => { + console.log(`${getTimeout()}ms`); + }); + program .command('ignored [dir]') .description('Show ignore patterns currently in effect for a project directory') diff --git a/src/client.ts b/src/client.ts index a3e017f..bd97d32 100644 --- a/src/client.ts +++ b/src/client.ts @@ -133,6 +133,7 @@ export class OverleafClient { private csrf: string; private baseUrl: string; private verbose: boolean = false; + private timeoutMs: number = 10000; // Cache per-project folder trees so repeated uploads in sync/upload calls // don't re-fetch the tree via Socket.IO on every file. private folderTreeCache: Map> = new Map(); @@ -148,6 +149,11 @@ export class OverleafClient { this.verbose = v; } + /** Set the global HTTP request timeout in milliseconds. */ + setGlobalTimeout(ms: number): void { + this.timeoutMs = ms; + } + /** * Resolve (and cache) the folder tree for a project. Falls back to a * minimal tree containing only the root folder when the Socket.IO probe @@ -312,7 +318,7 @@ export class OverleafClient { expect?: 'text' | 'json' | 'buffer'; } = {}): Promise<{ status: number; ok: boolean; headers: Record; body: string | Buffer | any }> { const method = options.method || 'GET'; - const timeoutMs = options.timeoutMs ?? 10000; + const timeoutMs = options.timeoutMs ?? this.timeoutMs; const maxRedirects = options.maxRedirects ?? 5; const expect = options.expect ?? 'text'; @@ -644,10 +650,11 @@ export class OverleafClient { * characters in response headers (e.g. Content-Disposition with Unicode * project names). See: https://github.com/aloth/olcli/issues/2 */ - private async downloadBuffer(url: string): Promise { + private async downloadBuffer(url: string, timeoutMs?: number): Promise { const response = await this.httpRequest(url, { headers: this.getHeaders(), - expect: 'buffer' + expect: 'buffer', + timeoutMs }); if (!response.ok) { @@ -665,8 +672,8 @@ export class OverleafClient { * Uses downloadBuffer to avoid ByteString errors from non-Latin1 * Content-Disposition headers. See: https://github.com/aloth/olcli/issues/2 */ - async downloadProject(projectId: string): Promise { - return this.downloadBuffer(this.downloadUrl(projectId)); + async downloadProject(projectId: string, timeoutMs?: number): Promise { + return this.downloadBuffer(this.downloadUrl(projectId), timeoutMs); } /** @@ -718,9 +725,9 @@ export class OverleafClient { /** * Download compiled PDF */ - async downloadPdf(projectId: string): Promise { + async downloadPdf(projectId: string, timeoutMs?: number): Promise { const { pdfUrl } = await this.compileProject(projectId); - return this.downloadBuffer(pdfUrl); + return this.downloadBuffer(pdfUrl, timeoutMs); } /** @@ -2085,7 +2092,7 @@ export class OverleafClient { /** * Download a compile output file (logs, bbl, aux, etc.) */ - async downloadOutputFile(url: string): Promise { - return this.downloadBuffer(url); + async downloadOutputFile(url: string, timeoutMs?: number): Promise { + return this.downloadBuffer(url, timeoutMs); } } diff --git a/src/config.ts b/src/config.ts index 07bb053..5656cde 100644 --- a/src/config.ts +++ b/src/config.ts @@ -13,6 +13,7 @@ interface OlcliConfig { lastProject?: string; baseUrl?: string; sessionCookieName?: string; + timeout?: number; } const config = new Conf({ @@ -22,7 +23,8 @@ const config = new Conf({ csrf: { type: 'string' }, lastProject: { type: 'string' }, baseUrl: { type: 'string' }, - sessionCookieName: { type: 'string' } + sessionCookieName: { type: 'string' }, + timeout: { type: 'number' } } }); @@ -34,6 +36,14 @@ export function setBaseUrl(url: string): void { config.set('baseUrl', url); } +export function getTimeout(): number { + return Number.parseInt(process.env.OVERLEAF_TIMEOUT || '') || config.get('timeout') || 10000; +} + +export function setTimeout(ms: number): void { + config.set('timeout', ms); +} + export function getSessionCookieName(): string { return process.env.OVERLEAF_COOKIE_NAME || config.get('sessionCookieName') || 'overleaf_session2'; }