Environment
google/cloud-storage: v1.51.0
google/cloud-core: v1.72.0
- PHP: 8.x
Summary
Since google/cloud-storage v1.51.0, uploads via Bucket::upload() (and the underlying MultipartUploader / ResumableUploader) no longer honor the restOptions passed to the StorageClient constructor — notably proxy and verify. The change is a regression introduced by #8825 ("Enable full object checksum validation on JSON path").
For environments where outbound HTTPS to storage.googleapis.com is only reachable through an HTTP forward proxy, this breaks all uploads after upgrading from v1.50.x.
Reproduction
use Google\Cloud\Storage\StorageClient;
$storage = new StorageClient([
'restOptions' => [
'proxy' => 'http://proxy.example.com:8080',
'verify' => false,
],
// ... credentials etc.
]);
$bucket = $storage->bucket('my-bucket');
$bucket->upload(fopen('/tmp/some.csv', 'r'), ['name' => 'foo.csv']);
- v1.49.x / v1.50.x: the upload request is sent through
http://proxy.example.com:8080.
- v1.51.0: the upload request goes direct, ignoring the proxy and failing in proxy-only environments.
Metadata calls (e.g. $bucket->info(), $bucket->exists()) and auth token fetches still go through the proxy as expected. Only the actual upload POST is affected.
Root cause
#8825 added X-Goog-Hash header injection in Storage/src/Connection/Rest.php::resolveUploadOptions():
if (!empty($xGoogHashHeader)) {
$args['uploaderOptions']['restOptions']['headers']['X-Goog-Hash'] = $xGoogHashHeader;
}
uploaderOptions is then passed to MultipartUploader / ResumableUploader, stored as $this->requestOptions, and forwarded to RequestWrapper::send($request, $this->requestOptions).
In Core/src/RequestWrapper.php::getRequestOptions():
$restOptions = $options['restOptions'] ?? $this->restOptions;
Per-call $options['restOptions'] replaces the wrapper's stored $this->restOptions instead of merging with it. Before v1.51.0 the uploader didn't pass per-call restOptions (no header injection), so the stored restOptions — including proxy and verify — was used. Now that the uploader always sets restOptions.headers.X-Goog-Hash (because chooseValidationMethod() defaults to crc32 / md5), the stored restOptions is silently dropped on every upload.
Suggested fixes
Either:
- In
RequestWrapper::getRequestOptions() — merge per-call restOptions with the stored one rather than overriding (deep merge, with per-call values winning on conflict). This is the more general fix and preserves the existing API contract for callers that only want to add headers.
- In
Rest::resolveUploadOptions() — instead of writing to $args['uploaderOptions']['restOptions']['headers']['X-Goog-Hash'], pass the header through a dedicated headers field that gets merged with the wrapper's existing restOptions at send time.
(1) is preferable since it also covers the same pattern in #9210 (downloads CRC32C validation) before it lands.
Note
The bug appears to extend to #9210 (object download checksum validation, currently open) which uses the same uploaderOptions.restOptions.headers pattern — worth fixing before that PR merges.
Environment
google/cloud-storage:v1.51.0google/cloud-core:v1.72.0Summary
Since
google/cloud-storagev1.51.0, uploads viaBucket::upload()(and the underlyingMultipartUploader/ResumableUploader) no longer honor therestOptionspassed to theStorageClientconstructor — notablyproxyandverify. The change is a regression introduced by #8825 ("Enable full object checksum validation on JSON path").For environments where outbound HTTPS to
storage.googleapis.comis only reachable through an HTTP forward proxy, this breaks all uploads after upgrading fromv1.50.x.Reproduction
http://proxy.example.com:8080.Metadata calls (e.g.
$bucket->info(),$bucket->exists()) and auth token fetches still go through the proxy as expected. Only the actual upload POST is affected.Root cause
#8825 added
X-Goog-Hashheader injection inStorage/src/Connection/Rest.php::resolveUploadOptions():uploaderOptionsis then passed toMultipartUploader/ResumableUploader, stored as$this->requestOptions, and forwarded toRequestWrapper::send($request, $this->requestOptions).In
Core/src/RequestWrapper.php::getRequestOptions():Per-call
$options['restOptions']replaces the wrapper's stored$this->restOptionsinstead of merging with it. Before v1.51.0 the uploader didn't pass per-callrestOptions(no header injection), so the storedrestOptions— includingproxyandverify— was used. Now that the uploader always setsrestOptions.headers.X-Goog-Hash(becausechooseValidationMethod()defaults tocrc32/md5), the storedrestOptionsis silently dropped on every upload.Suggested fixes
Either:
RequestWrapper::getRequestOptions()— merge per-callrestOptionswith the stored one rather than overriding (deep merge, with per-call values winning on conflict). This is the more general fix and preserves the existing API contract for callers that only want to add headers.Rest::resolveUploadOptions()— instead of writing to$args['uploaderOptions']['restOptions']['headers']['X-Goog-Hash'], pass the header through a dedicatedheadersfield that gets merged with the wrapper's existingrestOptionsat send time.(1) is preferable since it also covers the same pattern in #9210 (downloads CRC32C validation) before it lands.
Note
The bug appears to extend to #9210 (object download checksum validation, currently open) which uses the same
uploaderOptions.restOptions.headerspattern — worth fixing before that PR merges.