From 94716b82bb4cb3ffb8a0e48406590cde62a80bd2 Mon Sep 17 00:00:00 2001 From: Daniel Vystrcil Date: Sat, 1 Aug 2026 16:19:12 -0700 Subject: [PATCH] fix: make uvicorn's keep-alive timeout configurable, default 75s uvicorn's default timeout_keep_alive (5s) can be shorter than a client's HTTP connection-pool reuse window, so a pooled-but-idle connection gets written to right after the server has already closed it -- surfacing as ConnectionResetError on the client even though the underlying command completed successfully. Adds OPEN_TERMINAL_UVICORN_TIMEOUT_KEEP_ALIVE (or uvicorn_timeout_keep_alive in config.toml), following the same pattern as the existing OPEN_TERMINAL_UVICORN_LOOP option. Defaults to 75s, matching gunicorn/nginx's common default. --- CHANGELOG.md | 6 ++++++ open_terminal/cli.py | 10 ++++++++-- open_terminal/env.py | 13 +++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17246ea..7fc8ade 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [Unreleased] + +### Fixed + +- 🔌 **`Connection reset by peer` on client tool calls** — uvicorn's default keep-alive timeout (5s) can be shorter than a client's HTTP connection-pool reuse window, so a pooled-but-idle connection gets written to right after the server has already closed it, surfacing as `ConnectionResetError` even though the underlying command completed successfully. `timeout_keep_alive` is now configurable (`OPEN_TERMINAL_UVICORN_TIMEOUT_KEEP_ALIVE` or `uvicorn_timeout_keep_alive` in config.toml) and defaults to 75s. + ## [0.11.34] - 2026-04-08 ### Added diff --git a/open_terminal/cli.py b/open_terminal/cli.py index 15adb15..e8be4ca 100644 --- a/open_terminal/cli.py +++ b/open_terminal/cli.py @@ -128,8 +128,14 @@ def run( click.echo(click.style(" └─────────────────────────────────────────────────────────────┘", fg="yellow")) click.echo() - from open_terminal.env import UVICORN_LOOP - uvicorn.run("open_terminal.main:app", host=host, port=port, loop=UVICORN_LOOP) + from open_terminal.env import UVICORN_LOOP, UVICORN_TIMEOUT_KEEP_ALIVE + uvicorn.run( + "open_terminal.main:app", + host=host, + port=port, + loop=UVICORN_LOOP, + timeout_keep_alive=UVICORN_TIMEOUT_KEEP_ALIVE, + ) @main.command() diff --git a/open_terminal/env.py b/open_terminal/env.py index b7bd1d1..610510c 100644 --- a/open_terminal/env.py +++ b/open_terminal/env.py @@ -156,6 +156,19 @@ def _resolve_file_env(var: str, default: str = "") -> str: config.get("uvicorn_loop", "auto"), ) +# uvicorn's own default (5s) can be shorter than a client's HTTP +# connection-pool reuse window, so a pooled-but-idle connection can be +# written to right after the server has already closed it -- surfacing +# as a ConnectionResetError on the client even though the underlying +# command completed successfully. 75s matches gunicorn/nginx's common +# default and comfortably exceeds most client pool idle timeouts. +UVICORN_TIMEOUT_KEEP_ALIVE = float( + os.environ.get( + "OPEN_TERMINAL_UVICORN_TIMEOUT_KEEP_ALIVE", + config.get("uvicorn_timeout_keep_alive", 75), + ) +) + OPEN_TERMINAL_INFO = os.environ.get( "OPEN_TERMINAL_INFO", config.get("info", ""),