diff --git a/packages/aws-sdk-signers/src/aws_sdk_signers/interfaces/http.py b/packages/aws-sdk-signers/src/aws_sdk_signers/interfaces/http.py index 8d450ceb..30a4f8ea 100644 --- a/packages/aws-sdk-signers/src/aws_sdk_signers/interfaces/http.py +++ b/packages/aws-sdk-signers/src/aws_sdk_signers/interfaces/http.py @@ -57,7 +57,7 @@ def remove(self, value: str) -> None: """Remove all matching entries from list.""" ... - def as_string(self, delimiter: str = ", ") -> str: + def as_string(self, delimiter: str = ",") -> str: """Serialize the ``Field``'s values into a single line string.""" ... diff --git a/packages/aws-sdk-signers/src/aws_sdk_signers/signers.py b/packages/aws-sdk-signers/src/aws_sdk_signers/signers.py index 29ba0289..5cb2137a 100644 --- a/packages/aws-sdk-signers/src/aws_sdk_signers/signers.py +++ b/packages/aws-sdk-signers/src/aws_sdk_signers/signers.py @@ -705,7 +705,7 @@ async def _format_canonical_query(self, *, query: str | None) -> str: async def _normalize_signing_fields(self, *, request: AWSRequest) -> dict[str, str]: normalized_fields = { - field.name.lower(): field.as_string() + field.name.lower(): field.as_string(delimiter=",") for field in request.fields if self._is_signable_header(field.name.lower()) } diff --git a/packages/smithy-http/.changes/next-release/smithy-http-bugfix-20251023131033.json b/packages/smithy-http/.changes/next-release/smithy-http-bugfix-20251023131033.json new file mode 100644 index 00000000..78ddf08c --- /dev/null +++ b/packages/smithy-http/.changes/next-release/smithy-http-bugfix-20251023131033.json @@ -0,0 +1,4 @@ +{ + "type": "bugfix", + "description": "Fix `InvalidSignatureException` caused by mismatched list header formatting during signing (e.g., `MyHeader: value1, value2` vs `MyHeader: value1,value2`)." +} \ No newline at end of file diff --git a/packages/smithy-http/src/smithy_http/__init__.py b/packages/smithy-http/src/smithy_http/__init__.py index bdd5efda..b627f2ea 100644 --- a/packages/smithy-http/src/smithy_http/__init__.py +++ b/packages/smithy-http/src/smithy_http/__init__.py @@ -63,7 +63,7 @@ def as_string(self, delimiter: str = ",") -> str: return "" if value_count == 1: return self.values[0] - return ", ".join(quote_and_escape_field_value(val) for val in self.values) + return delimiter.join(quote_and_escape_field_value(val) for val in self.values) def as_tuples(self) -> list[tuple[str, str]]: """Get list of ``name``, ``value`` tuples where each tuple represents one diff --git a/packages/smithy-http/tests/unit/test_fields.py b/packages/smithy-http/tests/unit/test_fields.py index 884c9d10..aebc9557 100644 --- a/packages/smithy-http/tests/unit/test_fields.py +++ b/packages/smithy-http/tests/unit/test_fields.py @@ -23,7 +23,7 @@ def test_field_multi_valued_basics() -> None: assert field.name == "fname" assert field.kind == FieldPosition.HEADER assert field.values == ["fval1", "fval2"] - assert field.as_string() == "fval1, fval2" + assert field.as_string() == "fval1,fval2" assert field.as_tuples() == [("fname", "fval1"), ("fname", "fval2")] @@ -36,24 +36,24 @@ def test_field_multi_valued_basics() -> None: (['"'], '"'), (['val"1'], 'val"1'), (["val\\1"], "val\\1"), - # Multi-valued fields are joined with one comma and one space as separator. - (["val1", "val2"], "val1, val2"), - (["val1", "val2", "val3", "val4"], "val1, val2, val3, val4"), - (["©väl", "val2"], "©väl, val2"), + # Multi-valued fields are joined with one comma as separator. + (["val1", "val2"], "val1,val2"), + (["val1", "val2", "val3", "val4"], "val1,val2,val3,val4"), + (["©väl", "val2"], "©väl,val2"), # Values containing commas must be double-quoted. - (["val1", "val2,val3", "val4"], 'val1, "val2,val3", val4'), - (["v,a,l,1", "val2"], '"v,a,l,1", val2'), + (["val1", "val2,val3", "val4"], 'val1,"val2,val3",val4'), + (["v,a,l,1", "val2"], '"v,a,l,1",val2'), # In strings that get quoted, pre-existing double quotes are escaped with a # single backslash. The second backslash below is for escaping the actual # backslash in the string for Python. - (["slc", '4,196"'], 'slc, "4,196\\""'), - (['"val1"', "val2"], '"\\"val1\\"", val2'), - (["val1", '"'], 'val1, "\\""'), - (['val1:2",val3:4"', "val5"], '"val1:2\\",val3:4\\"", val5'), + (["slc", '4,196"'], 'slc,"4,196\\""'), + (['"val1"', "val2"], '"\\"val1\\"",val2'), + (["val1", '"'], 'val1,"\\""'), + (['val1:2",val3:4"', "val5"], '"val1:2\\",val3:4\\"",val5'), # If quoting happens, backslashes are also escaped. The following case is a # single backslash getting serialized into two backslashes. Python escaping # accounts for each actual backslash being written as two. - (["foo,bar\\", "val2"], '"foo,bar\\\\", val2'), + (["foo,bar\\", "val2"], '"foo,bar\\\\",val2'), ], ) def test_field_serialization(values: list[str], expected: str):