Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/kernel/_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import httpx
import pydantic

from ._types import NoneType
from ._types import NoneType, NoContentAsNone
from ._utils import is_given, extract_type_arg, is_annotated_type, is_type_alias_type, extract_type_var_from_base
from ._models import BaseModel, is_basemodel
from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER
Expand Down Expand Up @@ -185,6 +185,13 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T:
return cast(R, None)

response = self.http_response
if (
response.status_code == 204
and to is None
and isinstance(origin, type)
and issubclass(origin, NoContentAsNone)
):
return cast(R, None)
if cast_to == str:
return cast(R, response.text)

Expand Down
6 changes: 6 additions & 0 deletions src/kernel/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@
Query = Mapping[str, object]
Body = object
AnyMapping = Mapping[str, object]


class NoContentAsNone:
pass


ModelT = TypeVar("ModelT", bound=pydantic.BaseModel)
_T = TypeVar("_T")

Expand Down
4 changes: 2 additions & 2 deletions src/kernel/resources/browser_pools.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ def acquire(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> BrowserPoolAcquireResponse:
) -> Optional[BrowserPoolAcquireResponse]:
"""Long-polling endpoint to acquire a browser from the pool.

Returns immediately
Expand Down Expand Up @@ -1199,7 +1199,7 @@ async def acquire(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> BrowserPoolAcquireResponse:
) -> Optional[BrowserPoolAcquireResponse]:
"""Long-polling endpoint to acquire a browser from the pool.

Returns immediately
Expand Down
3 changes: 2 additions & 1 deletion src/kernel/types/browser_pool_acquire_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing_extensions import Literal

from .tags import Tags
from .._types import NoContentAsNone
from .profile import Profile
from .._models import BaseModel
from .browser_proxy import BrowserProxy
Expand All @@ -20,7 +21,7 @@
__all__ = ["BrowserPoolAcquireResponse"]


class BrowserPoolAcquireResponse(BaseModel):
class BrowserPoolAcquireResponse(NoContentAsNone, BaseModel):
cdp_ws_url: str
"""Websocket URL for Chrome DevTools Protocol connections to the browser session"""

Expand Down
70 changes: 70 additions & 0 deletions tests/test_browser_pool_acquire_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import httpx
import pytest

from kernel import Kernel, AsyncKernel, NotFoundError, APIResponseValidationError


def test_acquire_returns_browser() -> None:
browser = {
"cdp_ws_url": "wss://example.test/cdp",
"created_at": "2026-10-09T00:00:00Z",
"headless": True,
"memory": "1GiB",
"region": "us-east",
"session_id": "session-1",
"stealth": False,
"webdriver_ws_url": "wss://example.test/webdriver",
}
with Kernel(
api_key="test",
http_client=httpx.Client(transport=httpx.MockTransport(lambda _request: httpx.Response(200, json=browser))),
) as client:
result = client.browser_pools.acquire("pool")
assert result is not None
assert result.session_id == "session-1"


def test_acquire_returns_none_on_poll_timeout() -> None:
calls = 0

def handle(_request: httpx.Request) -> httpx.Response:
nonlocal calls
calls += 1
return httpx.Response(204)

with Kernel(api_key="test", http_client=httpx.Client(transport=httpx.MockTransport(handle))) as client:
assert client.browser_pools.acquire("pool") is None
response = client.browser_pools.with_raw_response.acquire("pool")
assert response.status_code == 204
assert response.parse() is None
assert response.parse(to=str) == ""
assert response.parse(to=bytes) == b""
assert response.parse(to=httpx.Response) is response.http_response
assert calls == 2


async def test_async_acquire_returns_none_on_poll_timeout() -> None:
async with AsyncKernel(
api_key="test",
http_client=httpx.AsyncClient(transport=httpx.MockTransport(lambda _request: httpx.Response(204))),
) as client:
assert await client.browser_pools.acquire("pool") is None


def test_acquire_still_raises_not_found() -> None:
with Kernel(
api_key="test",
http_client=httpx.Client(transport=httpx.MockTransport(lambda _request: httpx.Response(404, json={}))),
) as client:
with pytest.raises(NotFoundError):
client.browser_pools.acquire("missing")


def test_unexpected_204_still_fails_strict_validation() -> None:
with Kernel(
api_key="test",
_strict_response_validation=True,
http_client=httpx.Client(transport=httpx.MockTransport(lambda _request: httpx.Response(204))),
) as client:
with pytest.raises(APIResponseValidationError):
client.browser_pools.retrieve("pool")
Loading