Murugesan
Murugesan

Reputation: 41

Handling Cookies and Axios Interceptors in Qwik JS SSR

I am developing a SaaS-based inventory application in Qwik JS with TypeScript. I use Axios to fetch data from a REST API and have implemented Axios interceptors to refresh the access token when it expires using the refresh token.

Since** Qwik JS is SSR**, cookies are not accessible in client mode. To authenticate protected API calls, I pass the cookie in the request header.

My approach:

I have a function axiosProtected, where I pass the cookie object as a parameter. On each call to this function, a singleton createAxiosInstance is invoked.

My concerns:

My implementation (code snippet):

import { type Cookie } from '@builder.io/qwik-city';
import type { AxiosError, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, AxiosInstance } from 'axios';
import type { ApiOptions, ResultResponse } from '../types';
import { APP_CONSTANTS, REFRESH_TOKEN, VIRUKSH_PUBLIC_URL } from '~/constants';
import { getAccessToken, refreshAuthToken, setToken } from './tokenService';
import axios from 'axios';


/**
 * Function to create axios instance to serve private requests with token in the header
 * Use interceptors.response to refresh the access token, if access token expired at any moment in between
 * @param cookieObj - Cookie
 * new token from interceptors.response set to cookie
 * update on 27-April-2024 - converted GET to POST
*/
export async function createAxiosInstance(cookieObj: Cookie) {
  const fetchAxiosClient: AxiosInstance = axios.create({
    baseURL: VIRUKSH_PUBLIC_URL,
    timeout: 60000
  });
  interface FailedRequests {
    resolve: (value: AxiosResponse) => void;
    reject: (value: AxiosError) => void;
    config: AxiosRequestConfig;
    error: AxiosError;
  }
  // Add the auth token to every request
  fetchAxiosClient.interceptors.request.use(
    (config: InternalAxiosRequestConfig) => {
      const token = getAccessToken(cookieObj).access_token;
      if (token) {
        config.headers['Authorization'] = `Bearer ${token}`;
      }
      config.headers['Content-Type'] = 'application/json';
      return config;
    },
    error => Promise.reject(error)
  );


  let failedRequests: FailedRequests[] = [];
  let isTokenRefreshing = false;

  fetchAxiosClient.interceptors.response.use(
    function (response: AxiosResponse) {
      return response;
    },
    async (error: AxiosError) => {

      const status = error.response?.status;
      //const data = error.response?.data as AxiosFecthError;
      //if (status !== 401 || data.code === 'ERR10088'|| data.code === 'ERR10086') {
      if (status !== 401) {
        return Promise.reject(error);
      }
      const originalRequestConfig = error.config!;
      if (isTokenRefreshing) {
        return new Promise((resolve, reject) => {
          failedRequests.push({resolve, reject, config: originalRequestConfig,
            error: error,
          });
        });
      }
      isTokenRefreshing = true;
      try {
        const { access_token, refresh_token } = await refreshAuthToken(cookieObj);
        setToken(cookieObj, access_token!);
        setToken(cookieObj, refresh_token!);
        failedRequests.forEach(({ resolve, reject, config }) => {
          fetchAxiosClient(config)
            .then((response) => resolve(response))
            .catch((error) => reject(error));
        });
      } catch (_error: unknown) {
        console.error(_error);
        failedRequests.forEach(({ reject, error }) => reject(error));
        cookieObj.delete(APP_CONSTANTS.ACCESS_TOKEN, { path: '/' });
        return Promise.reject(error);
      } finally {
        failedRequests = [];
        isTokenRefreshing = false;
      }
      return fetchAxiosClient(originalRequestConfig);
    }
  );
  return fetchAxiosClient;
}

/**
 * axios post, put, get, delete
 * to access protected api we need to share AccessToken in the headers
 * @param cookie - Cookie object to get Access token
 * @returns Promise<T>
 */
export async function axiosProtected<T>(options: ApiOptions, cookieObj: Cookie): Promise<ResultResponse<T>> {
  const { endpoint, method, headers, body, signal } = options;
  const apiUrl = `${VIRUKSH_PUBLIC_URL}${endpoint}`;
  const requestHeaders: Record<string, string> = {
    "Content-Type": "application/json",
    'Accept': 'application/json',
    ...(headers || {}),
  };
  const fetchAxiosClient = await createAxiosInstance(cookieObj);
  const response = await fetchAxiosClient!({
    signal: signal,
    method: method,
    url: apiUrl,
    headers: requestHeaders,
    data: body ? JSON.stringify(body) : undefined,
  }).then((response) => {
    if (response.data.code === 200) {
      const data = response.data as T;
      return data
    } else {
      return {
        code: response.data.response.code,
        response: undefined,
        success: false,
        message: response.data.response.message,
        error: new Error(response.data.response.error),
        title: response.data.response.title,
      } as T
    }
  }).catch((exception: any) => {
    switch (exception.response.status) {
      case 404:
        return {
          code: "10404",
          response: undefined,
          success: false,
          message: "Page not found",
          error: exception,
          title: "Page not found",
        } as T
      case 400:
        return {
          code: exception.response.data.code,
          response: undefined,
          success: exception.response.data.success,
          message: exception.response.data.message,
          error: exception,
          title: exception.response.data.title,
        } as T
      default:
        return {
          code: exception.response.data.code ? exception.response.data.code : "10144",
          response: exception.response.data.response ? exception.response.data.response : undefined,
          success: false,
          message: exception.response.data.message ? exception.response.data.message : "Server not ready",
          error: exception.response.data.error ? exception.response.data.error : exception,
          title: exception.response.data.title ? exception.response.data.title : "Server error",
        } as T
    }
  });
  return response;
}

I would appreciate any insights on improving this implementation, especially considering Qwik’s SSR nature.

Let me know, if this approach is correct?

Upvotes: 0

Views: 19

Answers (0)

Related Questions