fix(export): close leaked fds and time out hung API requests - #292
Merged
petertzy merged 3 commits intoSep 29, 2026
Merged
Conversation
backend/routers/export.py ignored the descriptor returned by tempfile.mkstemp in _make_output_path (one leaked fd per export without an explicit output path, plus a blocked re-open on Windows) and in download_html (never adopted or closed it, and left the temp file on disk forever). Close the descriptor in _make_output_path, write through os.fdopen in download_html, and remove the temporary file once the response has been streamed via a BackgroundTask. frontend/src/lib/api.ts used plain fetch for every API call, so a hung backend left the UI awaiting indefinitely (AI.getSettings even carried a bespoke Promise.race timeout). Route apiFetch/apiFetchBlob through the existing fetchWithTimeout (30s default, configurable per call) and drop the hand-rolled race. Adds backend regression tests for the fd leak and temp-file cleanup.
Addresses review feedback on the request-timeout half of the fd-leak PR: - Endpoint-specific timeouts: settings/metadata keep the short default (30s); long-running AI/conversion endpoints (chat, work, translate, translateSentences, translateSentenceBatch, model listing, PDF/HTML conversion, DOCX/PDF export) get a generous 5-minute budget. - fetchWithTimeout now composes a caller-supplied AbortSignal with the timeout signal instead of replacing it (init.signal ?? controller.signal silently dropped the timeout whenever a caller supplied a signal). - timeoutMs <= 0 disables the automatic abort (explicit no-deadline opt-out). - New regression tests covering hung-abort, long-budget success, caller abort winning, timeout firing alongside a caller signal, and the compose helper. Signed-off-by: Harsh Thakkar <harsh-thakkar7@users.noreply.github.com>
Owner
|
Thanks for addressing the earlier timeout review and cleaning up the export file descriptors. I found one remaining timeout gap: the deadline was cleared as soon as fetch() received response headers, so a stalled JSON or Blob response body could still hang indefinitely. I fixed this in f76c86f; the deadline now stays active until the API response body has been read. I also made the timeout tests independent of a local HTTP port. After the fix, all 16 frontend tests, ESLint, TypeScript, Ruff checks on the changed backend files, and the diff check pass. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Follow-up to the closed fd/temp-file + request-timeout PR, incorporating review feedback from
karaaslanzon the request-timeout half. The fd/temp-file cleanup (backend/routers/export.py,tests/test_export_fd_leak.py) is unchanged — the reviewer confirmed that half is well-scoped.Request timeouts, revised per review
The previous version applied one 30 s default to every
apiFetch/apiFetchBlobcall, which would have broken legitimately long-running requests:AI.chat,AI.work,AI.translate,AI.translateSentences,AI.translateSentenceBatchand provider model listing can exceed 30 s (slow/local models, long documents).Changes in
frontend/src/lib/api.ts+ newfrontend/src/lib/http-timeout.mjs:DEFAULT_REQUEST_TIMEOUT_MS, 30 s). Long-running AI/conversion/export endpoints passLONG_REQUEST_TIMEOUT_MS(5 minutes) explicitly.fetchWithTimeoutpreviously didinit.signal ?? controller.signal, which silently dropped the timeout whenever a caller supplied a signal (e.g.translateSentenceBatch(..., signal)). It now composes caller + timeout signals viacomposeAbortSignals(usesAbortSignal.any, with a manual-wiring fallback for older webviews), so both caller cancellation and the timeout stay effective.timeoutMs <= 0disables the automatic abort.http-timeout.mjs) so the Node-based regression suite can exercise the exact code the app runs.Regression tests (
frontend/tests/api-fetch-timeout.test.mjs, 8 tests)AbortSignalaborts the request even when a generous timeout is set.timeoutMs = 0disables the deadline.composeAbortSignalsunit cases (single/absent signals, any-input abort, already-aborted input).Why this matters
Without the per-endpoint budgets, the 30 s default would turn previously-working slow AI/conversion requests into client-side aborts. Without composing the caller signal, cancel-and-timeout would not coexist.
How Has This Been Tested?
node --test tests/api-fetch-timeout.test.mjs→ 8/8 passing.frontend/src/lib/api.tstype-checks (transpile clean); the newhttp-timeout.mjsis dependency-free plain ESM.tests/test_export_fd_leak.py).Related
Supersedes the earlier fd-leak + timeout PR after review. Standalone PR — no matching open issue.