OiO.lk Community platform!

Oio.lk is an excellent forum for developers, providing a wide range of resources, discussions, and support for those in the developer community. Join oio.lk today to connect with like-minded professionals, share insights, and stay updated on the latest trends and technologies in the development field.
  You need to log in or register to access the solved answers to this problem.
  • You have reached the maximum number of guest views allowed
  • Please register below to remove this limitation

Retrying requests where the response is 401 with axios interceptors [duplicate]

  • Thread starter Thread starter Igor Shmukler
  • Start date Start date
I

Igor Shmukler

Guest
The below code, implement the basic REST API client.

Code:
export class BaseAPI<TRequest extends OAuthTokenRequest, THeader extends OAuthTokenRequestHeaders> {
  protected readonly configuration: ApiConfiguration<TRequest, THeader>
  protected readonly axiosInstance: AxiosInstance
  protected currentToken?: OAuthTokenResponse
  private expirationBuffer: number

  protected constructor(config: ApiConfiguration<TRequest, THeader>) {
    this.configuration = config

    this.axiosInstance = axios.create({ timeout: config.requestTimeout })
    // Setup an interceptor to ensure any request done through this Axios instance
    // will always have the Authorization header set with a valid token
    /* const interceptor = */ this.axiosInstance.interceptors.request.use(this.interceptorSetToken.bind(this))
    this.axiosInstance.interceptors.request.use(
      (response) => {
        return response
      },
      (error) => {
        if (error.response.status === 401) {
          this.currentToken = undefined
          // this.axiosInstance.interceptors.response.eject(interceptor)
        }
      }
    )

    this.expirationBuffer = parseInt(getEnvVars().OAUTH_TOKEN_EXPIRE_BUFFER?.trim() || DEFAULT_EXPIRATION_BUFFER, 10)
  }

  private async interceptorSetToken(config: AxiosRequestConfig): Promise<AxiosRequestConfig> {
    if (!this.tokenIsValid()) {
      LOGGER.debug('Token is not set or is expired. Requesting a new one...: %j', this.currentToken)
      this.currentToken = await this.getToken()
      LOGGER.debug('Token requested successfully: %j', this.currentToken)
    }

    const newHeaders = config.headers ?? {}
    newHeaders['Authorization'] = `Bearer ${this.currentToken?.access_token}`
    config.headers = newHeaders
    return config
  }

  private tokenIsValid(): boolean {
    if (!this.currentToken) {
      return false
    }

    const currentTime = moment().unix() + this.expirationBuffer
    const isExpired = currentTime > parseInt(this.currentToken.expires_on, 10)

    return !isExpired
  }

  private async getToken(): Promise<OAuthTokenResponse> {
    const {
      oAuthTokenEndpoint,
      request: { client_id, client_secret, grant_type, ...customParams },
    } = this.configuration

    const { contentType, ...customHeaders } = this.configuration.header || {}

    const tokenRequest: any = {
      client_id,
      client_secret,
      grant_type: grant_type || 'client_credentials',
      ...customParams,
    }

    const headerParams = {
      'Content-Type': contentType || API_FORM_URL_ENCODED_CONTENT_TYPE,
      ...customHeaders,
    }

    // Need to use the "axios" default instance because this is called inside a request interceptor. If not, we will get
    // an infinite loop
    const response = await axios.post(oAuthTokenEndpoint, qs.stringify(tokenRequest), { headers: headerParams })
    const tokenResponse: OAuthTokenResponse = response.data as OAuthTokenResponse

    if (!tokenResponse.expires_on && tokenResponse.expires_in) {
      LOGGER.info('Received token without expires_on.  Calculating expires_on based on expires_in value', {
        expires_in: tokenResponse.expires_in,
        expires_on: String(moment().unix() + tokenResponse.expires_in),
      })

      // calculate the expires_on value
      return {
        ...tokenResponse,
        expires_on: String(moment().unix() + tokenResponse.expires_in),
      }
    }
    return tokenResponse
  }

  private getHeaders = (headers: any) => {
    const { contentType, ...customHeaders } = headers || {}

    const headerParams = {
      'Content-Type': contentType || 'application/json',
      ...customHeaders,
    }

    return headerParams
  }

  protected async delete<TResponse>(url: string, headers?: any): Promise<TResponse> {
    const headerParams = this.getHeaders(headers)

    const response = await this.axiosInstance.delete(url, {
      headers: headerParams,
    })

    return response.data
  }

  protected async get<TResponse>(url: string, headers?: any): Promise<TResponse> {
    const headerParams = this.getHeaders(headers)

    const response = await this.axiosInstance.get(url, {
      headers: headerParams,
    })

    return response.data
  }

  protected async patch<TResponse>(url: string, data: any, headers?: any): Promise<TResponse> {
    const headerParams = this.getHeaders(headers)

    const response = await this.axiosInstance.patch(url, data, {
      headers: headerParams,
    })

    return response.data
  }

  protected async post<TResponse>(url: string, data: any, headers?: any): Promise<TResponse> {
    const headerParams = this.getHeaders(headers)

    const response = await this.axiosInstance.post(url, data, {
      headers: headerParams,
    })

    return response.data
  }

  protected async put<TResponse>(url: string, data: any, headers?: any): Promise<TResponse> {
    const headerParams = this.getHeaders(headers)

    const response = await this.axiosInstance.put(url, data, {
      headers: headerParams,
    })

    return response.data
  }
}

It always had interceptorSetToken to retrieve a new token. Unfortunately, sometimes, I am seeing some of my clients built on top return 401 for long-lived tokens until those expire, despite getting 401 many times in the row.

I want to add another interceptor that would do two things:

  1. clear expired token
  2. retry the failed request

I added second interceptor to address the issue. Did not get a chance to test it, yet. Hopefully, what I got so far would clear the token.

However, how would I implement the re-try of the HTTP request that resulted in 401, in the first place, hopefully invoking the interceptor that retrieves a new token?

<p>The below code, implement the basic REST API client.</p>
<pre><code>export class BaseAPI<TRequest extends OAuthTokenRequest, THeader extends OAuthTokenRequestHeaders> {
protected readonly configuration: ApiConfiguration<TRequest, THeader>
protected readonly axiosInstance: AxiosInstance
protected currentToken?: OAuthTokenResponse
private expirationBuffer: number

protected constructor(config: ApiConfiguration<TRequest, THeader>) {
this.configuration = config

this.axiosInstance = axios.create({ timeout: config.requestTimeout })
// Setup an interceptor to ensure any request done through this Axios instance
// will always have the Authorization header set with a valid token
/* const interceptor = */ this.axiosInstance.interceptors.request.use(this.interceptorSetToken.bind(this))
this.axiosInstance.interceptors.request.use(
(response) => {
return response
},
(error) => {
if (error.response.status === 401) {
this.currentToken = undefined
// this.axiosInstance.interceptors.response.eject(interceptor)
}
}
)

this.expirationBuffer = parseInt(getEnvVars().OAUTH_TOKEN_EXPIRE_BUFFER?.trim() || DEFAULT_EXPIRATION_BUFFER, 10)
}

private async interceptorSetToken(config: AxiosRequestConfig): Promise<AxiosRequestConfig> {
if (!this.tokenIsValid()) {
LOGGER.debug('Token is not set or is expired. Requesting a new one...: %j', this.currentToken)
this.currentToken = await this.getToken()
LOGGER.debug('Token requested successfully: %j', this.currentToken)
}

const newHeaders = config.headers ?? {}
newHeaders['Authorization'] = `Bearer ${this.currentToken?.access_token}`
config.headers = newHeaders
return config
}

private tokenIsValid(): boolean {
if (!this.currentToken) {
return false
}

const currentTime = moment().unix() + this.expirationBuffer
const isExpired = currentTime > parseInt(this.currentToken.expires_on, 10)

return !isExpired
}

private async getToken(): Promise<OAuthTokenResponse> {
const {
oAuthTokenEndpoint,
request: { client_id, client_secret, grant_type, ...customParams },
} = this.configuration

const { contentType, ...customHeaders } = this.configuration.header || {}

const tokenRequest: any = {
client_id,
client_secret,
grant_type: grant_type || 'client_credentials',
...customParams,
}

const headerParams = {
'Content-Type': contentType || API_FORM_URL_ENCODED_CONTENT_TYPE,
...customHeaders,
}

// Need to use the "axios" default instance because this is called inside a request interceptor. If not, we will get
// an infinite loop
const response = await axios.post(oAuthTokenEndpoint, qs.stringify(tokenRequest), { headers: headerParams })
const tokenResponse: OAuthTokenResponse = response.data as OAuthTokenResponse

if (!tokenResponse.expires_on && tokenResponse.expires_in) {
LOGGER.info('Received token without expires_on. Calculating expires_on based on expires_in value', {
expires_in: tokenResponse.expires_in,
expires_on: String(moment().unix() + tokenResponse.expires_in),
})

// calculate the expires_on value
return {
...tokenResponse,
expires_on: String(moment().unix() + tokenResponse.expires_in),
}
}
return tokenResponse
}

private getHeaders = (headers: any) => {
const { contentType, ...customHeaders } = headers || {}

const headerParams = {
'Content-Type': contentType || 'application/json',
...customHeaders,
}

return headerParams
}

protected async delete<TResponse>(url: string, headers?: any): Promise<TResponse> {
const headerParams = this.getHeaders(headers)

const response = await this.axiosInstance.delete(url, {
headers: headerParams,
})

return response.data
}

protected async get<TResponse>(url: string, headers?: any): Promise<TResponse> {
const headerParams = this.getHeaders(headers)

const response = await this.axiosInstance.get(url, {
headers: headerParams,
})

return response.data
}

protected async patch<TResponse>(url: string, data: any, headers?: any): Promise<TResponse> {
const headerParams = this.getHeaders(headers)

const response = await this.axiosInstance.patch(url, data, {
headers: headerParams,
})

return response.data
}

protected async post<TResponse>(url: string, data: any, headers?: any): Promise<TResponse> {
const headerParams = this.getHeaders(headers)

const response = await this.axiosInstance.post(url, data, {
headers: headerParams,
})

return response.data
}

protected async put<TResponse>(url: string, data: any, headers?: any): Promise<TResponse> {
const headerParams = this.getHeaders(headers)

const response = await this.axiosInstance.put(url, data, {
headers: headerParams,
})

return response.data
}
}
</code></pre>
<p>It always had <code>interceptorSetToken</code> to retrieve a new token. Unfortunately, sometimes, I am seeing some of my clients built on top return 401 for long-lived tokens until those expire, despite getting <code>401</code> many times in the row.</p>
<p>I want to add another interceptor that would do two things:</p>
<ol>
<li>clear expired token</li>
<li>retry the failed request</li>
</ol>
<p>I added second interceptor to address the issue. Did not get a chance to test it, yet. Hopefully, what I got so far would clear the token.</p>
<p>However, how would I implement the re-try of the HTTP request that resulted in 401, in the first place, hopefully invoking the interceptor that retrieves a new token?</p>
 

Latest posts

А
Replies
0
Views
1
Али-Мухаммад Закарьяев
А
M
Replies
0
Views
1
Marcos R. Guevara
M
M
Replies
0
Views
1
Marcos R. Guevara
M
Top