Skip to content

gcsemu -- handle HEAD requests like GET - #415

Open
crocodele wants to merge 1 commit into
fullstorydev:masterfrom
crocodele:gcsemu-head-support
Open

gcsemu -- handle HEAD requests like GET#415
crocodele wants to merge 1 commit into
fullstorydev:masterfrom
crocodele:gcsemu-head-support

Conversation

@crocodele

@crocodele crocodele commented Sep 10, 2026

Copy link
Copy Markdown

Problem

GcsEmu.Handler dispatches on DELETE / GET / PATCH / POST / PUT and falls through to 405 Method Not Allowed for HEAD. The real JSON API answers HEAD on both the object metadata and media paths.

This breaks clients that probe for existence with HEAD. Concretely, google-cloud-php changed StorageObject::exists() from a GET to a HEAD against /storage/v1/b/{bucket}/o/{object} in 2.3.0. Against gcsemu it gets a 405, which is not a NotFoundException, so exists() throws a ServiceException instead of returning false. The exception message is empty, because net/http discards the body of a HEAD response — so the failure is fairly opaque from the client side.

Fix

Widen the GET case to case "GET", "HEAD":.

No separate handler is needed:

  • net/http eats handler body writes for HEAD (chunkWriter.Write) while preserving the status code and Content-Length, so the existing GET handlers already produce correct HEAD responses.
  • handleGcsMetadataRequest already returns 404 for a missing object or bucket, which is the case exists() actually depends on.

Tests

Four cases added to the testRawHttp table, appended at the end so the batch subtest's tcs[1:6] slice is unchanged:

Case Asserts
rawHeadMeta 200, empty body
rawHeadMeta-ObjectNotFound 404
rawHeadMeta-BucketNotFound 404
rawHeadObject (?alt=media) 200, empty body, Content-Length == len(v1)

All four fail on master with 405 and pass with the change.

Since testRawHttp is shared, these also run against real GCS via TestRealStore. I ran that with BUCKET_ID set against a real bucket and all four pass there too, so the emulator and production agree on every case asserted here.

Two notes from that run:

  • Real GCS serves metadata HEAD over HTTP/2 and sends no Content-Length, while the emulator (HTTP/1.1) does. I deliberately do not assert Content-Length on the metadata case for that reason. The media case asserts it, since real GCS does set it there.
  • TestRealStore/List_Buckets and TestRealStore/CopyBasics fail against real GCS, but they fail identically on unmodified master, so they are unrelated to this change. (List_Buckets skips on bucket == "" rather than project == "", so it runs without PROJECT_ID; CopyBasics hits a Finalized timestamp mismatch at gcsemu_test.go:580.)

make -C storage ci passes.


Note

Low Risk
Single dispatch-line change in the test emulator with new HTTP tests; no production auth or data-path changes.

Overview
gcsemu now treats HTTP HEAD like GET on the JSON API paths (object metadata and ?alt=media downloads), instead of returning 405 Method Not Allowed. That aligns the emulator with real GCS and fixes clients such as google-cloud-php StorageObject::exists(), which probe existence with HEAD and expect 404 when an object is missing.

Existing GET handlers are reused; net/http strips response bodies for HEAD while keeping status codes and headers (e.g. Content-Length on media).

Tests: four raw HTTP cases in testRawHttp cover successful metadata/media HEAD, missing object, and missing bucket.

Reviewed by Cursor Bugbot for commit 08c86bb. Bugbot is set up for automated code reviews on this repo. Configure here.

The JSON API answers HEAD on both the object metadata and media paths.
gcsemu only dispatched DELETE/GET/PATCH/POST/PUT and fell through to
405 for HEAD.

This breaks clients that probe for existence with HEAD. For example
google-cloud-php implements StorageObject::exists() as a HEAD against
/storage/v1/b/{bucket}/o/{object} as of 2.3.0; against the emulator it
gets a 405, which is not a NotFoundException, so exists() throws
instead of returning false.

No separate handler is needed. net/http discards handler body writes
for HEAD while preserving the status code and Content-Length, and the
metadata path already returns 404 for a missing object or bucket.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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