Skip to content

Commit 4d81af2

Browse files
fix: seal browser pool acquire SDK results
Stainless-Generated-From: f64758e79131d7749f23be27de0655ee48e5f835
1 parent e6e016a commit 4d81af2

6 files changed

Lines changed: 88 additions & 12 deletions

File tree

‎src/kernel/_response.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import httpx
2525
import pydantic
2626

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

187187
response = self.http_response
188+
if (
189+
response.status_code == 204
190+
and to is None
191+
and isinstance(origin, type)
192+
and issubclass(origin, NoContentAsNone)
193+
):
194+
return cast(R, None)
188195
if cast_to == str:
189196
return cast(R, response.text)
190197

‎src/kernel/_types.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@
4444
Query = Mapping[str, object]
4545
Body = object
4646
AnyMapping = Mapping[str, object]
47+
48+
49+
class NoContentAsNone:
50+
pass
51+
52+
4753
ModelT = TypeVar("ModelT", bound=pydantic.BaseModel)
4854
_T = TypeVar("_T")
4955

‎src/kernel/resources/browser_pools.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ def acquire(
542542
extra_query: Query | None = None,
543543
extra_body: Body | None = None,
544544
timeout: float | httpx.Timeout | None | NotGiven = not_given,
545-
) -> BrowserPoolAcquireResponse:
545+
) -> Optional[BrowserPoolAcquireResponse]:
546546
"""Long-polling endpoint to acquire a browser from the pool.
547547
548548
Returns immediately
@@ -1199,7 +1199,7 @@ async def acquire(
11991199
extra_query: Query | None = None,
12001200
extra_body: Body | None = None,
12011201
timeout: float | httpx.Timeout | None | NotGiven = not_given,
1202-
) -> BrowserPoolAcquireResponse:
1202+
) -> Optional[BrowserPoolAcquireResponse]:
12031203
"""Long-polling endpoint to acquire a browser from the pool.
12041204
12051205
Returns immediately

‎src/kernel/resources/browsers/playwright/playwright.py‎

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,6 @@ def execute(
114114
executors are not removed automatically while the browser runs; when it shuts
115115
down, they are removed and their tabs closed.
116116
117-
A named call to a browser whose image predates executors fails with 400 instead
118-
of running on the active tab; calls without 'executor' keep working on every
119-
image.
120-
121117
Args:
122118
code: TypeScript/JavaScript code to execute. The code has access to 'page', 'context',
123119
and 'browser' variables. It runs within a function, so you can use a return
@@ -244,10 +240,6 @@ async def execute(
244240
executors are not removed automatically while the browser runs; when it shuts
245241
down, they are removed and their tabs closed.
246242
247-
A named call to a browser whose image predates executors fails with 400 instead
248-
of running on the active tab; calls without 'executor' keep working on every
249-
image.
250-
251243
Args:
252244
code: TypeScript/JavaScript code to execute. The code has access to 'page', 'context',
253245
and 'browser' variables. It runs within a function, so you can use a return

‎src/kernel/types/browser_pool_acquire_response.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing_extensions import Literal
66

77
from .tags import Tags
8+
from .._types import NoContentAsNone
89
from .profile import Profile
910
from .._models import BaseModel
1011
from .browser_proxy import BrowserProxy
@@ -20,7 +21,7 @@
2021
__all__ = ["BrowserPoolAcquireResponse"]
2122

2223

23-
class BrowserPoolAcquireResponse(BaseModel):
24+
class BrowserPoolAcquireResponse(NoContentAsNone, BaseModel):
2425
cdp_ws_url: str
2526
"""Websocket URL for Chrome DevTools Protocol connections to the browser session"""
2627

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import httpx
2+
import pytest
3+
4+
from kernel import Kernel, AsyncKernel, NotFoundError, APIResponseValidationError
5+
6+
7+
def test_acquire_returns_browser() -> None:
8+
browser = {
9+
"cdp_ws_url": "wss://example.test/cdp",
10+
"created_at": "2026-10-09T00:00:00Z",
11+
"headless": True,
12+
"memory": "1GiB",
13+
"region": "us-east",
14+
"session_id": "session-1",
15+
"stealth": False,
16+
"webdriver_ws_url": "wss://example.test/webdriver",
17+
}
18+
with Kernel(
19+
api_key="test",
20+
http_client=httpx.Client(transport=httpx.MockTransport(lambda _request: httpx.Response(200, json=browser))),
21+
) as client:
22+
result = client.browser_pools.acquire("pool")
23+
assert result is not None
24+
assert result.session_id == "session-1"
25+
26+
27+
def test_acquire_returns_none_on_poll_timeout() -> None:
28+
calls = 0
29+
30+
def handle(_request: httpx.Request) -> httpx.Response:
31+
nonlocal calls
32+
calls += 1
33+
return httpx.Response(204)
34+
35+
with Kernel(api_key="test", http_client=httpx.Client(transport=httpx.MockTransport(handle))) as client:
36+
assert client.browser_pools.acquire("pool") is None
37+
response = client.browser_pools.with_raw_response.acquire("pool")
38+
assert response.status_code == 204
39+
assert response.parse() is None
40+
assert response.parse(to=str) == ""
41+
assert response.parse(to=bytes) == b""
42+
assert response.parse(to=httpx.Response) is response.http_response
43+
assert calls == 2
44+
45+
46+
async def test_async_acquire_returns_none_on_poll_timeout() -> None:
47+
async with AsyncKernel(
48+
api_key="test",
49+
http_client=httpx.AsyncClient(transport=httpx.MockTransport(lambda _request: httpx.Response(204))),
50+
) as client:
51+
assert await client.browser_pools.acquire("pool") is None
52+
53+
54+
def test_acquire_still_raises_not_found() -> None:
55+
with Kernel(
56+
api_key="test",
57+
http_client=httpx.Client(transport=httpx.MockTransport(lambda _request: httpx.Response(404, json={}))),
58+
) as client:
59+
with pytest.raises(NotFoundError):
60+
client.browser_pools.acquire("missing")
61+
62+
63+
def test_unexpected_204_still_fails_strict_validation() -> None:
64+
with Kernel(
65+
api_key="test",
66+
_strict_response_validation=True,
67+
http_client=httpx.Client(transport=httpx.MockTransport(lambda _request: httpx.Response(204))),
68+
) as client:
69+
with pytest.raises(APIResponseValidationError):
70+
client.browser_pools.retrieve("pool")

0 commit comments

Comments
 (0)