From 53bba3a272f3af3544b8634d7a14d8e70e374e4b Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Fri, 26 Jun 2026 22:38:51 -0700 Subject: [PATCH] test(smoke): regression for HEAD on cacheable-extension keys Guards the Cloudflare HEAD->GET cache-rewrite bug fixed in #95: for a key whose extension is in CF's cacheable static-asset set (.tif/.dmg/.zip/...), the edge rewrote the Worker's outbound HEAD into a GET, which failed SigV4 against the HEAD-signed presigned URL (403 SignatureDoesNotMatch). PUTs a `.tif` object to the writable real-S3 bucket and asserts HEAD returns 200. Only reproduces against the real Cloudflare edge, so it lives in the smoke suite (MinIO has no edge cache); gated on SMOKE_WRITE_BUCKET like the multipart-checksum regression. Co-Authored-By: Claude Opus 4.8 --- tests/smoke/test_smoke.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/smoke/test_smoke.py b/tests/smoke/test_smoke.py index e0b10db..d8248bd 100644 --- a/tests/smoke/test_smoke.py +++ b/tests/smoke/test_smoke.py @@ -292,3 +292,34 @@ def test_multipart_roundtrip_with_checksums(self, actions_credentials): assert resp["Body"].read() == body finally: client.delete_object(Bucket=WRITE_BUCKET, Key=key) + + +@requires_write_bucket +class TestHeadOnCacheableExtension: + """Regression guard for HEAD on a key with a cacheable static-asset extension. + + Cloudflare's edge cache only operates on GET; for a URL it treats as a + cacheable static asset (a key whose extension is in CF's default list, e.g. + `.tif`/`.dmg`/`.zip`) it rewrites an outbound HEAD into a GET. Because the + proxy forwards each read via a presigned URL signed for the *original* + method, the rewritten GET fails SigV4 and S3 returns + `403 SignatureDoesNotMatch`. The proxy must mark HEAD subrequests + `RequestCache::NoStore` so the edge leaves the method alone. + + Only reproduces against the real Cloudflare edge — MinIO (the integration + backend) has no edge cache — so this lives in the smoke suite. The `.tif` + extension is what makes the URL look cacheable; a key with no extension was + never affected. + """ + + def test_head_object_with_cacheable_extension(self, actions_credentials): + client = s3_client(actions_credentials) + key = f"smoke-head-{uuid.uuid4()}.tif" + try: + client.put_object(Bucket=WRITE_BUCKET, Key=key, Body=b"regression") + # Before the fix, Cloudflare rewrote this HEAD to GET and S3 + # returned 403 SignatureDoesNotMatch (boto3 would raise here). + resp = client.head_object(Bucket=WRITE_BUCKET, Key=key) + assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200 + finally: + client.delete_object(Bucket=WRITE_BUCKET, Key=key)