Skip to content

Encode query keys and values before building request URLs - #514

Draft
Kayvan-Zahiri wants to merge 1 commit into
Adyen:mainfrom
Kayvan-Zahiri:codex/encode-query-parameters
Draft

Kayvan-Zahiri wants to merge 1 commit into
Adyen:mainfrom
Kayvan-Zahiri:codex/encode-query-parameters

Conversation

@Kayvan-Zahiri

Copy link
Copy Markdown

get_query directly interpolates query keys and values. Reserved characters are therefore interpreted as URL syntax: & creates extra parameters, + becomes a space, and # truncates the transmitted query by starting a fragment. Unicode and percent characters are also left unencoded.

Use urllib.parse.urlencode for the existing scalar query dictionary. Preserve the leading ?, numeric values, insertion order, and empty-dictionary output. No generated service or template changes are needed; this changes the shared handwritten utility.

Validation:

  • Two new regression tests fail before the fix and pass afterward; an empty-query control passes throughout.
  • One regression invokes the actual public Checkout service with a mocked HTTP client and parses the exact URL passed to transport. Another round-trips reserved characters in keys/values, Unicode, empty strings, and zero via parse_qs.
  • The complete documented unittest discover -s test -p '*Test.py' suite passes: 184 tests (plus an existing nested 12-test run).
  • Full repository Ruff lint passes; all 142 checked files pass formatting; git diff --check passes.
  • No hosted payment requests, credentials, full Python/OS matrix, or code-generation workflow were used. No generated API services or templates changed.

@sonarqubecloud

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request replaces manual query string construction with urllib.parse.urlencode in Adyen/util.py and introduces a new test suite in test/QueryEncodingTest.py. The review feedback recommends enhancing the query encoding by utilizing quote_via=quote to ensure spaces are encoded as %20 (for RFC 3986 compliance) instead of +, and enabling doseq=True to correctly handle list or sequence parameters. Additionally, it is suggested to update the test suite to verify these encoding behaviors.

Comment thread Adyen/util.py
import copy
import hashlib
import hmac
from urllib.parse import urlencode

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.

medium

Import quote from urllib.parse to support encoding spaces as %20 instead of + in query parameters, ensuring compliance with RFC 3986.

Suggested change
from urllib.parse import urlencode
from urllib.parse import quote, urlencode

Comment thread Adyen/util.py

def get_query(query_parameters):
return "?" + "&".join([f"{k}={v}" for k, v in query_parameters.items()])
return "?" + urlencode(query_parameters)

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.

medium

By default, urlencode uses quote_plus which encodes spaces as +. While + is standard for application/x-www-form-urlencoded POST bodies, RFC 3986 defines + as a reserved character in URLs, and spaces should be encoded as %20 in query strings. Some web servers or API gateways do not decode + to spaces in query parameters, which can lead to unexpected behavior (e.g., receiving literal + characters instead of spaces). Using quote_via=quote ensures spaces are encoded as %20, which is universally compatible and compliant with RFC 3986.

Additionally, using doseq=True ensures that if any query parameter contains a list or sequence of values, it is correctly encoded as repeated parameters (e.g., param=a&param=b) rather than stringifying the sequence.

Suggested change
return "?" + urlencode(query_parameters)
return "?" + urlencode(query_parameters, doseq=True, quote_via=quote)

Comment thread test/QueryEncodingTest.py
Comment on lines +10 to +22
def test_reserved_characters_round_trip(self):
parameters = {
"reference": "Invoice A&B + 10% #café",
"filter[key]&name": "x=y?z/1",
"empty": "",
"pageNumber": 0,
}
encoded = get_query(parameters)
self.assertEqual(
parse_qs(urlsplit("https://example.test/" + encoded).query, keep_blank_values=True),
{key: [str(value)] for key, value in parameters.items()},
)
self.assertEqual(urlsplit("https://example.test/" + encoded).fragment, "")

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.

medium

Update the test to verify that spaces are encoded as %20 (not +) and to cover list/sequence parameters using doseq=True.

Suggested change
def test_reserved_characters_round_trip(self):
parameters = {
"reference": "Invoice A&B + 10% #café",
"filter[key]&name": "x=y?z/1",
"empty": "",
"pageNumber": 0,
}
encoded = get_query(parameters)
self.assertEqual(
parse_qs(urlsplit("https://example.test/" + encoded).query, keep_blank_values=True),
{key: [str(value)] for key, value in parameters.items()},
)
self.assertEqual(urlsplit("https://example.test/" + encoded).fragment, "")
def test_reserved_characters_round_trip(self):
parameters = {
"reference": "Invoice A&B + 10% #café",
"filter[key]&name": "x=y?z/1",
"empty": "",
"pageNumber": 0,
"list_param": ["a", "b"],
}
encoded = get_query(parameters)
self.assertNotIn("+", encoded)
self.assertEqual(
parse_qs(urlsplit("https://example.test/" + encoded).query, keep_blank_values=True),
{key: [str(v) for v in value] if isinstance(value, list) else [str(value)] for key, value in parameters.items()},
)
self.assertEqual(urlsplit("https://example.test/" + encoded).fragment, "")

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant