Skip to content

Fix REST signing for S3 bulk deletes - #4039

Draft
smaheshwar-pltr wants to merge 1 commit into
apache:mainfrom
smaheshwar-pltr:fix/s3-remote-signing-delete-body
Draft

smaheshwar-pltr wants to merge 1 commit into
apache:mainfrom
smaheshwar-pltr:fix/s3-remote-signing-delete-body

Conversation

@smaheshwar-pltr

@smaheshwar-pltr smaheshwar-pltr commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Rationale for this change

FsspecFileIO.delete goes through s3fs, which sends a DeleteObjects request even for a single file. That request lists the object keys in its XML body, and the URL only names the bucket. S3V4RestSigner never forwarded the body, so a signing service could not check which objects it was authorizing. The REST spec's body field on the sign request exists for this case.

The signer now forwards the body when botocore's before-sign event names the DeleteObjects operation. The Java client checks for POST with a delete query parameter instead. botocore already passes the operation name to signing hooks, so this avoids parsing the URL. Other requests are unchanged, so upload bodies are never read or sent to the signing service.

Are these changes tested?

Yes. A new s3 test writes, reads, and deletes a file through S3V4RestSigner against the local S3 store used by make test-s3 and the integration-test-s3 CI job. The signing endpoint is a requests_mock stub that SigV4-signs each request with the store's credentials, so every S3 call goes through the REST signing path. The test checks that exactly one sign request carries a body and that it names the deleted key. It fails without the fix.

Are there any user-facing changes?

Sign requests for DeleteObjects now include the request body.

🤖 Generated with Claude Code

@smaheshwar-pltr smaheshwar-pltr left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change follows the Java signer's DeleteObjects handling. The inline notes link the protocol contract, the Python serialization path, and the caller that makes this necessary.

Comment thread pyiceberg/io/fsspec.py Outdated
"uri": request.url,
"headers": {key: [val] for key, val in request.headers.items()},
}
if request.method == "POST" and "delete" in parse_qs(urlparse(request.url).query, keep_blank_values=True):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This matches the Java client's existing predicate and body handling: POST with a delete query key. The REST contract calls out DeleteObjects because its body contains the keys that the signing service must validate; the URL identifies only the bucket. keep_blank_values=True makes both ?delete and ?delete= recognizable. Other POST operations, including multipart completion, keep their existing behavior.

Comment thread pyiceberg/io/fsspec.py Outdated
}
if request.method == "POST" and "delete" in parse_qs(urlparse(request.url).query, keep_blank_values=True):
if body := request.body:
signer_body["body"] = body.decode("utf-8")

@smaheshwar-pltr smaheshwar-pltr Oct 1, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bytes assumption comes from Botocore's REST XML serializer, which serializes the request using its UTF-8 default. AWSRequest.body exposes that prepared body. Decoding creates the string expected by the signing API; it does not replace the original XML bytes or checksum headers sent to S3. The guards above prevent accessing unrelated upload streams.

Comment thread tests/io/test_fsspec.py Outdated


@pytest.mark.parametrize("query", ["delete", "delete=", "delete=&x-id=DeleteObjects"])
def test_s3v4_rest_signer_sends_delete_objects_body(requests_mock: Mocker, query: str) -> None:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is reachable even when deleting one file: FsspecFileIO.delete calls fs.rm, and s3fs batches those paths through _bulk_delete, which calls DeleteObjects. All three cases here failed before the fix because the captured signing JSON had no body. The six cases below separately guard against forwarding or reading unrelated request bodies.

@Fokko

Fokko commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Thanks for raising this @smaheshwar-pltr. I know pre-signed URLs conceptually, but not the details. Maybe @danielcweeks has a free cycle to take a look at this. Can I ask if you have tested this against a S3 instance? One issue we have with the pre-signed url code is that it doesn't run any integration tests again MinIO.

s3fs deletes files with DeleteObjects, which lists the object keys in the
request body. S3V4RestSigner never forwarded the body, so a signing
service could not authorize the keys before signing.

Also add an S3 test that writes, reads, and deletes through the REST
signer, which previously had only mocked unit tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@smaheshwar-pltr
smaheshwar-pltr force-pushed the fix/s3-remote-signing-delete-body branch from 5d96c0e to cd46cb3 Compare October 15, 2026 20:11

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.

2 participants