This is an automated email from the ASF dual-hosted git repository.

kevinjqliu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-python.git


The following commit(s) were added to refs/heads/main by this push:
     new 097b458b fix(rest): handle empty body in AWS SigV4 signing (#2827)
097b458b is described below

commit 097b458bfd313fe99603efb53644e9203783803e
Author: Jiajia Li <[email protected]>
AuthorDate: Fri Jan 16 08:10:24 2026 +0800

    fix(rest): handle empty body in AWS SigV4 signing (#2827)
    
    # Rationale for this change
    The x-amz-content-sha256 header is required for AWS requests. It
    provides a hash of the request payload. If there is no payload, you must
    provide the hash of an empty string.
    
    ## Are these changes tested?
    
    ## Are there any user-facing changes?
    
    ---------
    
    Co-authored-by: Fokko Driesprong <[email protected]>
---
 pyiceberg/catalog/rest/__init__.py |  7 +++-
 tests/catalog/test_rest.py         | 67 +++++++++++++++++++++++++++++++++++++-
 2 files changed, 72 insertions(+), 2 deletions(-)

diff --git a/pyiceberg/catalog/rest/__init__.py 
b/pyiceberg/catalog/rest/__init__.py
index 29fcade7..533c4313 100644
--- a/pyiceberg/catalog/rest/__init__.py
+++ b/pyiceberg/catalog/rest/__init__.py
@@ -216,6 +216,7 @@ SSL = "ssl"
 SIGV4 = "rest.sigv4-enabled"
 SIGV4_REGION = "rest.signing-region"
 SIGV4_SERVICE = "rest.signing-name"
+EMPTY_BODY_SHA256: str = 
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
 OAUTH2_SERVER_URI = "oauth2-server-uri"
 SNAPSHOT_LOADING_MODE = "snapshot-loading-mode"
 AUTH = "auth"
@@ -560,7 +561,11 @@ class RestCatalog(Catalog):
                 params = dict(parse.parse_qsl(query))
 
                 # remove the connection header as it will be updated after 
signing
-                del request.headers["connection"]
+                if "connection" in request.headers:
+                    del request.headers["connection"]
+                # For empty bodies, explicitly set the content hash header to 
the SHA256 of an empty string
+                if not request.body:
+                    request.headers["x-amz-content-sha256"] = EMPTY_BODY_SHA256
 
                 aws_request = AWSRequest(
                     method=request.method, url=url, params=params, 
data=request.body, headers=dict(request.headers)
diff --git a/tests/catalog/test_rest.py b/tests/catalog/test_rest.py
index 281418e0..8b9cbd89 100644
--- a/tests/catalog/test_rest.py
+++ b/tests/catalog/test_rest.py
@@ -22,12 +22,21 @@ from typing import Any, cast
 from unittest import mock
 
 import pytest
+from requests import Request
+from requests.adapters import HTTPAdapter
 from requests.exceptions import HTTPError
 from requests_mock import Mocker
 
 import pyiceberg
 from pyiceberg.catalog import PropertiesUpdateSummary, load_catalog
-from pyiceberg.catalog.rest import DEFAULT_ENDPOINTS, OAUTH2_SERVER_URI, 
SNAPSHOT_LOADING_MODE, Capability, RestCatalog
+from pyiceberg.catalog.rest import (
+    DEFAULT_ENDPOINTS,
+    EMPTY_BODY_SHA256,
+    OAUTH2_SERVER_URI,
+    SNAPSHOT_LOADING_MODE,
+    Capability,
+    RestCatalog,
+)
 from pyiceberg.exceptions import (
     AuthorizationExpiredError,
     NamespaceAlreadyExistsError,
@@ -451,6 +460,62 @@ def test_list_tables_200_sigv4(rest_mock: Mocker) -> None:
     assert rest_mock.called
 
 
+def test_sigv4_sign_request_without_body(rest_mock: Mocker) -> None:
+    existing_token = "existing_token"
+
+    catalog = RestCatalog(
+        "rest",
+        **{
+            "uri": TEST_URI,
+            "token": existing_token,
+            "rest.sigv4-enabled": "true",
+            "rest.signing-region": "us-west-2",
+            "client.access-key-id": "id",
+            "client.secret-access-key": "secret",
+        },
+    )
+
+    prepared = catalog._session.prepare_request(Request("GET", 
f"{TEST_URI}v1/config"))
+    adapter = catalog._session.adapters[catalog.uri]
+    assert isinstance(adapter, HTTPAdapter)
+    adapter.add_headers(prepared)
+
+    assert prepared.headers["Authorization"].startswith("AWS4-HMAC-SHA256")
+    assert prepared.headers["Original-Authorization"] == f"Bearer 
{existing_token}"
+    assert prepared.headers["x-amz-content-sha256"] == EMPTY_BODY_SHA256
+
+
+def test_sigv4_sign_request_with_body(rest_mock: Mocker) -> None:
+    existing_token = "existing_token"
+
+    catalog = RestCatalog(
+        "rest",
+        **{
+            "uri": TEST_URI,
+            "token": existing_token,
+            "rest.sigv4-enabled": "true",
+            "rest.signing-region": "us-west-2",
+            "client.access-key-id": "id",
+            "client.secret-access-key": "secret",
+        },
+    )
+
+    prepared = catalog._session.prepare_request(
+        Request(
+            "POST",
+            f"{TEST_URI}v1/namespaces",
+            data={"namespace": "asdfasd"},
+        )
+    )
+    adapter = catalog._session.adapters[catalog.uri]
+    assert isinstance(adapter, HTTPAdapter)
+    adapter.add_headers(prepared)
+
+    assert prepared.headers["Authorization"].startswith("AWS4-HMAC-SHA256")
+    assert prepared.headers["Original-Authorization"] == f"Bearer 
{existing_token}"
+    assert prepared.headers.get("x-amz-content-sha256") != EMPTY_BODY_SHA256
+
+
 def test_list_tables_404(rest_mock: Mocker) -> None:
     namespace = "examples"
     rest_mock.get(

Reply via email to