Skip to content

Automatically retry partners requests that fail with 401 #5646

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

shauns
Copy link
Contributor

@shauns shauns commented Apr 14, 2025

WHY are these changes introduced?

To improve error handling in the Partners API client by adding support for token refresh when unauthorized responses are received.

WHAT is this pull request doing?

Adds an unauthorized handler to the Partners API client that automatically refreshes the token when an unauthorized response is received. This prevents API requests from failing due to expired tokens by:

  1. Adding an optional unauthorizedHandler parameter to partnersRequest and partnersRequestDoc functions
  2. Creating a handler in the PartnersClient class that refreshes the token when needed
  3. Updating tests to expect the new handler parameter

Measuring impact

We should see an improvement in our internal command success metrics.

Look out for

Do we need to increase the scope of this? Other services and such.

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've considered possible documentation changes

Copy link
Contributor Author

shauns commented Apr 14, 2025

This stack of pull requests is managed by Graphite. Learn more about stacking.

@shauns shauns marked this pull request as ready for review April 14, 2025 14:18
@shauns shauns requested a review from a team as a code owner April 14, 2025 14:18
Copy link
Contributor

github-actions bot commented Apr 14, 2025

Coverage report

St.
Category Percentage Covered / Total
🟡 Statements
76.56% (-0.11% 🔻)
9487/12392
🟡 Branches
71.71% (-0.05% 🔻)
4659/6497
🟡 Functions
76.39% (-0.06% 🔻)
2459/3219
🟡 Lines
77.09% (-0.11% 🔻)
8973/11639
Show files with reduced coverage 🔻
St.
File Statements Branches Functions Lines
🔴
... / partners-client.ts
25% (-0.66% 🔻)
28.57% (-3.01% 🔻)
18.03% (+1.08% 🔼)
24.68% (-0.66% 🔻)
🟢
... / api.ts
83.49% (-0.17% 🔻)
71.01% (+0.25% 🔼)
92.31% (-7.69% 🔻)
83.65% (+0.65% 🔼)
🟡
... / graphql.ts
80% (-20% 🔻)
60% (-11.43% 🔻)
90.91% (-9.09% 🔻)
79.59% (-20.41% 🔻)

Test suite run success

2199 tests passing in 959 suites.

Report generated by 🧪jest coverage report action from 0b76517

@shauns shauns force-pushed the shauns/04-14-automatically_retry_partners_requests_that_fail_with_401 branch from cf1a625 to 441eee5 Compare April 15, 2025 12:18
Copy link
Contributor

We detected some changes at packages/*/src and there are no updates in the .changeset.
If the changes are user-facing, run "pnpm changeset add" to track your changes and include them in the next release CHANGELOG.

@shauns shauns force-pushed the shauns/04-14-automatically_retry_partners_requests_that_fail_with_401 branch from 441eee5 to 0b76517 Compare April 16, 2025 15:48
Copy link
Contributor

Differences in type declarations

We detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

We found no new type declarations in this PR

Existing type declarations

packages/cli-kit/dist/private/node/api.d.ts
@@ -11,6 +11,13 @@ type RequestOptions<T> = {
     request: () => Promise<T>;
     url: string;
 } & NetworkRetryBehaviour;
+export interface UnauthorizedHandlerThrow {
+    action: 'throw';
+}
+type UnauthorizedHandlerContinue = {
+    action: 'continue';
+} | undefined;
+export type UnauthorizedHandlerResult = Promise<UnauthorizedHandlerThrow | UnauthorizedHandlerContinue>;
 export declare function simpleRequestWithDebugLog<T extends {
     headers: Headers;
     status: number;
@@ -35,7 +42,7 @@ export declare function simpleRequestWithDebugLog<T extends {
 export declare function retryAwareRequest<T extends {
     headers: Headers;
     status: number;
-}>(requestOptions: RequestOptions<T>, errorHandler?: (error: unknown, requestId: string | undefined) => unknown, unauthorizedHandler?: () => Promise<void>, retryOptions?: {
+}>(requestOptions: RequestOptions<T>, errorHandler?: (error: unknown, requestId: string | undefined) => unknown, unauthorizedHandler?: () => UnauthorizedHandlerResult, retryOptions?: {
     limitRetriesTo?: number;
     defaultDelayMs?: number;
     scheduleDelay: (fn: () => void, delay: number) => void;
packages/cli-kit/dist/public/node/api/graphql.d.ts
@@ -1,3 +1,4 @@
+import { UnauthorizedHandlerResult, UnauthorizedHandlerThrow } from '../../../private/node/api.js';
 import { ConfSchema, TimeInterval } from '../../../private/node/conf-store.js';
 import { LocalStorage } from '../local-storage.js';
 import { rawRequest, RequestDocument, Variables } from 'graphql-request';
@@ -16,6 +17,11 @@ export interface CacheOptions {
     cacheExtraKey?: string;
     cacheStore?: LocalStorage<ConfSchema>;
 }
+interface RefreshTokenOnAuthorizedResponseRetryWithNewToken {
+    action: 'retry';
+    token: string;
+}
+export type RefreshTokenOnAuthorizedResponse = Promise<UnauthorizedHandlerThrow | RefreshTokenOnAuthorizedResponseRetryWithNewToken>;
 interface GraphQLRequestBaseOptions<TResult> {
     api: string;
     url: string;
@@ -25,18 +31,19 @@ interface GraphQLRequestBaseOptions<TResult> {
     };
     responseOptions?: GraphQLResponseOptions<TResult>;
     cacheOptions?: CacheOptions;
+    refreshTokenOnAuthorizedResponse?: () => RefreshTokenOnAuthorizedResponse;
 }
 export type GraphQLRequestOptions<T> = GraphQLRequestBaseOptions<T> & {
     query: RequestDocument;
     variables?: Variables;
-    unauthorizedHandler?: () => Promise<void>;
+    unauthorizedHandler?: () => UnauthorizedHandlerResult;
 };
 export type GraphQLRequestDocOptions<TResult, TVariables> = GraphQLRequestBaseOptions<TResult> & {
     query: TypedDocumentNode<TResult, TVariables> | TypedDocumentNode<TResult, Exact<{
         [key: string]: never;
     }>>;
     variables?: TVariables;
-    unauthorizedHandler?: () => Promise<void>;
+    unauthorizedHandler?: () => UnauthorizedHandlerResult;
 };
 export interface GraphQLResponseOptions<T> {
     handleErrors?: boolean;
packages/cli-kit/dist/public/node/api/partners.d.ts
@@ -1,4 +1,4 @@
-import { GraphQLVariables, GraphQLResponse, CacheOptions } from './graphql.js';
+import { GraphQLVariables, GraphQLResponse, CacheOptions, RefreshTokenOnAuthorizedResponse } from './graphql.js';
 import { Variables } from 'graphql-request';
 import { TypedDocumentNode } from '@graphql-typed-document-node/core';
 /**
@@ -8,9 +8,10 @@ import { TypedDocumentNode } from '@graphql-typed-document-node/core';
  * @param token - Partners token.
  * @param variables - GraphQL variables to pass to the query.
  * @param cacheOptions - Cache options.
+ * @param refreshTokenOnAuthorizedResponse - Optional handler for unauthorized requests.
  * @returns The response of the query of generic type <T>.
  */
-export declare function partnersRequest<T>(query: string, token: string, variables?: GraphQLVariables, cacheOptions?: CacheOptions): Promise<T>;
+export declare function partnersRequest<T>(query: string, token: string, variables?: GraphQLVariables, cacheOptions?: CacheOptions, refreshTokenOnAuthorizedResponse?: () => RefreshTokenOnAuthorizedResponse): Promise<T>;
 export declare const generateFetchAppLogUrl: (cursor?: string, filters?: {
     status?: string;
     source?: string;
@@ -21,9 +22,10 @@ export declare const generateFetchAppLogUrl: (cursor?: string, filters?: {
  * @param query - GraphQL query to execute.
  * @param token - Partners token.
  * @param variables - GraphQL variables to pass to the query.
+ * @param refreshTokenOnAuthorizedResponse - Optional handler for unauthorized requests.
  * @returns The response of the query of generic type <TResult>.
  */
-export declare function partnersRequestDoc<TResult, TVariables extends Variables>(query: TypedDocumentNode<TResult, TVariables>, token: string, variables?: TVariables): Promise<TResult>;
+export declare function partnersRequestDoc<TResult, TVariables extends Variables>(query: TypedDocumentNode<TResult, TVariables>, token: string, variables?: TVariables, refreshTokenOnAuthorizedResponse?: () => RefreshTokenOnAuthorizedResponse): Promise<TResult>;
 /**
  * Sets the next deprecation date from [GraphQL response extensions](https://www.apollographql.com/docs/resources/graphql-glossary/#extensions)
  * if  objects contain a  (ISO 8601-formatted string).

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