Skip to content

fix(storage): resolve transport and retry issues#8235

Open
thiyaguk09 wants to merge 20 commits into
googleapis:storage-node-18from
thiyaguk09:storage-transport-retry-fix
Open

fix(storage): resolve transport and retry issues#8235
thiyaguk09 wants to merge 20 commits into
googleapis:storage-node-18from
thiyaguk09:storage-transport-retry-fix

Conversation

@thiyaguk09
Copy link
Copy Markdown
Contributor

Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly:

  • Make sure to open an issue as a bug/issue before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea
  • Ensure the tests and linter pass
  • Code coverage does not decrease (if any source code was changed)
  • Appropriate docs were updated (if necessary)

Fixes #<issue_number_goes_here> 🦕

@thiyaguk09 thiyaguk09 requested review from a team as code owners May 7, 2026 12:00
@product-auto-label product-auto-label Bot added the api: storage Issues related to the Cloud Storage API. label May 7, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors request handling and retry logic by transitioning from the common Service class to a specialized StorageTransport. Key changes include the introduction of granular idempotency logic for HTTP methods, improved encryption header management, and a custom parameter serializer. Review feedback identifies a high-severity race condition involving shared interceptors on the Gaxios instance and inconsistent return types in the response handler. Additionally, improvements were suggested regarding the efficiency of JSON parsing within retry loops and the safety of non-null assertions in encryption headers.

Comment on lines 140 to 145
this.gaxiosInstance.interceptors.request.clear();
if (reqOpts.interceptors) {
this.gaxiosInstance.interceptors.request.clear();
for (const inter of reqOpts.interceptors) {
this.gaxiosInstance.interceptors.request.add(inter);
}
}
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.

high

Clearing and adding interceptors on a shared this.gaxiosInstance before each asynchronous request creates a race condition. If multiple makeRequest calls are in flight, one call may clear the interceptors intended for another. Additionally, verify if this.authClient.request actually uses this.gaxiosInstance; if it doesn't, these interceptors are ineffective.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

required for retry operations fix in conformance Test PR

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.

What is the retry operations fix and why do we need to make this change in the current PR ? Same for the other comments below.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

To support custom headers required by conformance tests without creating race conditions where concurrent async requests overwrite each other's configuration on a shared instance, we modified makeRequest to isolate custom interceptors using a fresh, request-scoped Gaxios instance, while explicitly stripping the adapter property during cloning to prevent infinite recursion and heap out-of-memory crashes between GoogleAuth and Gaxios.

Comment on lines +183 to +192
const responseHandler = (resp: GaxiosResponse<T>) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data = resp.data as any;
if (data !== null && typeof data === 'object') {
data.headers = resp.headers;
data.status = resp.status;
return data;
}
return resp;
};
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.

high

The responseHandler returns inconsistent types. If resp.data is an object, it returns the data with injected headers and status properties. Otherwise, it returns the full GaxiosResponse object. This inconsistency will likely cause runtime errors for consumers expecting a uniform return structure. Furthermore, injecting properties into resp.data is risky if the data is a Buffer or Stream.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

required for retry operations

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.

Please elaborate as stated in the above comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

To preserve backward compatibility for legacy SDK methods that expect resource metadata objects to carry .headers and .status properties without introducing runtime errors or mutating binary data, we added a strict validation check to ensure these properties are strictly injected into plain JSON response objects and never onto Buffer or Stream payloads.

Comment thread handwritten/storage/src/storage.ts Outdated
// Optimized Precondition Check
let bodyEtag = false;
try {
const parsedBody = typeof data === 'string' ? JSON.parse(data) : data;
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.

medium

Parsing the request body using JSON.parse inside the retry logic is inefficient for large payloads and will be executed on every retryable error check. Consider checking for preconditions earlier or in a way that avoids repeated parsing of the body. Additionally, this logic may fail or behave unexpectedly if data is a Buffer or Stream.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

required for retry operations fix in conformance Test PR

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.

Please elaborate as stated in the above comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

To eliminate the severe performance degradation and stream-breaking risks of repeatedly running JSON.parse inside the retry loop to detect preconditions like an etag, we now pre-calculate this presence once during the request preparation phase in StorageTransport.makeRequest and cache it as a simple boolean flag on the request configuration for the retry evaluator to read instantly.

Comment thread handwritten/storage/src/file.ts Outdated
return {
'x-goog-encryption-algorithm': 'AES256',
'x-goog-encryption-key': this.encryptionKey.toString('base64'),
'x-goog-encryption-key-sha256': this.encryptionKeyHash!,
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.

medium

The non-null assertion this.encryptionKeyHash! is unsafe. If encryptionKeyHash is missing, the header will be set to undefined. It's better to provide a fallback value.

Suggested change
'x-goog-encryption-key-sha256': this.encryptionKeyHash!,
'x-goog-encryption-key-sha256': this.encryptionKeyHash || '',

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.

Resolved without response. Reopening again.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. Replaced the non-null assertion with an empty string fallback (|| '') to safely handle cases where encryptionKeyHash is missing and prevent undefined headers.

thiyaguk09 and others added 2 commits May 7, 2026 17:49
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
…sport (googleapis#8283)

- Remove Service.ts and common.ts files from handwritten/storage

- Migrate remaining functionality to StorageTransport

- chore(ci): upgrade conformance tests to Node 18
…sport (googleapis#8283)

- Remove Service.ts and common.ts files from handwritten/storage

- Migrate remaining functionality to StorageTransport

- chore(ci): upgrade conformance tests to Node 18
…sport (googleapis#8283)

- Remove Service.ts and common.ts files from handwritten/storage

- Migrate remaining functionality to StorageTransport

- chore(ci): upgrade conformance tests to Node 18
…sport (googleapis#8283)

- Remove Service.ts and common.ts files from handwritten/storage

- Migrate remaining functionality to StorageTransport

- chore(ci): upgrade conformance tests to Node 18
…e etag check

- Updated `responseHandler` to correctly handle different payload types:
- Plain objects are mutated with `.headers` and `.status` and
returned.
- Binary payloads (Buffer/Stream) return raw data to prevent dangerous
mutations.
- Primitives (e.g., empty strings) return the full `GaxiosResponse`
wrapper to preserve access to headers like `Location` for resumable
upload initiation.
- Fixed `hasPrecondition` logic to safely parse stringified JSON or
inspect objects directly for an `etag` property. This prevents false
positives on raw text payloads containing the word "etag" and false
negatives on object payloads.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api: storage Issues related to the Cloud Storage API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants