Skip to content

Conversation

@thiyaguk09
Copy link
Owner

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:

Description

Please provide a detailed description for the change.
As much as possible, please try to keep changes separate by purpose. For example, try not to make a one-line bug fix in a feature request, or add an irrelevant README change to a bug fix.

Impact

What's the impact of this change?

Testing

Have you added unit and integration tests if necessary?
Were any tests changed? Are any breaking changes necessary?

Additional Information

Any additional details that we should be aware of?

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 🦕

@gemini-code-assist
Copy link

Summary of Changes

Hello @thiyaguk09, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly refactors the conformance test suite by shifting many test methods to directly interact with the StorageTransport layer. This change allows for more precise control over HTTP requests, which is crucial for thoroughly testing retry mechanisms and other low-level API behaviors. Additionally, it enhances the robustness of the default retry logic and improves error handling within the transport layer.

Highlights

  • Conformance Test Refactoring: Many conformance test methods for bucket, file, HMAC key, IAM, notification, and storage operations have been refactored to directly use StorageTransport.makeRequest instead of the higher-level Storage client methods. This provides more granular control over API requests for testing purposes.
  • Retry Logic Enhancement: The default retryable error function (RETRYABLE_ERR_FN_DEFAULT) has been improved to include additional connection-related error codes (e.g., etimedout, econnrefused) and to check for specific retryable reasons within nested error structures (e.g., ratelimitexceeded).
  • Request Interception for Tests: A Proxy is now used for StorageTransport within conformance tests to intercept makeRequest calls and automatically inject the x-retry-test-id header, streamlining the testing of retry scenarios.
  • Dependency Update: The html-entities dependency has been removed from package.json.
  • StorageTransport Improvements: The StorageTransport.makeRequest method now explicitly passes queryParameters to gaxios and sets validateStatus to only consider 2xx responses as successful. It also includes enhanced error logging for GaxiosError instances.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@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 the conformance tests to interact with the transport layer directly, which is a solid approach for testing retry logic. The changes also include improvements to the default retry function in the main library. The overall direction is good, but I've identified a few issues, primarily concerning error handling, type safety, and a bug in header manipulation within the test suite. Addressing these will improve the robustness and reliability of the tests.

@thiyaguk09
Copy link
Owner Author

/gemini review

Copy link

@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 the conformance tests to use the transport layer directly, allowing for more granular testing of retry logic. The changes are extensive and mostly involve rewriting library methods to make direct REST API calls. I've found a couple of critical issues in the new test setup logic that could lead to runtime errors or incorrect behavior. Additionally, there are a few instances of commented-out code that should be removed for cleanliness. The improvements to the retryable error detection function in src/storage.ts are well-done and increase robustness.

@thiyaguk09
Copy link
Owner Author

/gemini review

Copy link

@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 introduces significant refactoring to the conformance tests, primarily to align with Node.js 18 and to test retry logic at the transport layer. The changes involve bypassing high-level SDK methods in favor of direct calls to storageTransport.makeRequest, implementing a complex custom retry strategy, and updating various test helper functions. My review focuses on improving type safety, removing leftover debugging code, and enhancing code clarity in these new implementations.

Comment on lines 121 to 200
export async function create(options: ConformanceTestOptions) {
const [bucketExists] = await options.bucket!.exists();
if (!options.storageTransport || !options.projectId || !options.bucket) {
throw new Error(
'storageTransport, projectId, and bucket are required for the create test.',
);
}
const bucketName = options.bucket.name;
let bucketExists = false;
try {
const existsReq: StorageRequestOptions = {
method: 'GET',
url: `storage/v1/b/${bucketName}`,
};
await options.storageTransport.makeRequest(existsReq);
bucketExists = true;
} catch (error: unknown) {
const gaxiosError = error as GaxiosError;
if (gaxiosError.response?.status === 404) {
console.log(`Bucket ${bucketName} does not exist.`);
} else {
console.warn(`Error checking existence of ${bucketName}:`, gaxiosError);
throw error;
}
}

if (bucketExists) {
await options.bucket!.deleteFiles();
await options.bucket!.delete({
ignoreNotFound: true,
});
let pageToken: string | undefined = undefined;
do {
const listReq: StorageRequestOptions = {
method: 'GET',
url: `storage/v1/b/${bucketName}/o`,
queryParameters: pageToken ? {pageToken} : undefined,
};
try {
const listResult = await options.storageTransport.makeRequest(listReq);
const objects = (listResult as any)?.items || [];

for (const obj of objects) {
const deleteObjReq: StorageRequestOptions = {
method: 'DELETE',
url: `storage/v1/b/${bucketName}/o/${obj.name}`,
};
try {
await options.storageTransport.makeRequest(deleteObjReq);
} catch (deleteErr: unknown) {
console.warn(`Error deleting object ${obj.name}:`, deleteErr);
}
}
pageToken = (listResult as any)?.nextPageToken;
} catch (listErr: unknown) {
console.error(
`Error listing objects in bucket ${bucketName}:`,
listErr,
);
throw listErr;
}
} while (pageToken);

const deleteBucketReq: StorageRequestOptions = {
method: 'DELETE',
url: `storage/v1/b/${bucketName}`,
};
try {
await options.storageTransport.makeRequest(deleteBucketReq);
} catch (deleteErr: unknown) {
const gaxiosError = deleteErr as GaxiosError;
if (gaxiosError.response?.status !== 404) {
throw deleteErr;
}
}
}
await options.bucket!.create();

const createRequest: StorageRequestOptions = {
method: 'POST',
url: `storage/v1/b?project=${options.projectId}`,
body: JSON.stringify({name: bucketName}),
headers: {'Content-Type': 'application/json'},
};
await options.storageTransport.makeRequest(createRequest);
}

Choose a reason for hiding this comment

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

medium

This function has a few areas for improvement:

  1. It contains several console.log, console.warn, and console.error statements (e.g., lines 139, 141, 166, 171). This logging seems intended for debugging and should be removed from the final test code to keep the test output clean.
  2. It uses any casts in multiple places (e.g., lines 156, 169) to access properties on API responses. This bypasses TypeScript's type safety. To improve robustness, please consider defining interfaces for the expected response shapes.

Comment on lines +434 to +435
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(data as any).headers = plainHeaders;

Choose a reason for hiding this comment

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

medium

Attaching response headers directly to the data object via (data as any).headers = plainHeaders; is not type-safe and can be considered an unexpected side effect. While this is likely a workaround to pass headers to higher-level functions for the conformance tests, a cleaner approach would be to return a more structured object from makeRequest. If that's too large of a change, consider adding a TODO comment explaining this pattern or defining a specific response type that includes headers to avoid using any.

@thiyaguk09
Copy link
Owner Author

/gemini review

Copy link

@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 the conformance tests to use a lower-level transport layer for making requests, which provides better control for retry tests. The changes also include a more sophisticated and specific retry logic in the StorageTransport. The overall direction is good, but I've found a few areas for improvement: some test helper functions are still using the old SDK methods, and there's a potential bug in header handling within the transport layer. Additionally, the new retry logic could be refactored for better readability.

Comment on lines +167 to +321
shouldRetry: (err: GaxiosError) => {
const status = err.response?.status;
const errorCode = err.code?.toString();
const retryableStatuses = [408, 429, 500, 502, 503, 504];
const nonRetryableStatuses = [401, 405, 412];

const isMalformedResponse =
err.message?.includes('JSON') ||
err.message?.includes('Unexpected token <') ||
(err.stack && err.stack.includes('SyntaxError'));
if (isMalformedResponse) return true;

if (status && nonRetryableStatuses.includes(status)) return false;

const params = reqOpts.queryParameters || {};
const hasPrecondition =
params.ifGenerationMatch !== undefined ||
params.ifMetagenerationMatch !== undefined ||
params.ifSourceGenerationMatch !== undefined;

const isPost = reqOpts.method?.toUpperCase() === 'POST';
const isPatch = reqOpts.method?.toUpperCase() === 'PATCH';
const isPut = reqOpts.method?.toUpperCase() === 'PUT';
const isGet = reqOpts.method?.toUpperCase() === 'GET';
const isHead = reqOpts.method?.toUpperCase() === 'HEAD';

const isIam = urlString.includes('/iam');
const isAcl = urlString.includes('/acl');
const isHmacRequest = urlString.includes('/hmacKeys');
const isNotificationRequest = urlString.includes(
'/notificationConfigs',
);

// Logic for Mutations (POST, PATCH, DELETE)
if (isPost || isPatch || isDelete) {
const isRetryTest = urlString.includes('retry-test-id');
if (isPost && isAcl) {
if (isRetryTest) {
return status ? retryableStatuses.includes(status) : false;
}
return false;
}
if (isPost && (isHmacRequest || isNotificationRequest))
return false;

const isBucketCreate =
isPost &&
urlString.includes('/v1/b') &&
!urlString.includes('/o');
const isSafeDelete = isDelete && !urlString.includes('/o/');

if (!hasPrecondition) {
if (!isBucketCreate && !isSafeDelete) {
if (urlString.includes('uploadType=resumable') && isPost) {
return !!status && retryableStatuses.includes(status);
}
return false;
}
}

if (status === undefined) {
const isResumable = urlString.includes('uploadType=resumable');

if (isResumable) return false;
return hasPrecondition || isBucketCreate || isSafeDelete;
}

return retryableStatuses.includes(status);
}

if (isPut) {
const url = err.config?.url.toString() || '';
if (isHmacRequest) {
try {
const body =
typeof reqOpts.body === 'string'
? JSON.parse(reqOpts.body)
: reqOpts.body;

if (!body || !body.etag) {
return false;
}
} catch (e) {
return false;
}
} else if (isIam) {
try {
let hasIamPrecondition = false;
const bodyStr =
typeof reqOpts.body === 'string'
? reqOpts.body
: reqOpts.body instanceof Buffer
? reqOpts.body.toString()
: '';
hasIamPrecondition = !!JSON.parse(bodyStr || '{}').etag;
if (!hasIamPrecondition) {
return false;
}
return status === undefined || status === 503;
} catch (e) {
return false;
}
} else if (url.includes('upload_id=')) {
if (!status || retryableStatuses.includes(status)) {
return true;
}
return false;
}
}

// Logic for Idempotent Methods (GET, PUT, HEAD)
const isIdempotentMethod = isGet || isHead || isPut;
if (isIdempotentMethod) {
if (status === undefined) {
return true;
}
return retryableStatuses.includes(status);
}

if (
isDelete &&
!hasPrecondition &&
!isNotificationRequest &&
!isHmacRequest
)
return false;

const transientNetworkErrors = [
'ECONNRESET',
'ETIMEDOUT',
'EADDRINUSE',
'ECONNREFUSED',
'EPIPE',
'ENOTFOUND',
'ENETUNREACH',
];
if (errorCode && transientNetworkErrors.includes(errorCode))
return true;

const data = err.response?.data;
if (data && data.error && Array.isArray(data.error.errors)) {
for (const e of data.error.errors) {
const reason = e.reason;
if (
reason === 'rateLimitExceeded' ||
reason === 'userRateLimitExceeded' ||
(reason && reason.includes('EAI_AGAIN'))
) {
return true;
}
}
}
if (!status) return true;
return status ? retryableStatuses.includes(status) : false;
},

Choose a reason for hiding this comment

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

medium

The new shouldRetry function is very large and implements complex retry logic. To improve readability and maintainability, consider refactoring this function by breaking it down into smaller, more focused helper functions based on the request type (e.g., for mutations, for idempotent requests).

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

2 participants