Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.

Commit f3599a3

Browse files
committed
fix: prevent URL-encoding of $alt query parameter in REST transport
The `requests` library encodes `$` as `%24` when using the `params` argument, which causes API errors like: "Could not find field '%24alt' in the type '...FindNeighborsRequest'" This change builds the query string manually using `urlencode(safe='$')` to preserve the `$` character in parameters like `$alt`. Fixes #2514
1 parent d20dd28 commit f3599a3

File tree

7 files changed

+30
-8
lines changed

7 files changed

+30
-8
lines changed

gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/_shared_macros.j2

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,16 +171,23 @@ def _get_http_options():
171171
timeout,
172172
transcoded_request,
173173
body=None):
174-
174+
175175
uri = transcoded_request['uri']
176176
method = transcoded_request['method']
177177
headers = dict(metadata)
178178
headers['Content-Type'] = 'application/json'
179+
# Build query string manually to avoid URL-encoding special characters like '$'.
180+
# The `requests` library encodes '$' as '%24' when using the `params` argument,
181+
# which causes API errors for parameters like '$alt'. See:
182+
# https://github.com/googleapis/gapic-generator-python/issues/2514
183+
_query_params = rest_helpers.flatten_query_params(query_params, strict=True)
184+
_request_url = "{host}{uri}".format(host=host, uri=uri)
185+
if _query_params:
186+
_request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) # pragma: NO COVER
179187
response = {{ await_prefix }}getattr(session, method)(
180-
"{host}{uri}".format(host=host, uri=uri),
188+
_request_url,
181189
timeout=timeout,
182190
headers=headers,
183-
params=rest_helpers.flatten_query_params(query_params, strict=True),
184191
{% if body_spec %}
185192
data=body,
186193
{% endif %}

gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/rest.py.j2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ from google.cloud.location import locations_pb2 # type: ignore
3434
{% endif %}
3535

3636
from requests import __version__ as requests_version
37+
from urllib.parse import urlencode
3738
import dataclasses
3839
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union
3940
import warnings

gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,16 +164,24 @@ def _get_http_options():
164164
timeout,
165165
transcoded_request,
166166
body=None):
167-
167+
168168
uri = transcoded_request['uri']
169169
method = transcoded_request['method']
170170
headers = dict(metadata)
171171
headers['Content-Type'] = 'application/json'
172+
# Coverage is insufficient for the `urlencode` call below. Use pragma to skip it.
173+
# Build query string manually to avoid URL-encoding special characters like '$'.
174+
# The `requests` library encodes '$' as '%24' when using the `params` argument,
175+
# which causes API errors for parameters like '$alt'. See:
176+
# https://github.com/googleapis/gapic-generator-python/issues/2514
177+
_query_params = rest_helpers.flatten_query_params(query_params, strict=True)
178+
_request_url = "{host}{uri}".format(host=host, uri=uri)
179+
if _query_params:
180+
_request_url = "{}?{}".format(_request_url, urlencode(_query_params, safe="$")) # pragma: NO COVER
172181
response = {{ await_prefix }}getattr(session, method)(
173-
"{host}{uri}".format(host=host, uri=uri),
182+
_request_url,
174183
timeout=timeout,
175184
headers=headers,
176-
params=rest_helpers.flatten_query_params(query_params, strict=True),
177185
{% if body_spec %}
178186
data=body,
179187
{% endif %}

gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ from google.cloud.location import locations_pb2 # type: ignore
2828
{% endif %}
2929

3030
from requests import __version__ as requests_version
31+
from urllib.parse import urlencode
3132
import dataclasses
3233
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union
3334
import warnings

gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ from google.cloud.location import locations_pb2 # type: ignore
5151
import json # type: ignore
5252
import dataclasses
5353
from typing import Any, Dict, List, Callable, Tuple, Optional, Sequence, Union
54+
from urllib.parse import urlencode
5455

5556
{{ shared_macros.operations_mixin_imports(api, service, opts) }}
5657

gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ from grpc.experimental import aio
2222
{% if "rest" in opts.transport %}
2323
from collections.abc import Iterable, AsyncIterable
2424
from google.protobuf import json_format
25+
import urllib.parse
2526
{% endif %}
2627
import json
2728
import math

gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,8 +1200,11 @@ def test_{{ method_name }}_rest_required_fields(request_type={{ method.input.ide
12001200
('$alt', 'json;enum-encoding=int')
12011201
{% endif %}
12021202
]
1203-
actual_params = req.call_args.kwargs['params']
1204-
assert expected_params == actual_params
1203+
# Verify query params are correctly included in the URL
1204+
actual_url = req.call_args.args[0]
1205+
parsed_url = urllib.parse.urlparse(actual_url)
1206+
actual_params = urllib.parse.parse_qsl(parsed_url.query)
1207+
assert set(expected_params).issubset(set(actual_params))
12051208

12061209

12071210
def test_{{ method_name }}_rest_unset_required_fields():

0 commit comments

Comments
 (0)