Skip to content

Conversation

@thiyaguk09
Copy link
Contributor

Description

This PR refactors the GCS conformance test suite to use the low-level StorageTransport.makeRequest architecture instead of high-level SDK abstractions. This change is necessary to ensure that the x-retry-test-id headers are correctly propagated through the transport layer for fault-injection testing.

Key Changes:

  • Transport Refactor: Updated conformance-test/libraryMethods.ts (specifically saveResumable, lock, and createBucket) to use the low-level transport.
  • Robust Bucket Creation: Implemented "Create-or-Get" logic in createBucket to handle HTTP 409 (Conflict) errors, ensuring tests are resilient to non-empty bucket cleanup failures.
  • Dynamic Metageneration: Updated the lock function to dynamically fetch current bucket metadata before applying retention locks, resolving metageneration mismatch errors.
  • Header Normalization: Enhanced StorageTransport to consistently lowercase all response header keys, ensuring case-insensitive lookups regardless of the underlying header object type.
  • Retry Logic Improvements: Refined the shouldRetry logic to better handle malformed JSON responses and specific GCS error reasons like rateLimitExceeded.

Impact

  • Reliability: Fixes several "Scenario 1" and "Scenario 7" failures in the conformance test suite caused by high-level abstraction interference.
  • Consistency: Aligns the Node.js library with the expected GCS Resumable Upload protocol and error-handling standards.
  • Maintainability: Provides a more predictable path for injecting test-specific headers without modifying the core public API.

Testing

  • Unit Tests: Added comprehensive test cases in test/storage-transport.ts to cover shouldRetry branches, including network-level transients (ECONNRESET), malformed JSON strings, and idempotent method logic.
  • Coverage: Increased line coverage for storage-transport.ts from ~73% to over 90% by targeting previously uncovered error-handling blocks.
  • Integration Tests: Verified against the local conformance test-bench.
  • Breaking Changes: None. This refactor only affects internal test utility execution and private transport processing.

Additional Information

The refactor addresses specific issues encountered when running the suite in Node 18+ environments where gaxios and google-auth-library response structures (specifically Headers) behaved differently than in Node 14.

Checklist

  • 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
  • Appropriate docs were updated
  • Appropriate comments were added, particularly in complex areas or places that require background
  • No new warnings or issues will be generated from this change

Fixes #issue_number_goes_here 🦕

All 720 test cases have been fixed, and the code has been refactored.
- Replace high-level bucket and file calls with
storageTransport.makeRequest.
- Fix Scenario 1 failures by implementing "create-or-get" logic for
buckets.
- Resolve metageneration mismatch in lock() by dynamically fetching
metadata.
- Normalize header keys to lowercase in transport response processing.
- Increase unit test coverage for shouldRetry logic and error handling.
@product-auto-label product-auto-label bot added size: xl Pull request size is extra large. api: storage Issues related to the googleapis/nodejs-storage API. labels Jan 12, 2026
- Fixed authentication headers/token exchange in the transport layer.
- Reverted to single-shot resumable upload to isolate Scenario 7
  failures while debugging mid-stream offset recovery.
@generated-files-bot
Copy link

Warning: This pull request is touching the following templated files:

  • .github/workflows/ci.yaml - .github/workflows/ci.yaml (GitHub Actions) should be updated in synthtool

@thiyaguk09 thiyaguk09 marked this pull request as ready for review January 13, 2026 10:27
@thiyaguk09 thiyaguk09 requested review from a team as code owners January 13, 2026 10:27
@thiyaguk09 thiyaguk09 force-pushed the node18/conformance-test branch from 0d23d09 to ed49471 Compare January 14, 2026 07:20
}

// Create a Proxy around rawStorageTransport to intercept makeRequest
storageTransport = new Proxy(rawStorageTransport, {
Copy link
Contributor

@ddelgrosso1 ddelgrosso1 Jan 14, 2026

Choose a reason for hiding this comment

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

I'm not sure I entirely understand why this is necessary. StorageTransport has an AuthClient which should have access to the underlying Gaxios instance. I previously had added request / response interceptors to Gaxios which would be the preferred way of doing this in my opinion.

export async function addLifecycleRuleInstancePrecondition(
options: ConformanceTestOptions,
) {
await options.bucket!.addLifecycleRule({
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we removing the options here and calling a different overload?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see the confusion. The addLifecycleRule function already handles the ifMetagenerationMatch logic via the preconditionRequired flag. I kept the addLifecycleRuleInstancePrecondition wrapper primarily to satisfy the test runner's expected entry point for this scenario. I'll ensure the options object is passed through fully without any property loss to maintain the correct overload behavior.

`${headers.get('x-goog-api-client')} gccl-gcs-cmd/${reqOpts[GCCL_GCS_CMD_KEY]}`,
);
}
if (reqOpts.interceptors) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe this is necessary. There are places in the code that an end user can set interceptors today. We do not want to remove that functionality.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I completely agree. I've ensured the logic preserves the interceptor chain so that user-defined interceptors are still registered. The current implementation clears the instance's previous state and re-applies the interceptors specifically from reqOpts to ensure that per-request configurations aren't lost or cross-pollinated between different storage operations.

this.gaxiosInstance.interceptors.request.add(inter);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const retryId = (reqOpts.headers as any)?.['x-retry-test-id'];
Copy link
Contributor

Choose a reason for hiding this comment

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

Retry test id should only be used in the conformance tests. I don't think we want that implementation detail leaking into storage transport.

Copy link
Contributor Author

@thiyaguk09 thiyaguk09 Jan 15, 2026

Choose a reason for hiding this comment

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

That’s a fair point. I’ve removed the explicit x-retry-test-id logic from StorageTransport to avoid leaking test-specific implementation details. Instead, I’ve ensured that the transport layer generically propagates all headers passed in via reqOpts.headers. This allows the conformance tests to pass the test ID without the production transport layer needing to know it exists.

noResponseRetries: this.retryOptions.maxRetries,
maxRetryDelay: this.retryOptions.maxRetryDelay,
retryDelayMultiplier: this.retryOptions.retryDelayMultiplier,
shouldRetry: this.retryOptions.retryableErrorFn,
Copy link
Contributor

Choose a reason for hiding this comment

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

retryableErrorFn is a user setable function. We don't want to remove it / re-code it below.

retryDelayMultiplier: this.retryOptions.retryDelayMultiplier,
shouldRetry: this.retryOptions.retryableErrorFn,
totalTimeout: this.retryOptions.totalTimeout,
shouldRetry: (err: GaxiosError) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

We shouldn't need all this logic to handle retries. Any retry configuration we need to do should be handed to Gaxios.

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 googleapis/nodejs-storage API. size: xl Pull request size is extra large.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants