googleapis / googleapis/google-cloud-php

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

Open
#9,212 6 comments 1 reaction 1 assignee Claimed by @Hectorhammett View on GitHub
Dominant language
PHP
Stars
1.2k
Forks
463
Avg merge
2d 1h
Merged PRs (30d)
145

Description

### 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

```php
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()`:

```php
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()`:

```php
$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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.