Skip to content
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

feat: add refreshTokenHandler option #7237

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .changeset/wise-spoons-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
"@vue-storefront/sdk": minor
---

[ADDED] new option to the `middlewareModule` - `refreshTokenHandler`.
This special handler can be used to handle 401 errors and refresh the token.
It is called before the generic `errorHandler`.
By default, it thrown an error which is being caught by the `errorHandler` and rethrown.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think that some small diagram (even ascii) with the sequence will be useful here. Or maybe we can somehow rephrase this sentence, I think I would not understand it without my knowledge about how it works. Something like an explanation that by default we are not able to predict the mechanism of token refresh because of differences in different services therefore we do not provide any default behavior, we simply push the error to the errorHandler without any further actions.


Example:

```ts
import { SdkHttpError } from "@vue-storefront/sdk";

const refereshToken = async () => {
// Refresh the token
};

const options: Options = {
apiUrl: "https://api.example.com",
refreshTokenHandler: async ({
error,
methodName,
url,
params,
config,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this a module config? Or what kind of config? Will I have some docs in TS? If not maybe lets explain the injected object here?

httpClient,
}) => {
try {
await refreshToken();
} catch (error) {
throw new SdkHttpError({
statusCode: 401,
message: "Unauthorized",
cause: error,
});
}

throw error;
},
};
```
46 changes: 41 additions & 5 deletions docs/content/4.sdk/2.getting-started/2.middleware-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,6 @@ You can add a custom error handler by passing it in the `errorHandler` option of

```ts
import { SdkHttpError } from "@vue-storefront/sdk";
import { refreshToken } from "./handlers/refreshToken"; // Custom implementation of the refresh token logic

const options: Options = {
apiUrl: "https://api.example.com",
Expand All @@ -349,11 +348,48 @@ const options: Options = {
config,
httpClient,
}) => {
if (error.status === 401 && methodName !== "login") {
// Refresh the token
const msg = `A request to ${url} with params ${JSON.stringify(params)} failed with an error: ${error.message}`;

console.error(msg);

throw new SdkHttpError({
statusCode: error.statusCode || 500,
message: msg,
cause: error,
});
},
};
```

### Add a custom refresh token handler

You can add a custom refresh token handler by passing it in the `refreshTokenHandler` option of the `middlewareModule` function.

```ts
import { SdkHttpError } from "@vue-storefront/sdk";

const refereshToken = async () => {
// Refresh the token
};

const options: Options = {
apiUrl: "https://api.example.com",
refreshTokenHandler: async ({
error,
methodName,
url,
params,
config,
httpClient,
}) => {
try {
await refreshToken();
// Retry the request
return httpClient(url, params, config);
} catch (error) {
throw new SdkHttpError({
statusCode: 401,
message: "Unauthorized",
cause: error,
});
}

throw error;
Expand Down
25 changes: 9 additions & 16 deletions packages/sdk/src/modules/middlewareModule/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,25 +275,18 @@ export type Options<
* If not provided, errors will be thrown as is.
*
* This enables custom error handling, like retrying the request or refreshing tokens, depending on the error type and details of the request that failed.
*/
errorHandler?: ErrorHandler;

/**
* Optional error handler for refreshing tokens.
*
* @example
* ```typescript
* const options: Options = {
* apiUrl: "https://api.example.com",
* errorHandler: async ({ error, methodName, url, params, config, httpClient }) => {
* if (error.status === 401 && methodName !== "login") {
* // Refresh token
* await refreshToken();
* // Retry the request
* return httpClient(url, params, config);
* }
* @remarks
*
* throw error;
* },
* };
* ```
* The refresh token handler is a specific error handler that is triggered when the request fails due to 401 Unauthorized error.
* It is going to be triggered before the `errorHandler`, which is more generic.
*/
errorHandler?: ErrorHandler;
refreshTokenHandler?: ErrorHandler;

/**
* Unique identifier for CDN cache busting.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
HTTPClient,
ErrorHandler,
RequestSender,
ErrorHandlerContext,
Logger,
} from "../types";
import { SdkHttpError } from "./SdkHttpError";
Expand Down Expand Up @@ -125,10 +126,15 @@ export const getRequestSender = (options: Options): RequestSender => {
throw error;
};

const defaultRefreshHandler: ErrorHandler = async ({ error }) => {
throw error;
};

return async (methodName, params, config?) => {
const {
httpClient = defaultHTTPClient,
errorHandler = defaultErrorHandler,
refreshTokenHandler = defaultRefreshHandler,
} = options;
const logger = getLogger(options.logger);
const { method, headers = {}, ...restConfig } = config ?? {};
Expand Down Expand Up @@ -160,14 +166,28 @@ export const getRequestSender = (options: Options): RequestSender => {
});
return response;
} catch (error) {
return await errorHandler({
error,
methodName,
url: finalUrl,
params: finalParams,
config: finalConfig,
httpClient,
});
try {
const handlerContext: ErrorHandlerContext = {
error,
methodName,
url: finalUrl,
params: finalParams,
config: finalConfig,
httpClient,
};
return await refreshTokenHandler(handlerContext);
} catch (innerError) {
const handlerContext: ErrorHandlerContext = {
error: innerError,
methodName,
url: finalUrl,
params: finalParams,
config: finalConfig,
httpClient,
};

return await errorHandler(handlerContext);
}
}
};
};
Loading