最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to fetch response body using fetch in case of HTTP error 422? - Stack Overflow

programmeradmin6浏览0评论

I have an API on backend which sends status 201 in case of a successful call and if there's any error with the submitted data it sends status 422 (Unprocessable Entity) with a json response like below:

{
  "error": "Some error text here explaining the error"
}

Additionally it sends 404 in case API fails to work on back end for some reason.

On the front-end I'm using the below code to fetch the response and perform a success or error callback based on response status code:

fetch("api_url_here", {
    method: 'some_method_here', 
    credentials: "same-origin",
    headers: {
      "Content-type": "application/json; charset=UTF-8",
      'X-CSRF-Token': "some_token_here"
    }
    })
  .then(checkStatus)
  .then(function json(response) {
    return response.json()
  })
  .then(function(resp){
    successCallback(resp)
  })
  .catch(function(error){
    errorCallback(error);
  });
  
//status function used above
checkStatus = (response) => {
  if (response.status >= 200 && response.status < 300) {
    return Promise.resolve(response)
  } else {
    return Promise.reject(new Error(response))
  }
}

I have an API on backend which sends status 201 in case of a successful call and if there's any error with the submitted data it sends status 422 (Unprocessable Entity) with a json response like below:

{
  "error": "Some error text here explaining the error"
}

Additionally it sends 404 in case API fails to work on back end for some reason.

On the front-end I'm using the below code to fetch the response and perform a success or error callback based on response status code:

fetch("api_url_here", {
    method: 'some_method_here', 
    credentials: "same-origin",
    headers: {
      "Content-type": "application/json; charset=UTF-8",
      'X-CSRF-Token': "some_token_here"
    }
    })
  .then(checkStatus)
  .then(function json(response) {
    return response.json()
  })
  .then(function(resp){
    successCallback(resp)
  })
  .catch(function(error){
    errorCallback(error);
  });
  
//status function used above
checkStatus = (response) => {
  if (response.status >= 200 && response.status < 300) {
    return Promise.resolve(response)
  } else {
    return Promise.reject(new Error(response))
  }
}

The successCallback function is able to receive the response in proper JSON format in case of status code 201 but when I try to fetch the error response in errorCallback (status: 422) it shows something like this (logging the response in console):

Error: [object Response]
    at status (grocery-market.js:447)

When I try to console.log the error response before wrapping it in new Error() like this,

  checkStatus = (response) => {
    if (response.status >= 200 && response.status < 300) {
      return Promise.resolve(response)
    } else {
      console.log(response.json()) //logging the response beforehand
      return Promise.reject(new Error(response.statusText))
    }
  }

I get the below promise in console (the [[PromiseValue]] property actually contains the error text that i require)

Can someone please explain why this is happening only in error case, even though I'm calling response.json() in both error and success case?

How can I fix this in a clean manner?

EDIT: I found that if I create another json() promise on the error response I am able to get the correct error :

fetch("api_url_here", {
    method: 'some_method_here', 
    credentials: "same-origin",
    headers: {
      "Content-type": "application/json; charset=UTF-8",
      'X-CSRF-Token': "some_token_here"
    }
    })
  .then(checkStatus)
  .then(function json(response) {
    return response.json()
  })
  .then(function(resp){
    successCallback(resp)
  })
  .catch(function(error){
    error.json().then((error) => { //changed here
        errorCallback(error)
      });
  });

But why do I have to call another .json() on the response in error case?

Share Improve this question edited Sep 24, 2022 at 3:34 starball 50.8k32 gold badges204 silver badges890 bronze badges asked Aug 14, 2020 at 7:59 D_S_XD_S_X 1,5597 gold badges18 silver badges38 bronze badges 2
  • Response.json() is async so you might want to make checkStatus async and await the response – Søren Eriksen Commented Aug 14, 2020 at 8:37
  • @SørenEriksen yeah i understood that but fetching response in checkStatus in probably not the cleanest way to do this. That i did just to check what was happening beforehand. – D_S_X Commented Aug 14, 2020 at 8:48
Add a ment  | 

2 Answers 2

Reset to default 8 +50

Short answer

Because your assumption that you parse the response content twice is incorrect. In the original snippet, the then after the then(checkStatus) is skipped when the error condition is met.

Long answer

In general, well-structured promise chain consists of a:

  1. Promise to be fulfilled or rejected sometime in the future
  2. then handler that is run only upon fulfillment of the Promise
  3. catch handler that is run only upon rejection of the Promise
  4. finally (optional) that is run when a Promise is settled (in either 2 or 3)

Each of the handlers in 2 and 3 return a Promise to enable chaining.

Next, fetch method of the Fetch API rejects only on network failures, so the first then method is called regardless of the status code of the response. Your first handler, the onFulfilled callback returns either a fulfilled or rejected Promise.

If fulfilled, it passes control to the next then method call in the chain, where you extract JSON by calling json method on the response, which is then passed as Promise value to the last then method to be used in the successCallback.

If rejected, the control is passed to the catch method call, which receives the Promise with the value set to new Error(response) that you then promptly pass to errorCallback. Therefore, the latter receives an instance of Error, whose value is an instance of Response from Fetch API.

That is exactly what you see logged: Error: [object Response], a result of calling toString method on an instance of Error. The first part is the constructor name, and the second is a string tag of the contents (of the form [type Constructor]).

What to do?

Since your API returns JSON response for every possible use case (201, 404, 422), pass the parsed response to both fulfilled and rejected promise. Also, note that you accidentally declared checkStatus on the global scope by omitting a var, const, or let keyword:

//mock Response object
const res = {
  status: 200,
  body: "mock",
  async json() {
    const {
      body
    } = this;
    return body;
  }
};

const checkStatus = async (response) => {

  const parsed = await response.json();

  const {
    status
  } = response;

  if (status >= 200 && status < 300) {
    return parsed;
  }

  return Promise.reject(new Error(parsed));
};

const test = () => {

  return checkStatus(res)
    .then(console.log)
    .catch((err) => console.warn(err.message))
    .finally(() => {
      if (res.status === 200) {
        res.status = 422;
        return test();
      }
    });

};

test();


Additionally, since you already use ES6 features (judging by the presence of arrow functions), why not go all the way and use the syntactic sugar async/await provides:

(() => {
  try {
    const response = await fetch("api_url_here", {
      method: 'some_method_here',
      credentials: "same-origin",
      headers: {
        "Content-type": "application/json; charset=UTF-8",
        'X-CSRF-Token': "some_token_here"
      }
    });

    const parsed = await response.json(); //json method returns a Promise!

    const {
      status
    } = response;

    if (status === 201) {
      return successCallback(parsed);
    }

    throw new Error(parsed);

  } catch (error) {
    return errorCallback(error);
  }
})();

Note that when you are passing the parsed JSON content to the Error() constructor, an abstract ToString operation is called (see step 3a of the ECMA spec).

If the message is an object, unless the original object has a toString method, you will get a string tag, i.e. [object Object], resulting in the content of the object being inaccessible to the error handler:

(() => {

  const obj = { msg : "bang bang" };
  
  const err = new Error(obj);
  
  //will log [object Object]
  console.log(err.message);
  
  obj.toString = function () { return this.msg; };
  
  const errWithToString = new Error(obj);

  //will log "bang bang"
  console.log(errWithToString.message);

})();

Contrary, if you reject a Promise with the object passed as an argument, the rejected Promise's [[PromiseValue]] will be the object itself.

It's because in case of an error then handlers are skipped and the catch handler is executed, checkStatus returns a rejected promise and the remaining then chain is bypassed.

This is how I'd refactor your code.

checkStatus = async (response) => {
    if (response.status >= 200 && response.status < 300) 
      return await response.json()

    throw await response.json()
  }
}


fetch("api_url_here", {
    method: 'some_method_here', 
    credentials: "same-origin",
    headers: {
      "Content-type": "application/json; charset=UTF-8",
      'X-CSRF-Token': "some_token_here"
    }
    })
  .then(checkStatus)
  .then(successCallback)
  .catch(errorCallback);

P.S. this code can be made a bit better to look at visually by using async/await

发布评论

评论列表(0)

  1. 暂无评论