fix: set Basic Auth on HTTPRequest, not as fetch kwargs; close client cleanly
tornado rejects fetch(request, **kwargs) when the first arg is already an HTTPRequest and the kwargs overlap request construction (auth_username / auth_password). Move auth onto the HTTPRequest. Add OpenCodeClient.close() to release its AsyncHTTPClient; module-level atexit closes the tornado singleton so 'jupyter lab' can exit cleanly after SIGINT instead of hanging on 'received signal 2, stopping'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
ff932d9095
commit
c86de1d31b
@@ -1,5 +1,6 @@
|
||||
"""Async HTTP client for the local OpenCode server."""
|
||||
|
||||
import atexit
|
||||
import json
|
||||
from typing import Any, Optional
|
||||
|
||||
@@ -13,6 +14,21 @@ class OpenCodeError(Exception):
|
||||
"""Raised when an OpenCode API request fails."""
|
||||
|
||||
|
||||
# Ensure the tornado AsyncHTTPClient singleton is closed at interpreter
|
||||
# shutdown so the IOLoop can finish and the process can exit cleanly
|
||||
# (otherwise Ctrl+C on `jupyter lab` hangs after "received signal 2,
|
||||
# stopping" because the singleton keeps persistent connections alive).
|
||||
def _close_opencode_http() -> None:
|
||||
try:
|
||||
AsyncHTTPClient().close()
|
||||
except Exception:
|
||||
# Never block interpreter shutdown on close errors.
|
||||
pass
|
||||
|
||||
|
||||
atexit.register(_close_opencode_http)
|
||||
|
||||
|
||||
class OpenCodeClient:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -20,7 +36,24 @@ class OpenCodeClient:
|
||||
http_client: Optional[AsyncHTTPClient] = None,
|
||||
) -> None:
|
||||
self._config = config
|
||||
self._http_client = http_client or AsyncHTTPClient()
|
||||
# Use a dedicated client (not the singleton) so close() can
|
||||
# deterministically release its connections without affecting
|
||||
# other components. The module-level atexit still closes the
|
||||
# singleton as a safety net.
|
||||
self._http_client = http_client or AsyncHTTPClient(force_instance=True)
|
||||
self._owns_http_client = http_client is None
|
||||
|
||||
def close(self) -> None:
|
||||
"""Release the underlying HTTP client's connections. Idempotent."""
|
||||
if getattr(self, "_http_client", None) is None:
|
||||
return
|
||||
try:
|
||||
if self._owns_http_client:
|
||||
self._http_client.close()
|
||||
except Exception:
|
||||
pass
|
||||
self._http_client = None # type: ignore[assignment]
|
||||
self._owns_http_client = False
|
||||
|
||||
async def health(self) -> dict[str, Any]:
|
||||
return await self._request("GET", "/global/health")
|
||||
@@ -82,15 +115,17 @@ class OpenCodeClient:
|
||||
}
|
||||
if body is not None:
|
||||
request_kwargs["body"] = json.dumps(body).encode("utf-8")
|
||||
# HTTP Basic Auth must be set on the HTTPRequest itself; passing
|
||||
# auth_* as **kwargs to fetch() when the first arg is already an
|
||||
# HTTPRequest is rejected by tornado ("kwargs can't be used if
|
||||
# request is an HTTPRequest object").
|
||||
if self._config.auth is not None:
|
||||
request_kwargs["auth_username"] = self._config.auth[0]
|
||||
request_kwargs["auth_password"] = self._config.auth[1]
|
||||
|
||||
request = HTTPRequest(url, **request_kwargs)
|
||||
|
||||
fetch_kwargs: dict[str, str] = {}
|
||||
if self._config.auth is not None:
|
||||
fetch_kwargs["auth_username"] = self._config.auth[0]
|
||||
fetch_kwargs["auth_password"] = self._config.auth[1]
|
||||
|
||||
response = await self._http_client.fetch(request, **fetch_kwargs)
|
||||
response = await self._http_client.fetch(request)
|
||||
|
||||
if 200 <= response.code < 300:
|
||||
if not response.body:
|
||||
|
||||
Reference in New Issue
Block a user