diff --git a/.changeset/wise-spoons-draw.md b/.changeset/wise-spoons-draw.md new file mode 100644 index 0000000000..eccf6ef11f --- /dev/null +++ b/.changeset/wise-spoons-draw.md @@ -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. + +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, + httpClient, + }) => { + try { + await refreshToken(); + } catch (error) { + throw new SdkHttpError({ + statusCode: 401, + message: "Unauthorized", + cause: error, + }); + } + + throw error; + }, +}; +``` diff --git a/docs/content/4.sdk/2.getting-started/2.middleware-module.md b/docs/content/4.sdk/2.getting-started/2.middleware-module.md index 1f55fe5d62..d80fd88250 100644 --- a/docs/content/4.sdk/2.getting-started/2.middleware-module.md +++ b/docs/content/4.sdk/2.getting-started/2.middleware-module.md @@ -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", @@ -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; diff --git a/packages/sdk/src/modules/middlewareModule/types.ts b/packages/sdk/src/modules/middlewareModule/types.ts index 3481bb66fb..8fe81e8095 100644 --- a/packages/sdk/src/modules/middlewareModule/types.ts +++ b/packages/sdk/src/modules/middlewareModule/types.ts @@ -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. diff --git a/packages/sdk/src/modules/middlewareModule/utils/getRequestSender.ts b/packages/sdk/src/modules/middlewareModule/utils/getRequestSender.ts index cfd5f7a164..4ab6a6a153 100644 --- a/packages/sdk/src/modules/middlewareModule/utils/getRequestSender.ts +++ b/packages/sdk/src/modules/middlewareModule/utils/getRequestSender.ts @@ -6,6 +6,7 @@ import { HTTPClient, ErrorHandler, RequestSender, + ErrorHandlerContext, Logger, } from "../types"; import { SdkHttpError } from "./SdkHttpError"; @@ -135,10 +136,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 ?? {}; @@ -170,14 +176,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); + } } }; };