Skip to content

Commit 8a642c4

Browse files
committed
feat: Add per-request headers, timeout, and auth overrides to endpoint functions
Adds optional `headers`, `timeout`, and `auth` keyword arguments to all generated endpoint functions (sync_detailed, asyncio_detailed, sync, asyncio). These are forwarded directly to the underlying httpx `.request()` call, allowing per-request overrides without creating a new client instance or disrupting the shared connection pool. `with_headers()` and `with_timeout()` mutate the original client's underlying httpx client as a side effect and cause the returned client to open a new connection pool on first use, making them unsafe for runtime per-request use (especially in concurrent async code). This change provides the correct alternative. Known limitation: client-level headers cannot be removed for a single request; they can only be overridden with a different value.
1 parent 5fcaf72 commit 8a642c4

File tree

61 files changed

+1642
-40
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1642
-40
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
from unittest.mock import MagicMock
2+
3+
import httpx
4+
import pytest
5+
6+
from end_to_end_tests.functional_tests.helpers import (
7+
with_generated_client_fixture,
8+
with_generated_code_import,
9+
)
10+
11+
12+
SIMPLE_SPEC = """
13+
paths:
14+
"/items":
15+
get:
16+
operationId: getItems
17+
responses:
18+
"200":
19+
description: Success
20+
"""
21+
22+
SPEC_WITH_HEADER_PARAM = """
23+
paths:
24+
"/items":
25+
get:
26+
operationId: getItems
27+
parameters:
28+
- name: X-Request-Id
29+
in: header
30+
required: true
31+
schema:
32+
type: string
33+
responses:
34+
"200":
35+
description: Success
36+
"""
37+
38+
39+
def _make_mock_client(Client):
40+
mock_httpx_client = MagicMock(spec=httpx.Client)
41+
mock_response = MagicMock(spec=httpx.Response)
42+
mock_response.status_code = 200
43+
mock_response.content = b""
44+
mock_response.headers = {}
45+
mock_httpx_client.request.return_value = mock_response
46+
client = Client(base_url="https://api.example.com")
47+
client.set_httpx_client(mock_httpx_client)
48+
return client, mock_httpx_client
49+
50+
51+
@with_generated_client_fixture(SIMPLE_SPEC)
52+
@with_generated_code_import(".api.default.get_items.sync_detailed")
53+
@with_generated_code_import(".client.Client")
54+
class TestPerRequestHeaders:
55+
def test_extra_headers_are_forwarded(self, sync_detailed, Client):
56+
client, mock_httpx = _make_mock_client(Client)
57+
sync_detailed(client=client, headers={"X-Trace-Id": "abc123"})
58+
call_kwargs = mock_httpx.request.call_args[1]
59+
assert call_kwargs["headers"]["X-Trace-Id"] == "abc123"
60+
61+
def test_omitting_headers_does_not_inject_headers_key(self, sync_detailed, Client):
62+
client, mock_httpx = _make_mock_client(Client)
63+
sync_detailed(client=client)
64+
call_kwargs = mock_httpx.request.call_args[1]
65+
# No spec-defined headers and no override — headers key should be absent
66+
assert "headers" not in call_kwargs
67+
68+
def test_multiple_extra_headers_are_all_forwarded(self, sync_detailed, Client):
69+
client, mock_httpx = _make_mock_client(Client)
70+
sync_detailed(client=client, headers={"X-A": "1", "X-B": "2"})
71+
call_kwargs = mock_httpx.request.call_args[1]
72+
assert call_kwargs["headers"]["X-A"] == "1"
73+
assert call_kwargs["headers"]["X-B"] == "2"
74+
75+
76+
@with_generated_client_fixture(SPEC_WITH_HEADER_PARAM)
77+
@with_generated_code_import(".api.default.get_items.sync_detailed")
78+
@with_generated_code_import(".client.Client")
79+
class TestPerRequestHeadersWithSpecHeaders:
80+
def test_caller_header_overrides_spec_header(self, sync_detailed, Client):
81+
client, mock_httpx = _make_mock_client(Client)
82+
sync_detailed(client=client, x_request_id="spec-value", headers={"X-Request-Id": "override"})
83+
call_kwargs = mock_httpx.request.call_args[1]
84+
assert call_kwargs["headers"]["X-Request-Id"] == "override"
85+
86+
def test_caller_headers_merged_with_spec_headers(self, sync_detailed, Client):
87+
client, mock_httpx = _make_mock_client(Client)
88+
sync_detailed(client=client, x_request_id="spec-value", headers={"X-Extra": "extra"})
89+
call_kwargs = mock_httpx.request.call_args[1]
90+
assert call_kwargs["headers"]["X-Request-Id"] == "spec-value"
91+
assert call_kwargs["headers"]["X-Extra"] == "extra"
92+
93+
94+
@with_generated_client_fixture(SIMPLE_SPEC)
95+
@with_generated_code_import(".api.default.get_items.sync_detailed")
96+
@with_generated_code_import(".client.Client")
97+
class TestPerRequestTimeout:
98+
def test_timeout_override_is_forwarded(self, sync_detailed, Client):
99+
client, mock_httpx = _make_mock_client(Client)
100+
sync_detailed(client=client, timeout=httpx.Timeout(120.0))
101+
call_kwargs = mock_httpx.request.call_args[1]
102+
assert call_kwargs["timeout"] == httpx.Timeout(120.0)
103+
104+
def test_timeout_none_disables_timeout(self, sync_detailed, Client):
105+
client, mock_httpx = _make_mock_client(Client)
106+
sync_detailed(client=client, timeout=None)
107+
call_kwargs = mock_httpx.request.call_args[1]
108+
assert call_kwargs["timeout"] is None
109+
110+
def test_omitting_timeout_does_not_inject_timeout_key(self, sync_detailed, Client):
111+
client, mock_httpx = _make_mock_client(Client)
112+
sync_detailed(client=client)
113+
call_kwargs = mock_httpx.request.call_args[1]
114+
assert "timeout" not in call_kwargs
115+
116+
117+
@with_generated_client_fixture(SIMPLE_SPEC)
118+
@with_generated_code_import(".api.default.get_items.sync_detailed")
119+
@with_generated_code_import(".client.Client")
120+
class TestPerRequestAuth:
121+
def test_auth_override_is_forwarded(self, sync_detailed, Client):
122+
client, mock_httpx = _make_mock_client(Client)
123+
auth = httpx.BasicAuth("user", "pass")
124+
sync_detailed(client=client, auth=auth)
125+
call_kwargs = mock_httpx.request.call_args[1]
126+
assert call_kwargs["auth"] is auth
127+
128+
def test_auth_none_disables_auth(self, sync_detailed, Client):
129+
client, mock_httpx = _make_mock_client(Client)
130+
sync_detailed(client=client, auth=None)
131+
call_kwargs = mock_httpx.request.call_args[1]
132+
assert call_kwargs["auth"] is None
133+
134+
def test_omitting_auth_does_not_inject_auth_key(self, sync_detailed, Client):
135+
client, mock_httpx = _make_mock_client(Client)
136+
sync_detailed(client=client)
137+
call_kwargs = mock_httpx.request.call_args[1]
138+
assert "auth" not in call_kwargs

end_to_end_tests/golden-record/my_test_api_client/api/bodies/json_like.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ def sync_detailed(
5252
*,
5353
client: AuthenticatedClient | Client,
5454
body: JsonLikeBody | Unset = UNSET,
55+
headers: dict[str, str] | None = None,
56+
timeout: httpx.Timeout | None | Unset = UNSET,
57+
auth: httpx.Auth | None | Unset = UNSET,
5558
) -> Response[Any]:
5659
"""A content type that works like json but isn't application/json
5760
@@ -69,6 +72,12 @@ def sync_detailed(
6972
kwargs = _get_kwargs(
7073
body=body,
7174
)
75+
if headers is not None:
76+
kwargs["headers"] = {**kwargs.get("headers", {}), **headers}
77+
if not isinstance(timeout, Unset):
78+
kwargs["timeout"] = timeout
79+
if not isinstance(auth, Unset):
80+
kwargs["auth"] = auth
7281

7382
response = client.get_httpx_client().request(
7483
**kwargs,
@@ -81,6 +90,9 @@ async def asyncio_detailed(
8190
*,
8291
client: AuthenticatedClient | Client,
8392
body: JsonLikeBody | Unset = UNSET,
93+
headers: dict[str, str] | None = None,
94+
timeout: httpx.Timeout | None | Unset = UNSET,
95+
auth: httpx.Auth | None | Unset = UNSET,
8496
) -> Response[Any]:
8597
"""A content type that works like json but isn't application/json
8698
@@ -98,6 +110,12 @@ async def asyncio_detailed(
98110
kwargs = _get_kwargs(
99111
body=body,
100112
)
113+
if headers is not None:
114+
kwargs["headers"] = {**kwargs.get("headers", {}), **headers}
115+
if not isinstance(timeout, Unset):
116+
kwargs["timeout"] = timeout
117+
if not isinstance(auth, Unset):
118+
kwargs["auth"] = auth
101119

102120
response = await client.get_async_httpx_client().request(**kwargs)
103121

end_to_end_tests/golden-record/my_test_api_client/api/bodies/optional_body.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ def sync_detailed(
5252
*,
5353
client: AuthenticatedClient | Client,
5454
body: OptionalBodyBody | Unset = UNSET,
55+
headers: dict[str, str] | None = None,
56+
timeout: httpx.Timeout | None | Unset = UNSET,
57+
auth: httpx.Auth | None | Unset = UNSET,
5558
) -> Response[Any]:
5659
"""Test optional request body
5760
@@ -69,6 +72,12 @@ def sync_detailed(
6972
kwargs = _get_kwargs(
7073
body=body,
7174
)
75+
if headers is not None:
76+
kwargs["headers"] = {**kwargs.get("headers", {}), **headers}
77+
if not isinstance(timeout, Unset):
78+
kwargs["timeout"] = timeout
79+
if not isinstance(auth, Unset):
80+
kwargs["auth"] = auth
7281

7382
response = client.get_httpx_client().request(
7483
**kwargs,
@@ -81,6 +90,9 @@ async def asyncio_detailed(
8190
*,
8291
client: AuthenticatedClient | Client,
8392
body: OptionalBodyBody | Unset = UNSET,
93+
headers: dict[str, str] | None = None,
94+
timeout: httpx.Timeout | None | Unset = UNSET,
95+
auth: httpx.Auth | None | Unset = UNSET,
8496
) -> Response[Any]:
8597
"""Test optional request body
8698
@@ -98,6 +110,12 @@ async def asyncio_detailed(
98110
kwargs = _get_kwargs(
99111
body=body,
100112
)
113+
if headers is not None:
114+
kwargs["headers"] = {**kwargs.get("headers", {}), **headers}
115+
if not isinstance(timeout, Unset):
116+
kwargs["timeout"] = timeout
117+
if not isinstance(auth, Unset):
118+
kwargs["auth"] = auth
101119

102120
response = await client.get_async_httpx_client().request(**kwargs)
103121

end_to_end_tests/golden-record/my_test_api_client/api/bodies/post_bodies_multiple.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ def sync_detailed(
7070
*,
7171
client: AuthenticatedClient | Client,
7272
body: PostBodiesMultipleJsonBody | File | PostBodiesMultipleDataBody | PostBodiesMultipleFilesBody | Unset = UNSET,
73+
headers: dict[str, str] | None = None,
74+
timeout: httpx.Timeout | None | Unset = UNSET,
75+
auth: httpx.Auth | None | Unset = UNSET,
7376
) -> Response[Any]:
7477
"""Test multiple bodies
7578
@@ -90,6 +93,12 @@ def sync_detailed(
9093
kwargs = _get_kwargs(
9194
body=body,
9295
)
96+
if headers is not None:
97+
kwargs["headers"] = {**kwargs.get("headers", {}), **headers}
98+
if not isinstance(timeout, Unset):
99+
kwargs["timeout"] = timeout
100+
if not isinstance(auth, Unset):
101+
kwargs["auth"] = auth
93102

94103
response = client.get_httpx_client().request(
95104
**kwargs,
@@ -102,6 +111,9 @@ async def asyncio_detailed(
102111
*,
103112
client: AuthenticatedClient | Client,
104113
body: PostBodiesMultipleJsonBody | File | PostBodiesMultipleDataBody | PostBodiesMultipleFilesBody | Unset = UNSET,
114+
headers: dict[str, str] | None = None,
115+
timeout: httpx.Timeout | None | Unset = UNSET,
116+
auth: httpx.Auth | None | Unset = UNSET,
105117
) -> Response[Any]:
106118
"""Test multiple bodies
107119
@@ -122,6 +134,12 @@ async def asyncio_detailed(
122134
kwargs = _get_kwargs(
123135
body=body,
124136
)
137+
if headers is not None:
138+
kwargs["headers"] = {**kwargs.get("headers", {}), **headers}
139+
if not isinstance(timeout, Unset):
140+
kwargs["timeout"] = timeout
141+
if not isinstance(auth, Unset):
142+
kwargs["auth"] = auth
125143

126144
response = await client.get_async_httpx_client().request(**kwargs)
127145

end_to_end_tests/golden-record/my_test_api_client/api/bodies/refs.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ def sync_detailed(
5252
*,
5353
client: AuthenticatedClient | Client,
5454
body: AModel | Unset = UNSET,
55+
headers: dict[str, str] | None = None,
56+
timeout: httpx.Timeout | None | Unset = UNSET,
57+
auth: httpx.Auth | None | Unset = UNSET,
5558
) -> Response[Any]:
5659
"""Test request body defined via ref
5760
@@ -69,6 +72,12 @@ def sync_detailed(
6972
kwargs = _get_kwargs(
7073
body=body,
7174
)
75+
if headers is not None:
76+
kwargs["headers"] = {**kwargs.get("headers", {}), **headers}
77+
if not isinstance(timeout, Unset):
78+
kwargs["timeout"] = timeout
79+
if not isinstance(auth, Unset):
80+
kwargs["auth"] = auth
7281

7382
response = client.get_httpx_client().request(
7483
**kwargs,
@@ -81,6 +90,9 @@ async def asyncio_detailed(
8190
*,
8291
client: AuthenticatedClient | Client,
8392
body: AModel | Unset = UNSET,
93+
headers: dict[str, str] | None = None,
94+
timeout: httpx.Timeout | None | Unset = UNSET,
95+
auth: httpx.Auth | None | Unset = UNSET,
8496
) -> Response[Any]:
8597
"""Test request body defined via ref
8698
@@ -98,6 +110,12 @@ async def asyncio_detailed(
98110
kwargs = _get_kwargs(
99111
body=body,
100112
)
113+
if headers is not None:
114+
kwargs["headers"] = {**kwargs.get("headers", {}), **headers}
115+
if not isinstance(timeout, Unset):
116+
kwargs["timeout"] = timeout
117+
if not isinstance(auth, Unset):
118+
kwargs["auth"] = auth
101119

102120
response = await client.get_async_httpx_client().request(**kwargs)
103121

0 commit comments

Comments
 (0)