fix: close upstream HTTP responses to prevent FD exhaustion#31
Open
lxinfei5 wants to merge 1 commit into
Open
Conversation
All five urllib.request.urlopen() call sites in the proxy now close the HTTPResponse object after use (via try/finally), preventing file-descriptor leaks that crash the proxy under UltraCode concurrency, surfacing as 'ConnectionRefused' in Claude Code mid-session. Affected call sites: - _relay_response: wrap body in try/finally resp.close() - _mk_events (openai_compat): close resp in finally - _classifier_complete: close resp after read (both openai_compat and Anthropic passthrough backends) - _handle_models: close resp after /v1/models fetch 27/27 offline self-tests 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.
Summary
Fixes #30.
Every
urllib.request.urlopen()response in the proxy is now closed when the proxy is finished with it, preventing the file-descriptor leaks that crash the proxy under sustained UltraCode concurrency (surfacing asConnectionRefusedin Claude Code mid-session). See the issue for the full diagnosis.What changed
All five
urlopen()call sites are wrapped so the returnedHTTPResponseis closed on exit viatry / finally resp.close():_classifier_complete— openai_compat branch: read the body insidetry,resp.close()infinally._classifier_complete— Anthropic passthrough branch: same pattern.Handler._handle_models—/v1/modelsupstream fetch: nest the existing body in atryandresp.close()infinally(still inside the outertry/except, so the existing fallback-to-stock+custom behaviour is unchanged)._relay_response(resp, ...):_relay_responsenow owns the lifecycle of therespit is given and closes it in its ownfinally. The success path, the streaming loop, and the error/exception paths all reach thefinally.Handler._mk_events(openai_compat): hoistresp = None, wrap the open + theyield from _oai_response_to_events(resp)in atrywhosefinallyclosesrespif it was opened (error branchesreturnbefore opening, sorespstaysNone).Notes:
_mk_eventsgenerator keepsrespopen for the lifetime of_oai_response_to_events(resp)(the SSE stream is read lazily); it is only closed after the stream is fully consumed. This is the correct point to close — closing earlier would truncate streaming.finallyswallows nothing that would have propagated; errors still surface as before, just without leaving an FD behind.Verification
python3 test_proxy.py).lsof -p <pid> | wc -l) is now stable across turns instead of climbing monotonically.Checklist
pip install.proxy.py.urlopencall sites covered (grep confirms 5resp.close()sites, plus_relay_responseclosing therespit receives from the passthrough path).Opening this from my fork since it's where I run this proxy in production; happy to rebase onto anything the maintainer prefers.