Skip to content

fix(core): merge per-call restOptions with the client-level ones - #9437

Open
smichaelsen wants to merge 1 commit into
googleapis:mainfrom
smichaelsen:fix-core-request-options-merge
Open

fix(core): merge per-call restOptions with the client-level ones#9437
smichaelsen wants to merge 1 commit into
googleapis:mainfrom
smichaelsen:fix-core-request-options-merge

Conversation

@smichaelsen

Copy link
Copy Markdown

Fixes #9212.

Problem

RequestWrapper::getRequestOptions() replaces the client-level restOptions with the per-call ones whenever a call supplies any:

$restOptions = $options['restOptions'] ?? $this->restOptions;

Transport settings configured once on the client — proxy, verify, cert — are therefore silently dropped for any call that passes its own restOptions.

Storage made this visible in v1.51.0. The X-Goog-Hash checksum header added in #8825 travels as a per-call restOptions entry, so every upload loses the client's proxy configuration. Downloads reach the same code path through the on_headers callback used to detect transcoded objects. In proxy-only environments uploads and downloads stop working, while metadata and auth calls — which pass no per-call restOptions — keep succeeding, which makes the failure look unrelated to the upgrade.

This is not Storage-specific: any component that sets per-call restOptions drops client transport configuration the same way.

Fix

Merge instead of replace. Per-call options win on conflict, and nested headers arrays are merged on their own so a per-call header does not drop the client's default headers.

The merge is deliberately shallow otherwise. array_merge_recursive is not usable here: Guzzle options mix scalars and arrays, and colliding scalars such as proxy would be turned into arrays that Guzzle rejects.

Tests

Three cases added to Core/tests/Unit/RequestWrapperTest.php:

  • client-level proxy/verify survive a call that supplies its own restOptions (the Storage upload scenario)
  • per-call options take precedence over client-level ones on conflict
  • headers merge rather than replace

The first two fail on main with Undefined array key "proxy". The full Core unit suite passes (534 tests), as does phpcs --standard=phpcs-ruleset.xml on both changed files.

Credit

The root cause analysis and the merge strategy are @salilg-eng's, from the discussion in #9212. I'm opening the PR since the issue has been sitting for a couple of months and it is currently blocking us from upgrading past google/cloud-storage 1.50.

RequestWrapper::getRequestOptions() replaced the client-level restOptions
with the per-call ones whenever a call supplied any, so transport settings
configured once on the client — proxy, verify, cert — were silently dropped
for those calls.

Storage made this visible in v1.51.0: the X-Goog-Hash checksum header added
in googleapis#8825 travels as a per-call restOptions entry, so every upload lost the
client's proxy configuration. Downloads hit the same path through the
on_headers callback used to detect transcoded objects. In proxy-only
environments uploads and downloads stopped working while metadata and
auth calls, which pass no per-call restOptions, kept succeeding.

Per-call options now take precedence over the client-level ones on conflict,
and nested `headers` arrays are merged on their own so a per-call header does
not drop the client's default headers. The merge is otherwise shallow, which
avoids array_merge_recursive turning colliding scalars such as `proxy` into
arrays that Guzzle rejects.

Root cause analysis and the merge strategy are from @salilg-eng in googleapis#9212.

Fixes googleapis#9212
@smichaelsen
smichaelsen requested a review from a team as a code owner August 6, 2026 07:50
@google-cla

google-cla Bot commented Aug 6, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@bshaffer bshaffer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have one minor suggestion, but in general this is LGTM. We should get a review from @thiyaguk09 as well

Comment on lines +480 to +486
$perCallRestOptions = $options['restOptions'] ?? [];
$headers = ($perCallRestOptions['headers'] ?? []) + ($this->restOptions['headers'] ?? []);
$restOptions = $perCallRestOptions + $this->restOptions;

if ($headers) {
$restOptions['headers'] = $headers;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic would be cleaner like this:

Suggested change
$perCallRestOptions = $options['restOptions'] ?? [];
$headers = ($perCallRestOptions['headers'] ?? []) + ($this->restOptions['headers'] ?? []);
$restOptions = $perCallRestOptions + $this->restOptions;
if ($headers) {
$restOptions['headers'] = $headers;
}
$restOptions = ($options['restOptions'] ?? []) + $this->restOptions;
if ($headers = ($options['restOptions']['headers'] ?? []) + ($this->restOptions['headers'] ?? [])) {
$restOptions['headers'] = $headers;
}

@salilg-eng

Copy link
Copy Markdown
Contributor

Looks good to me! This is exactly what I had in mind for #9212. The array union approach is clean, and handling the headers separately was a good call.

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.

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

3 participants