Issue Description
Working on a Next.js project where the server-side fetch requests need to handle token refresh when the accessToken expires. My server responds with a new accessToken in an httpOnly and secure cookie after refreshing the token. However, I am encountering an issue where the subsequent fetch request after the token refresh still uses the old cookies and doesn’t include the newly refreshed accessToken.
Flow:
- The app fetches user information on the server side.
- When the accessToken expires, the API returns an error with the message ERR_TOKEN_EXPIRED, triggering the _handleError function.
- Inside _handleError, I make a call to refresh the accessToken using this.request(apis.authn.refreshAccessToken, {}).
- The refresh API successfully returns a 200 status code, and the new token is set as an httpOnly cookie.
- However, when the original request is retried (after the refresh), it still uses the old cookies, and the new accessToken is not included in the request.
(Seem next/headers cookie is not autoupdated)
Using “next”: “14.2.5”
export class HttpClient {
private _handleRequest = (req: RequestInit) => {
if (!isBrowser()) {
req.headers = {
Cookie: require('next/headers').cookies().toString(),
};
}
};
async request(api: any, { data }: { data?: any}) {
let url = api.url;
let requestData = data ? JSON.stringify(data) : null;
let requestInit: RequestInit = {
credentials: 'include',
method: api.method,
body: requestData,
headers: {
'Content-Type': 'application/json',
},
};
this._handleRequest(requestInit);
try {
const response = await fetch(`${this.baseURL}${url}`, requestInit);
return this._handleResponse(response);
} catch (err) {
return this._handleError(err, requestInit, url);
}
}
protected async _handleError(
err: any,
originalRequest: RequestInit,
url: string
) {
if (err.status === 401) {
try {
const refreshResponse = await this.request(apis.authn.refreshAccessToken, {});
if (refreshResponse.success) {
return await fetch(`${this.baseURL}${url}`, originalRequest)
.then(this._handleResponse)
.then((originalResponse: any) => originalResponse.data);
}
} catch (error) {
return Promise.reject(error);
}
}
return Promise.reject(err);
}
}