Skip to content
Open
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
16 changes: 8 additions & 8 deletions modules/openapi-generator/src/main/resources/python/api.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ https://github.com/OpenAPITools/openapi-generator/blob/c84b949df1a9ec04ba75989cb

_path_params: Dict[str, str] = {}
_query_params: List[Tuple[str, str]] = []
_header_params: Dict[str, Optional[str]] = _headers or {}
_header_params: Dict[str, Any] = self.api_client._merge_headers(_headers)
_form_params: List[Tuple[str, str]] = []
_files: Dict[
str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]
Expand Down Expand Up @@ -387,12 +387,12 @@ https://github.com/OpenAPITools/openapi-generator/blob/c84b949df1a9ec04ba75989cb
{{#headerParams}}
if {{paramName}} is not None:
{{#vendorExtensions.x-python-json-header}}
_header_params['{{baseName}}'] = json.dumps(
self.api_client._set_header(_header_params, '{{baseName}}', json.dumps(
self.api_client.sanitize_for_serialization({{paramName}})
)
))
{{/vendorExtensions.x-python-json-header}}
{{^vendorExtensions.x-python-json-header}}
_header_params['{{baseName}}'] = {{paramName}}
self.api_client._set_header(_header_params, '{{baseName}}', {{paramName}})
{{/vendorExtensions.x-python-json-header}}
{{/headerParams}}
# process the form parameters
Expand Down Expand Up @@ -431,13 +431,13 @@ https://github.com/OpenAPITools/openapi-generator/blob/c84b949df1a9ec04ba75989cb
{{/isQueryParam}}
{{#isHeaderParam}}
# Set client side default value of Header Param "{{baseName}}".
_header_params['{{baseName}}'] = {{#_enum}}'{{{.}}}'{{/_enum}}
self.api_client._set_header(_header_params, '{{baseName}}', {{#_enum}}'{{{.}}}'{{/_enum}})
{{/isHeaderParam}}
{{/constantParams}}

{{#hasProduces}}
# set the HTTP header `Accept`
if 'Accept' not in _header_params:
if not any(key.lower() == 'accept' for key in _header_params):
_header_params['Accept'] = self.api_client.select_header_accept(
[{{#produces}}
'{{{mediaType}}}'{{^-last}}, {{/-last}}{{/produces}}
Expand All @@ -448,7 +448,7 @@ https://github.com/OpenAPITools/openapi-generator/blob/c84b949df1a9ec04ba75989cb
{{#hasConsumes}}
# set the HTTP header `Content-Type`
if _content_type:
_header_params['Content-Type'] = _content_type
self.api_client._set_header(_header_params, 'Content-Type', _content_type)
else:
_default_content_type = (
self.api_client.select_header_content_type(
Expand All @@ -458,7 +458,7 @@ https://github.com/OpenAPITools/openapi-generator/blob/c84b949df1a9ec04ba75989cb
)
)
if _default_content_type is not None:
_header_params['Content-Type'] = _default_content_type
self.api_client._set_header(_header_params, 'Content-Type', _default_content_type)
{{/hasConsumes}}

# authentication setting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ from threading import Lock
{{/compatibleWithPythonLegacyApi}}

from urllib.parse import quote
from typing import Tuple, Optional, List, Dict, Union{{#compatibleWithPythonLegacy}}, Any{{/compatibleWithPythonLegacy}}
from typing import Any, Tuple, Optional, List, Dict, Union
from pydantic import SecretStr

from {{packageName}}.configuration import Configuration
Expand Down Expand Up @@ -231,16 +231,36 @@ https://github.com/OpenAPITools/openapi-generator/blob/c84b949df1a9ec04ba75989cb
@property
def user_agent(self):
"""User agent for this API client"""
return self.default_headers['User-Agent']
for name, value in reversed(self.default_headers.items()):
if name.lower() == 'user-agent':
return value
raise KeyError('User-Agent')

@user_agent.setter
def user_agent(self, value):
self.default_headers['User-Agent'] = value
self._set_header(self.default_headers, 'User-Agent', value)

def set_default_header(self, header_name, header_value):
self.default_headers[header_name] = header_value
self._set_header(self.default_headers, header_name, header_value)


@staticmethod
def _set_header(headers: Dict[str, Any], name: str, value: Any) -> None:
"""Replace a header case-insensitively, retaining the winning spelling."""
for key in list(headers):
if key.lower() == name.lower():
del headers[key]
headers[name] = value

@classmethod
def _merge_headers(cls, *sources: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""Copy headers; later sources and later entries in each dict win."""
headers: Dict[str, Any] = {}
for source in sources:
for name, value in (source or {}).items():
cls._set_header(headers, name, value)
return headers

_default = None

{{#useIndependentImplicitClients}}
Expand Down Expand Up @@ -321,10 +341,9 @@ https://github.com/OpenAPITools/openapi-generator/blob/c84b949df1a9ec04ba75989cb
config = self.configuration

# header parameters
header_params = header_params or {}
header_params.update(self.default_headers)
header_params = self._merge_headers(header_params, self.default_headers)
if self.cookie:
header_params['Cookie'] = self.cookie
self._set_header(header_params, 'Cookie', self.cookie)
if header_params:
header_params = self.sanitize_for_serialization(header_params)
header_params = dict(
Expand Down Expand Up @@ -856,7 +875,11 @@ https://github.com/OpenAPITools/openapi-generator/blob/c84b949df1a9ec04ba75989cb
:param auth_setting: auth settings for the endpoint
"""
if auth_setting['in'] == 'cookie':
if not 'Cookie' in headers:
for key in list(headers):
if key.lower() == 'cookie':
self._set_header(headers, 'Cookie', headers[key])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Copying the raw existing cookie value into a new Cookie entry before the headers['Cookie'] += "; " append makes a previously tolerated input crash. If a caller passes a non-string cookie through _headers or default_headers under a lowercase cookie key (e.g. an int), the old code started from headers['Cookie'] = "" and could never raise on +=; the new code now does int += str → TypeError: unsupported operand type(s) for +=. Header values elsewhere are stringified by parameters_to_tuples, so this branch is the lone path that assumes a string value.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At modules/openapi-generator/src/main/resources/python/api_client.mustache, line 891:

<comment>Copying the raw existing cookie value into a new `Cookie` entry before the `headers['Cookie'] += "; "` append makes a previously tolerated input crash. If a caller passes a non-string cookie through `_headers` or `default_headers` under a lowercase `cookie` key (e.g. an int), the old code started from `headers['Cookie'] = ""` and could never raise on `+=`; the new code now does `int += str` → `TypeError: unsupported operand type(s) for +=`. Header values elsewhere are stringified by `parameters_to_tuples`, so this branch is the lone path that assumes a string value.</comment>

<file context>
@@ -856,6 +885,12 @@ https://github.com/OpenAPITools/openapi-generator/blob/c84b949df1a9ec04ba75989cb
+{{#httpxLibrary}}
+            for key in list(headers):
+                if key.lower() == 'cookie':
+                    self._set_header(headers, 'Cookie', headers[key])
+                    break
+{{/httpxLibrary}}
</file context>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTTPX already rejected numeric Cookie values before this change, even alongside Cookie authentication. This PR preserves value validation. Could you provide a previously successful HTTPX request that now fails?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No previously successful request is established here: the reply says HTTPX already rejected numeric Cookie values, including alongside Cookie authentication, and that this PR preserves that validation. On the supplied evidence, the parent comment’s claim of a new regression does not apply.

break
if not headers.get('Cookie'):
headers['Cookie'] = ""
else:
headers['Cookie'] += "; "
Expand All @@ -865,15 +888,16 @@ https://github.com/OpenAPITools/openapi-generator/blob/c84b949df1a9ec04ba75989cb
headers['Cookie'] += f"{auth_setting['key']}={cookie_value}"
elif auth_setting['in'] == 'header':
if auth_setting['type'] != 'http-signature':
headers[auth_setting['key']] = auth_setting['value']
self._set_header(headers, auth_setting['key'], auth_setting['value'])
{{#hasHttpSignatureMethods}}
else:
# The HTTP signature scheme requires multiple HTTP headers
# that are calculated dynamically.
signing_info = self.configuration.signing_info
auth_headers = signing_info.get_http_signature_headers(
resource_path, method, headers, body, queries)
headers.update(auth_headers)
for name, value in auth_headers.items():
self._set_header(headers, name, value)
{{/hasHttpSignatureMethods}}
elif auth_setting['in'] == 'query':
queries.append((auth_setting['key'], auth_setting['value']))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ from typing import Any, Dict, Optional, Union

import aiohttp
import aiohttp_retry
from multidict import CIMultiDict

from {{packageName}}.exceptions import ApiException, ApiValueError

Expand Down Expand Up @@ -191,7 +192,7 @@ class RESTClientObject:
)

post_params = post_params or {}
headers = headers or {}
headers = CIMultiDict(headers or {})
# url already contains the URL query string
timeout = _request_timeout or 5 * 60

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,33 @@ Class | Method | HTTP request | Description

{{#apiInfo}}{{#apis}}{{#-last}}{{infoEmail}}
{{/-last}}{{/apis}}{{/apiInfo}}

## Request headers

Header names are compared case-insensitively in all Python client libraries.
Request headers are copied; supplying `_headers` does not change the caller's dict
or the client's shared defaults. The public `_headers` argument retains its dict
validation and existing value serialization. Pydantic can coerce mappings such as HTTPX `Headers` to a dict,
combining repeated fields into a single value; `_headers` does not provide a
repeated-field or list-of-pairs API.

Precedence, from lowest to highest:

1. `_headers` (within one dict, the last inserted case variant wins).
2. Explicit OpenAPI header parameters and generated `Content-Type` (or `_content_type`).
Generated `Accept` is added only when neither `_headers` nor explicit header
parameters contain an `Accept` case variant.
3. `ApiClient.default_headers` (including headers set with `set_default_header`).
4. The client's `cookie` setting, for `Cookie`.
5. Authentication headers. A non-empty `_request_auth` replaces configured
authentication for that call; `{}` retains configured authentication. Cookie
authentication appends to the selected `Cookie` value.

This retains the existing precedence for identically spelled names and extends it
to case variants. To override a generated content type, use `_content_type`;
client defaults still take precedence over it. The winning header's spelling and
value are retained for replacements. Cookie authentication uses the canonical
`Cookie` spelling and appends its value. Values containing legitimate
comma-separated lists are not split or deduplicated. This does not remove
duplicate values inside a header or change the handling of repeated fields
supplied directly to the transport.
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ class RESTClientObject:
)

post_params = post_params or {}
headers = headers or {}
headers = {{httpxModule}}.Headers(headers or {})
timeout = _request_timeout or 5 * 60

if 'Content-Type' not in headers:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ class RESTClientObject:
)

post_params = post_params or {}
headers = headers or {}
headers = urllib3.HTTPHeaderDict(headers or {})

timeout = None
if _request_timeout:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,9 @@ public void testJsonContentHeaderUsesJsonSerialization() throws IOException {
TestUtils.assertFileContains(
api,
"import json",
"_header_params['X-Json-Arg'] = json.dumps(",
"self.api_client._set_header(_header_params, 'X-Json-Arg', json.dumps(",
"self.api_client.sanitize_for_serialization(x_json_arg)",
"_header_params['X-Plain-Arg'] = x_plain_arg");
"self.api_client._set_header(_header_params, 'X-Plain-Arg', x_plain_arg)");

TestUtils.assertFileNotContains(
api,
Expand Down Expand Up @@ -806,7 +806,7 @@ public void testHandleConstantParams() throws IOException {
File apiFile = files
.get(Paths.get(output.getAbsolutePath(), "openapi_client", "api", "hello_example_api.py").toString());
assertNotNull(apiFile);
assertFileContains(apiFile.toPath(), "_header_params['X-CUSTOM_CONSTANT_HEADER'] = 'CONSTANT_VALUE'");
assertFileContains(apiFile.toPath(), "self.api_client._set_header(_header_params, 'X-CUSTOM_CONSTANT_HEADER', 'CONSTANT_VALUE')");
assertFileContains(apiFile.toPath(), "_query_params.append(('CONSTANT_QUERY_STRING_KEY', 'CONSTANT_QUERY_STRING_VALUE'))");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,33 @@ Authentication schemes defined for the API:

team@openapitools.org


## Request headers

Header names are compared case-insensitively in all Python client libraries.
Request headers are copied; supplying `_headers` does not change the caller's dict
or the client's shared defaults. The public `_headers` argument retains its dict
validation and existing value serialization. Pydantic can coerce mappings such as HTTPX `Headers` to a dict,
combining repeated fields into a single value; `_headers` does not provide a
repeated-field or list-of-pairs API.

Precedence, from lowest to highest:

1. `_headers` (within one dict, the last inserted case variant wins).
2. Explicit OpenAPI header parameters and generated `Content-Type` (or `_content_type`).
Generated `Accept` is added only when neither `_headers` nor explicit header
parameters contain an `Accept` case variant.
3. `ApiClient.default_headers` (including headers set with `set_default_header`).
4. The client's `cookie` setting, for `Cookie`.
5. Authentication headers. A non-empty `_request_auth` replaces configured
authentication for that call; `{}` retains configured authentication. Cookie
authentication appends to the selected `Cookie` value.

This retains the existing precedence for identically spelled names and extends it
to case variants. To override a generated content type, use `_content_type`;
client defaults still take precedence over it. The winning header's spelling and
value are retained for replacements. Cookie authentication uses the canonical
`Cookie` spelling and appends its value. Values containing legitimate
comma-separated lists are not split or deduplicated. This does not remove
duplicate values inside a header or change the handling of repeated fields
supplied directly to the transport.
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ def _test_auth_http_basic_serialize(

_path_params: Dict[str, str] = {}
_query_params: List[Tuple[str, str]] = []
_header_params: Dict[str, Optional[str]] = _headers or {}
_header_params: Dict[str, Any] = self.api_client._merge_headers(_headers)
_form_params: List[Tuple[str, str]] = []
_files: Dict[
str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]
Expand All @@ -251,7 +251,7 @@ def _test_auth_http_basic_serialize(


# set the HTTP header `Accept`
if 'Accept' not in _header_params:
if not any(key.lower() == 'accept' for key in _header_params):
_header_params['Accept'] = self.api_client.select_header_accept(
[
'text/plain'
Expand Down Expand Up @@ -482,7 +482,7 @@ def _test_auth_http_bearer_serialize(

_path_params: Dict[str, str] = {}
_query_params: List[Tuple[str, str]] = []
_header_params: Dict[str, Optional[str]] = _headers or {}
_header_params: Dict[str, Any] = self.api_client._merge_headers(_headers)
_form_params: List[Tuple[str, str]] = []
_files: Dict[
str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]
Expand All @@ -497,7 +497,7 @@ def _test_auth_http_bearer_serialize(


# set the HTTP header `Accept`
if 'Accept' not in _header_params:
if not any(key.lower() == 'accept' for key in _header_params):
_header_params['Accept'] = self.api_client.select_header_accept(
[
'text/plain'
Expand Down
Loading
Loading