I have a fetch block which integrates to an API and sets the bearer token in the headers. Sometimes when calling the API the bearer token is expired, in this scenario I need to handle the 401 response in catch block by calling a refresh endpoint, and set the access token value in a cookie. After which I can attempt to call the API again by using the new token set in the cookie.
I have the following fetch block
fetch(host + '/api/v1/list', {
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": "Bearer " + getBearerToken()
}
}).then((response) => {
return response.json();
}).then((data) => {
return data.items;
}).catch(response => {
return fetch(host + 'refresh', {
method: 'POST',
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": "Bearer " + getBearerToken()
}
});
}).then((response) => {
return response.json();
}).then((data) => {
let token = data['Access_token'];
document.cookie="SSO_token=" + token + '; expires=0; path=/';
return fetch(host + '/api/v1/list', {
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": "Bearer " + getBearerToken()
}
});
}).then((response) => {
return response.json();
}).then((data) => {
return data.items;
})
The console reports a TypeError in the first call to then after the catch
}).catch(response => {
return fetch(host + 'refresh', {
method: 'POST',
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": "Bearer " + getBearerToken()
}
});
}).then((response) => {
return response.json();
})
response.json is not a function
. I’m not understanding the error here. Any help appreciated
The immediate issue is the following sequence
(shortened for brevity)
fetch(/* data */)
.then((response) => { return response.json(); }) // <--+
.then((data) => { return data.items; }) // |
.catch(response => { return fetch(/* bearer token */); }) // |
.then((response) => { return response.json(); }) // <--+
This would only work if the first fetch()
fails which would then jump over the first response.json()
, go to the .catch()
and then continue with the next response.json()
.
However, if the first fetch()
succeeds, then the .catch()
would be passed over, as there is no error. This leads to effectively the following code:
const demoResponse = {
json() {
return { items: 42 };
}
}
const demoFetch = () => {
return Promise.resolve(demoResponse);
}
demoFetch(/* data */)
.then((response) => { return response.json(); }) // calling .json() on the response
.then((data) => { return data.items; }) // getting some data.
//No .catch() because there is no error
.then((response) => { return response.json(); }) // calling `.json()` on the previous result
.catch((error) => console.error("and this would always fail", error.message))
Taking a step back, the problem that led to this one is trying to do so many operations in the same sequence of promises. Much easier is to define what needs to be done. Something like
function makeRequest() {
return fetch(host + '/api/v1/list', {
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": "Bearer " + getBearerToken()
}
});
}
function refreshToken() {
return fetch(host + 'refresh', {
method: 'POST',
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": "Bearer " + getBearerToken()
}
}).then((response) => {
return response.json();
}).then((data) => {
let token = data['Access_token'];
document.cookie="SSO_token=" + token + '; expires=0; path=/';
})
}
Which can then more easily be composed together and would be more readable and easier to make sure it is correct:
makeRequest()
.catch(() => { return refreshToken(); } ) // attempt recovery...
.then(() => { return makeRequest(); }) // ...and retry
.then((response) => { return response.json(); })
.then((data) => { return data.items; })
With this code the token refresh is only attempted if the request fails and only once. Moreover, the refresh is itself in a separate function, so it does not need to be handled specially in the promise chain. Including it anywhere will mean the entire refresh process is done before continuing, without needing to try and handle the response of the refresh in the same promise chain as handling the other responses.
The second request can still fail two times in a row. Presumably this would not be due to the access token but for some other reason. At any rate, it is advised to handle other possible failures appropriately.
The second .then(response => …)
is called with either the result of the .catch()
callback (i.e. the refetch response) or – if there was no error in the first place – with the result of the previous then
callback (i.e. the data.items
), and the latter value has no .json()
method.
You want to run all of that code only if an error occurred, so you need to nest that entire promise chain inside the catch
callback:
fetch(host + '/api/v1/list', {
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": "Bearer " + getBearerToken()
}
}).then((response) => {
return response.json();
}).then((data) => {
return data.items;
}).catch(response => {
return fetch(host + 'refresh', {
method: 'POST',
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": "Bearer " + getBearerToken()
}
}).then((response) => {
return response.json();
}).then((data) => {
let token = data['Access_token'];
document.cookie="SSO_token=" + token + '; expires=0; path=/';
return fetch(host + '/api/v1/list', {
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": "Bearer " + getBearerToken()
}
});
}).then((response) => {
return response.json();
}).then((data) => {
return data.items;
});
});
However, the catch
is only handling network errors and json parsing errors, but not the http errors that you actually want to handle by retrying the request with a new token. To achieve that, you need to explicitly check the status of the response:
fetch(host + '/api/v1/list', {
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": "Bearer " + getBearerToken()
}
}).then((response) => {
if (response.ok) {
return response.json().then((data) => {
return data.items;
});
} else if (response.status == 401) {
return fetch(host + 'refresh', {
method: 'POST',
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": "Bearer " + getBearerToken()
}
}).then((response) => {
if (response.ok) {
return response.json();
} else {
throw new Error(response.statusText);
}
}).then((data) => {
let token = data['Access_token'];
document.cookie="SSO_token=" + token + '; expires=0; path=/';
return fetch(host + '/api/v1/list', {
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": "Bearer " + getBearerToken()
}
});
}).then((response) => {
if (response.ok) {
return response.json();
} else {
throw new Error(response.statusText);
}
}).then((data) => {
return data.items;
});
} else {
throw new Error(response.statusText);
}
});
The
response
argument coming into thecatch
block is the error/exception being thrown, not theresponse
of your fetch callYes but the issue isn’t in the
catch
I don’t believe, it’s the proceedingthen
after returning thefetch
from thecatch
Notice you’re not actually handling the 401 response anywhere?