skip to Main Content

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

3

Answers


  1. I would suggest to change the sequence of catch and then which is in the 2nd code block…
    The sequence of catch and then matters,

    According to on stackOverflow answer,

    If that .then() handler either throws or returns a promise that
    eventually rejects, then the .catch() handler cannot catch that
    because it is before it in the chain.

    Change to this,

    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;
        }).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;
        }).catch(response => {
            return fetch(host + 'refresh', {
                method: 'POST',
                headers: {
                    "Accept": "application/json",
                    "Content-Type": "application/json",
                    "Authorization": "Bearer " + getBearerToken()
                }
            });
        })
    

    Hope it helps

    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
  3. 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);
        }
    });
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search