googleapis / googleapis/google-cloud-php
Storage v1.51.0: uploads bypass client-level restOptions (proxy, verify) due to per-call override
- 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
Assessment
This issue has not been assessed yet.