From 9014338e0ba2dc8b48eb62a3be154bc7817b9b5f Mon Sep 17 00:00:00 2001 From: Marcel Konrad Date: Wed, 10 Jun 2026 16:34:52 +0200 Subject: [PATCH 1/5] Account for content-type parameters on request preparation --- .../src/main/resources/python/rest.mustache | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/python/rest.mustache b/modules/openapi-generator/src/main/resources/python/rest.mustache index 5d852b28d92b..deb0af4128f4 100644 --- a/modules/openapi-generator/src/main/resources/python/rest.mustache +++ b/modules/openapi-generator/src/main/resources/python/rest.mustache @@ -150,6 +150,10 @@ class RESTClientObject: else: self.pool_manager = urllib3.PoolManager(**pool_args) + def _contenttype_matches(contenttype, maintype, subtype): + pattern = '{type}/(?:[^+;]+\\+)?{subtype}(?:;.*)?'.format(type = re.escape(maintype), subtype = re.escape(subtype)) + return re.fullmatch(pattern, contenttype, re.IGNORECASE) is not None + def request( self, method, @@ -212,14 +216,18 @@ class RESTClientObject: content_type = headers.get('Content-Type') is_json = ( not content_type - or re.search('json', content_type, re.IGNORECASE) + or _contenttype_matches(content_type, 'application', 'json') ) # JSON is valid YAML 1.2, so structured YAML bodies can use # the existing JSON serializer: # https://yaml.org/spec/1.2.2/#13-relation-to-json is_structured_yaml = ( content_type - and re.search('yaml', content_type, re.IGNORECASE) + and ( + _contenttype_matches(content_type, 'application', 'yaml') + or _contenttype_matches(content_type, 'text', 'yaml') + or _contenttype_matches(content_type, 'text', 'x-yaml') + ) and not isinstance(body, (str, bytes)) ) if is_json or is_structured_yaml: @@ -234,7 +242,7 @@ class RESTClientObject: headers=headers, preload_content=False ) - elif content_type == 'application/x-www-form-urlencoded': + elif _contenttype_matches(content_type, 'application', 'x-www-form-urlencoded'): r = self.pool_manager.request( method, url, @@ -244,7 +252,7 @@ class RESTClientObject: headers=headers, preload_content=False ) - elif content_type == 'multipart/form-data': + elif _contenttype_matches(content_type, 'multipart', 'form-data'): # must del headers['Content-Type'], or the correct # Content-Type which generated by urllib3 will be # overwritten. @@ -272,7 +280,7 @@ class RESTClientObject: headers=headers, preload_content=False ) - elif headers['Content-Type'].startswith('text/') and isinstance(body, bool): + elif content_type.startswith('text/') and isinstance(body, bool): request_body = "true" if body else "false" r = self.pool_manager.request( method, From 0cbacbb68ab6af6df52d73bae20ae906af124c96 Mon Sep 17 00:00:00 2001 From: Marcel Konrad Date: Thu, 11 Jun 2026 10:58:47 +0200 Subject: [PATCH 2/5] Move helper method to outer scope --- .../src/main/resources/python/rest.mustache | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/python/rest.mustache b/modules/openapi-generator/src/main/resources/python/rest.mustache index deb0af4128f4..6b6fb3def1b6 100644 --- a/modules/openapi-generator/src/main/resources/python/rest.mustache +++ b/modules/openapi-generator/src/main/resources/python/rest.mustache @@ -27,6 +27,9 @@ def is_socks_proxy_url(url): else: return split_section[0].lower() in SUPPORTED_SOCKS_PROXIES +def contenttype_matches(contenttype, maintype, subtype): + pattern = '{type}/(?:[^+;]+\\+)?{subtype}(?:;.*)?'.format(type = re.escape(maintype), subtype = re.escape(subtype)) + return re.fullmatch(pattern, contenttype, re.IGNORECASE) is not None def should_bypass_proxies(url: str, no_proxy: str) -> bool: """Return whether ``url`` matches the comma-separated ``no_proxy`` rules.""" @@ -150,10 +153,6 @@ class RESTClientObject: else: self.pool_manager = urllib3.PoolManager(**pool_args) - def _contenttype_matches(contenttype, maintype, subtype): - pattern = '{type}/(?:[^+;]+\\+)?{subtype}(?:;.*)?'.format(type = re.escape(maintype), subtype = re.escape(subtype)) - return re.fullmatch(pattern, contenttype, re.IGNORECASE) is not None - def request( self, method, @@ -216,7 +215,7 @@ class RESTClientObject: content_type = headers.get('Content-Type') is_json = ( not content_type - or _contenttype_matches(content_type, 'application', 'json') + or contenttype_matches(content_type, 'application', 'json') ) # JSON is valid YAML 1.2, so structured YAML bodies can use # the existing JSON serializer: @@ -224,9 +223,9 @@ class RESTClientObject: is_structured_yaml = ( content_type and ( - _contenttype_matches(content_type, 'application', 'yaml') - or _contenttype_matches(content_type, 'text', 'yaml') - or _contenttype_matches(content_type, 'text', 'x-yaml') + contenttype_matches(content_type, 'application', 'yaml') + or contenttype_matches(content_type, 'text', 'yaml') + or contenttype_matches(content_type, 'text', 'x-yaml') ) and not isinstance(body, (str, bytes)) ) @@ -242,7 +241,7 @@ class RESTClientObject: headers=headers, preload_content=False ) - elif _contenttype_matches(content_type, 'application', 'x-www-form-urlencoded'): + elif contenttype_matches(content_type, 'application', 'x-www-form-urlencoded'): r = self.pool_manager.request( method, url, @@ -252,7 +251,7 @@ class RESTClientObject: headers=headers, preload_content=False ) - elif _contenttype_matches(content_type, 'multipart', 'form-data'): + elif contenttype_matches(content_type, 'multipart', 'form-data'): # must del headers['Content-Type'], or the correct # Content-Type which generated by urllib3 will be # overwritten. From c225ef54d82709f03f3ababa5b7e2576f2c85ef8 Mon Sep 17 00:00:00 2001 From: Marcel Konrad Date: Mon, 15 Jun 2026 09:54:32 +0200 Subject: [PATCH 3/5] Allow optional whitespace before parameters and wrap function arguments --- .../src/main/resources/python/rest.mustache | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/resources/python/rest.mustache b/modules/openapi-generator/src/main/resources/python/rest.mustache index 6b6fb3def1b6..2978ba61343b 100644 --- a/modules/openapi-generator/src/main/resources/python/rest.mustache +++ b/modules/openapi-generator/src/main/resources/python/rest.mustache @@ -28,7 +28,10 @@ def is_socks_proxy_url(url): return split_section[0].lower() in SUPPORTED_SOCKS_PROXIES def contenttype_matches(contenttype, maintype, subtype): - pattern = '{type}/(?:[^+;]+\\+)?{subtype}(?:;.*)?'.format(type = re.escape(maintype), subtype = re.escape(subtype)) + pattern = '{type}/(?:[^+;]+\\+)?{subtype}(?:[ \t]*;.*)?'.format( + type = re.escape(maintype), + subtype = re.escape(subtype), + ) return re.fullmatch(pattern, contenttype, re.IGNORECASE) is not None def should_bypass_proxies(url: str, no_proxy: str) -> bool: From 1bce667b9f351604ee919314ab8e3a3aff6cb5dc Mon Sep 17 00:00:00 2001 From: Marcel Konrad Date: Wed, 17 Jun 2026 12:50:40 +0200 Subject: [PATCH 4/5] Document contenttype_matches --- .../src/main/resources/python/rest.mustache | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/modules/openapi-generator/src/main/resources/python/rest.mustache b/modules/openapi-generator/src/main/resources/python/rest.mustache index 2978ba61343b..f814e2334575 100644 --- a/modules/openapi-generator/src/main/resources/python/rest.mustache +++ b/modules/openapi-generator/src/main/resources/python/rest.mustache @@ -28,6 +28,15 @@ def is_socks_proxy_url(url): return split_section[0].lower() in SUPPORTED_SOCKS_PROXIES def contenttype_matches(contenttype, maintype, subtype): + """Matches the given contenttype against the given type and subtype + + :param contenttype: the content type to match + :param maintype: the expected maintype + :param subtype: the expected subtype + :return: `true` when the given content type matches the given type and subtype, + regardless of the presence of mime type parameters, otherwise returns `false`. + :rtype: bool + """ pattern = '{type}/(?:[^+;]+\\+)?{subtype}(?:[ \t]*;.*)?'.format( type = re.escape(maintype), subtype = re.escape(subtype), From d9e47c8c8dbd8f5fe95ae80babd71ae5f9e0729f Mon Sep 17 00:00:00 2001 From: Marcel Konrad Date: Wed, 1 Jul 2026 12:12:25 +0200 Subject: [PATCH 5/5] Update examples --- .../openapi_client/rest.py | 29 +++++++++++++++---- .../echo_api/python/openapi_client/rest.py | 29 +++++++++++++++---- .../python-lazyImports/petstore_api/rest.py | 29 +++++++++++++++---- .../petstore/python/petstore_api/rest.py | 29 +++++++++++++++---- 4 files changed, 96 insertions(+), 20 deletions(-) diff --git a/samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent/openapi_client/rest.py b/samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent/openapi_client/rest.py index 934eb3f8b794..ef3ed39f4679 100644 --- a/samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent/openapi_client/rest.py +++ b/samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent/openapi_client/rest.py @@ -37,6 +37,21 @@ def is_socks_proxy_url(url): else: return split_section[0].lower() in SUPPORTED_SOCKS_PROXIES +def contenttype_matches(contenttype, maintype, subtype): + """Matches the given contenttype against the given type and subtype + + :param contenttype: the content type to match + :param maintype: the expected maintype + :param subtype: the expected subtype + :return: `true` when the given content type matches the given type and subtype, + regardless of the presence of mime type parameters, otherwise returns `false`. + :rtype: bool + """ + pattern = '{type}/(?:[^+;]+\\+)?{subtype}(?:[ \t]*;.*)?'.format( + type = re.escape(maintype), + subtype = re.escape(subtype), + ) + return re.fullmatch(pattern, contenttype, re.IGNORECASE) is not None def should_bypass_proxies(url: str, no_proxy: str) -> bool: """Return whether ``url`` matches the comma-separated ``no_proxy`` rules.""" @@ -222,14 +237,18 @@ def request( content_type = headers.get('Content-Type') is_json = ( not content_type - or re.search('json', content_type, re.IGNORECASE) + or contenttype_matches(content_type, 'application', 'json') ) # JSON is valid YAML 1.2, so structured YAML bodies can use # the existing JSON serializer: # https://yaml.org/spec/1.2.2/#13-relation-to-json is_structured_yaml = ( content_type - and re.search('yaml', content_type, re.IGNORECASE) + and ( + contenttype_matches(content_type, 'application', 'yaml') + or contenttype_matches(content_type, 'text', 'yaml') + or contenttype_matches(content_type, 'text', 'x-yaml') + ) and not isinstance(body, (str, bytes)) ) if is_json or is_structured_yaml: @@ -244,7 +263,7 @@ def request( headers=headers, preload_content=False ) - elif content_type == 'application/x-www-form-urlencoded': + elif contenttype_matches(content_type, 'application', 'x-www-form-urlencoded'): r = self.pool_manager.request( method, url, @@ -254,7 +273,7 @@ def request( headers=headers, preload_content=False ) - elif content_type == 'multipart/form-data': + elif contenttype_matches(content_type, 'multipart', 'form-data'): # must del headers['Content-Type'], or the correct # Content-Type which generated by urllib3 will be # overwritten. @@ -282,7 +301,7 @@ def request( headers=headers, preload_content=False ) - elif headers['Content-Type'].startswith('text/') and isinstance(body, bool): + elif content_type.startswith('text/') and isinstance(body, bool): request_body = "true" if body else "false" r = self.pool_manager.request( method, diff --git a/samples/client/echo_api/python/openapi_client/rest.py b/samples/client/echo_api/python/openapi_client/rest.py index b5a114226b79..5bd90b34957e 100644 --- a/samples/client/echo_api/python/openapi_client/rest.py +++ b/samples/client/echo_api/python/openapi_client/rest.py @@ -37,6 +37,21 @@ def is_socks_proxy_url(url): else: return split_section[0].lower() in SUPPORTED_SOCKS_PROXIES +def contenttype_matches(contenttype, maintype, subtype): + """Matches the given contenttype against the given type and subtype + + :param contenttype: the content type to match + :param maintype: the expected maintype + :param subtype: the expected subtype + :return: `true` when the given content type matches the given type and subtype, + regardless of the presence of mime type parameters, otherwise returns `false`. + :rtype: bool + """ + pattern = '{type}/(?:[^+;]+\\+)?{subtype}(?:[ \t]*;.*)?'.format( + type = re.escape(maintype), + subtype = re.escape(subtype), + ) + return re.fullmatch(pattern, contenttype, re.IGNORECASE) is not None def should_bypass_proxies(url: str, no_proxy: str) -> bool: """Return whether ``url`` matches the comma-separated ``no_proxy`` rules.""" @@ -222,14 +237,18 @@ def request( content_type = headers.get('Content-Type') is_json = ( not content_type - or re.search('json', content_type, re.IGNORECASE) + or contenttype_matches(content_type, 'application', 'json') ) # JSON is valid YAML 1.2, so structured YAML bodies can use # the existing JSON serializer: # https://yaml.org/spec/1.2.2/#13-relation-to-json is_structured_yaml = ( content_type - and re.search('yaml', content_type, re.IGNORECASE) + and ( + contenttype_matches(content_type, 'application', 'yaml') + or contenttype_matches(content_type, 'text', 'yaml') + or contenttype_matches(content_type, 'text', 'x-yaml') + ) and not isinstance(body, (str, bytes)) ) if is_json or is_structured_yaml: @@ -244,7 +263,7 @@ def request( headers=headers, preload_content=False ) - elif content_type == 'application/x-www-form-urlencoded': + elif contenttype_matches(content_type, 'application', 'x-www-form-urlencoded'): r = self.pool_manager.request( method, url, @@ -254,7 +273,7 @@ def request( headers=headers, preload_content=False ) - elif content_type == 'multipart/form-data': + elif contenttype_matches(content_type, 'multipart', 'form-data'): # must del headers['Content-Type'], or the correct # Content-Type which generated by urllib3 will be # overwritten. @@ -282,7 +301,7 @@ def request( headers=headers, preload_content=False ) - elif headers['Content-Type'].startswith('text/') and isinstance(body, bool): + elif content_type.startswith('text/') and isinstance(body, bool): request_body = "true" if body else "false" r = self.pool_manager.request( method, diff --git a/samples/openapi3/client/petstore/python-lazyImports/petstore_api/rest.py b/samples/openapi3/client/petstore/python-lazyImports/petstore_api/rest.py index 5df0581723cc..075dca30faee 100644 --- a/samples/openapi3/client/petstore/python-lazyImports/petstore_api/rest.py +++ b/samples/openapi3/client/petstore/python-lazyImports/petstore_api/rest.py @@ -36,6 +36,21 @@ def is_socks_proxy_url(url): else: return split_section[0].lower() in SUPPORTED_SOCKS_PROXIES +def contenttype_matches(contenttype, maintype, subtype): + """Matches the given contenttype against the given type and subtype + + :param contenttype: the content type to match + :param maintype: the expected maintype + :param subtype: the expected subtype + :return: `true` when the given content type matches the given type and subtype, + regardless of the presence of mime type parameters, otherwise returns `false`. + :rtype: bool + """ + pattern = '{type}/(?:[^+;]+\\+)?{subtype}(?:[ \t]*;.*)?'.format( + type = re.escape(maintype), + subtype = re.escape(subtype), + ) + return re.fullmatch(pattern, contenttype, re.IGNORECASE) is not None def should_bypass_proxies(url: str, no_proxy: str) -> bool: """Return whether ``url`` matches the comma-separated ``no_proxy`` rules.""" @@ -221,14 +236,18 @@ def request( content_type = headers.get('Content-Type') is_json = ( not content_type - or re.search('json', content_type, re.IGNORECASE) + or contenttype_matches(content_type, 'application', 'json') ) # JSON is valid YAML 1.2, so structured YAML bodies can use # the existing JSON serializer: # https://yaml.org/spec/1.2.2/#13-relation-to-json is_structured_yaml = ( content_type - and re.search('yaml', content_type, re.IGNORECASE) + and ( + contenttype_matches(content_type, 'application', 'yaml') + or contenttype_matches(content_type, 'text', 'yaml') + or contenttype_matches(content_type, 'text', 'x-yaml') + ) and not isinstance(body, (str, bytes)) ) if is_json or is_structured_yaml: @@ -243,7 +262,7 @@ def request( headers=headers, preload_content=False ) - elif content_type == 'application/x-www-form-urlencoded': + elif contenttype_matches(content_type, 'application', 'x-www-form-urlencoded'): r = self.pool_manager.request( method, url, @@ -253,7 +272,7 @@ def request( headers=headers, preload_content=False ) - elif content_type == 'multipart/form-data': + elif contenttype_matches(content_type, 'multipart', 'form-data'): # must del headers['Content-Type'], or the correct # Content-Type which generated by urllib3 will be # overwritten. @@ -281,7 +300,7 @@ def request( headers=headers, preload_content=False ) - elif headers['Content-Type'].startswith('text/') and isinstance(body, bool): + elif content_type.startswith('text/') and isinstance(body, bool): request_body = "true" if body else "false" r = self.pool_manager.request( method, diff --git a/samples/openapi3/client/petstore/python/petstore_api/rest.py b/samples/openapi3/client/petstore/python/petstore_api/rest.py index 5df0581723cc..075dca30faee 100755 --- a/samples/openapi3/client/petstore/python/petstore_api/rest.py +++ b/samples/openapi3/client/petstore/python/petstore_api/rest.py @@ -36,6 +36,21 @@ def is_socks_proxy_url(url): else: return split_section[0].lower() in SUPPORTED_SOCKS_PROXIES +def contenttype_matches(contenttype, maintype, subtype): + """Matches the given contenttype against the given type and subtype + + :param contenttype: the content type to match + :param maintype: the expected maintype + :param subtype: the expected subtype + :return: `true` when the given content type matches the given type and subtype, + regardless of the presence of mime type parameters, otherwise returns `false`. + :rtype: bool + """ + pattern = '{type}/(?:[^+;]+\\+)?{subtype}(?:[ \t]*;.*)?'.format( + type = re.escape(maintype), + subtype = re.escape(subtype), + ) + return re.fullmatch(pattern, contenttype, re.IGNORECASE) is not None def should_bypass_proxies(url: str, no_proxy: str) -> bool: """Return whether ``url`` matches the comma-separated ``no_proxy`` rules.""" @@ -221,14 +236,18 @@ def request( content_type = headers.get('Content-Type') is_json = ( not content_type - or re.search('json', content_type, re.IGNORECASE) + or contenttype_matches(content_type, 'application', 'json') ) # JSON is valid YAML 1.2, so structured YAML bodies can use # the existing JSON serializer: # https://yaml.org/spec/1.2.2/#13-relation-to-json is_structured_yaml = ( content_type - and re.search('yaml', content_type, re.IGNORECASE) + and ( + contenttype_matches(content_type, 'application', 'yaml') + or contenttype_matches(content_type, 'text', 'yaml') + or contenttype_matches(content_type, 'text', 'x-yaml') + ) and not isinstance(body, (str, bytes)) ) if is_json or is_structured_yaml: @@ -243,7 +262,7 @@ def request( headers=headers, preload_content=False ) - elif content_type == 'application/x-www-form-urlencoded': + elif contenttype_matches(content_type, 'application', 'x-www-form-urlencoded'): r = self.pool_manager.request( method, url, @@ -253,7 +272,7 @@ def request( headers=headers, preload_content=False ) - elif content_type == 'multipart/form-data': + elif contenttype_matches(content_type, 'multipart', 'form-data'): # must del headers['Content-Type'], or the correct # Content-Type which generated by urllib3 will be # overwritten. @@ -281,7 +300,7 @@ def request( headers=headers, preload_content=False ) - elif headers['Content-Type'].startswith('text/') and isinstance(body, bool): + elif content_type.startswith('text/') and isinstance(body, bool): request_body = "true" if body else "false" r = self.pool_manager.request( method,