Skip to content

Storage v1.51.0: uploads bypass client-level restOptions (proxy, verify) due to per-call override #9212

@smichaelsen

Description

@smichaelsen

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:

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions