New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to get the response json on HTTP error 400+ ? #203

Closed
brijeshb42 opened this Issue Sep 24, 2015 · 15 comments

Comments

Projects
None yet
@brijeshb42

brijeshb42 commented Sep 24, 2015

Suppose I create a request to http://example.com/404, and the response is 404 status code with a json response like this:

{
  "type": "error",
  "message": "What you were looking for isn't here."
}

How can I get the above json using fetch?

@dgraham

This comment has been minimized.

Member

dgraham commented Sep 24, 2015

fetch('/404').then(function(response) {
  if (response.status === 404) {
    return response.json()
  }
}).then(function(object) {
  console.log(object.type, object.message)
})

@dgraham dgraham closed this Sep 24, 2015

@lasergoat

This comment has been minimized.

lasergoat commented Sep 25, 2015

How would you catch both a 200 OR a 422 from a resource, both containing valid JSON in the body?

This is the only way I've figured it out so far.


    fetch(`${endpoint}user/${record.id}/`, {
      method: 'put',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'Authorization': `Bearer ${auth}`
      },
      body: JSON.stringify(record)
    })
    .then((result) => result.json())
    .then((result) => {

        if (result.id) {
          dispatch(savedUser( result ));
        } else {
          dispatch(savingUserError( result ));
        }

    });
@dgraham

This comment has been minimized.

Member

dgraham commented Sep 25, 2015

fetch('/resource').then(function(response) {
  if (response.status === 404 || response.status === 200) {
    return response.json()
  }
}).then(function(object) {
  if (object.type === 'error') {
    console.log(object.type, object.message)
  } else {
    console.log('success')
  }
})

or

fetch('/resource').then(function(response) {
  if (response.status === 404) {
    response.json().then(function(object) {
      console.log(object.type, object.message)
    })
  } else if (response.status === 200) {
    response.json().then(function(object) {
      console.log('success')
    })
  }
})
@lasergoat

This comment has been minimized.

lasergoat commented Oct 1, 2015

Hey, just want to let you know, I settled on a fair way of doing what I needed. I thought I'd post just in case anyone else ends up in a situation similar to me where they couldn't use a catch because fetch gives a ReadableByteStream instead of a json object.

// filename: api.js

export default function api(uri, options = {}) {

  options.headers = {
    'Accept': 'application/json',
    'Authorization': `Bearer ${token}`
  };

  return new Promise((resolve, reject) => {

    fetch(endpoint + uri, options)

      .then(response => {

        if (response.status === 200) {

          return resolve(response.json());

        } else {

          return reject(response.status, response.json());

        }
      });
  });

}

This solution keeps your code clean and semantic. You use it like this:

    api(`user/${id}`).then(result => {
      console.log('success');
      dispatch(receiveUser(result))
    })
    .catch((status, err) => {
      console.log('err');
      console.log(err);
    });
@somghosh

This comment has been minimized.

somghosh commented Sep 6, 2016

Hi @lasergoat , i tried your solution - seems not to work! Did it really work for you?

@nikravi

This comment has been minimized.

nikravi commented Dec 9, 2016

Here is the code similar to @lasergoat, but handles json regardless of the response.status. Also in case of network error, rejects with a custom error object

/**
 * Parses the JSON returned by a network request
 *
 * @param  {object} response A response from a network request
 *
 * @return {object}          The parsed JSON, status from the response
 */
function parseJSON(response) {
  return new Promise((resolve) => response.json()
    .then((json) => resolve({
      status: response.status,
      ok: response.ok,
      json,
    })));
}

/**
 * Requests a URL, returning a promise
 *
 * @param  {string} url       The URL we want to request
 * @param  {object} [options] The options we want to pass to "fetch"
 *
 * @return {Promise}           The request promise
 */
export default function request(url, options) {
  return new Promise((resolve, reject) => {
    fetch(endpoint  + url, options)
      .then(parseJSON)
      .then((response) => {
        if (response.ok) {
          return resolve(response.json);
        }
        // extract the error from the server's json
        return reject(response.json.meta.error);
      })
      .catch((error) => reject({
        networkError: error.message,
      }));
  });
}
@developer239

This comment has been minimized.

developer239 commented May 24, 2017

I ended up doing something like this. Probably not best but definitelly one of the shortes solutions. I just did not want to look at all those nested promises.

const parseFetchResponse = response => response.json().then(text => ({
  json: text,
  meta: response,
}))

return fetch(...your stuff...)
  .then(parseFetchResponse)
  .then(({ json, meta }) => {
    console.log('(response, meta)')
    console.log(json)
    console.log(meta)
})
@aaj

This comment has been minimized.

aaj commented Sep 23, 2017

Welp, here's mine:

function processResponse(response) {
  return new Promise((resolve, reject) => {
    // will resolve or reject depending on status, will pass both "status" and "data" in either case
    let func;
    response.status < 400 ? func = resolve : func = reject;
    response.json().then(data => func({'status': response.status, 'data': data}));
  });
}

fetch('/some/url/')
      .then(processResponse)
      .then(response => {
        // repsonses with status < 400 get resolved. you can access response.status and response.data here
      })
      .catch(response => {
        // repsonses with status >= 400 get rejected. you can access response.status and response.data here too
        if (response.status === 400) {
            // handle form validation errors, response.data.errors...
        } else if (response.status === 403) {
            // handle permission errors
        } // etc
      });

Just make sure to always return a JSON parsable body in your responses and you should be good!

@Siilwyn

This comment has been minimized.

Siilwyn commented Oct 11, 2017

Instead of depending on the body (JSON) to contain an indicator I think it's nicer to only look at the status. Downside is having to pass multiple arguments.

fetch('url')
  .then(response => Promise.all([response.ok, response.json()]))
  .then(([responseOk, body]) => {
    if (responseOk) {
      // handle success case
    } else {
      throw new Error(body);
    }
  })
  .catch(error => {
    // catches error case and if fetch itself rejects
  });
@abologna-r7

This comment has been minimized.

abologna-r7 commented Nov 13, 2017

@Siilwyn Agree, I always use response.ok and response.status to determine next action. Unfortunately, consider the fact that you might also want to return an error status with an error message as a JSON response...

{ error: {...}, ...}
@ayozebarrera

This comment has been minimized.

ayozebarrera commented Nov 21, 2017

Nice @Siilwyn but I had some responses as blobs and others as jsons so I have to check headers first, because in your promise, it always returns a json.

handlePromise = (response) => {
  if (response.headers) {
      const contentType = response.headers.get('Content-Type');
      if (contentType.includes('application/pdf')) {
        return Promise.all([response.ok, response.blob()]);
      }
      if (contentType.includes('application/json')) {
        return Promise.all([response.ok, response.json()]);
      }
    }
}

And use it on the fetch

fetch('url')
  .then(this.handlePromise)
  .then(([responseOk, body]) => { //body could be a blob, or json, or whatever!
    if (responseOk) {
      // handle success case
    } else {
      throw new Error(body);
    }
  })
  .catch(error => {
    // catches error case and if fetch itself rejects
  });
@Dajust

This comment has been minimized.

Dajust commented Feb 12, 2018

Just came to thank you guys!

@nilobarp

This comment has been minimized.

nilobarp commented Mar 4, 2018

To produce generic error objects I create a wrapper around the response and reject the promise. Finally throw the wrapped object. This produces a clean response on which higher level functions can depend.

fetch(url, opts)
      .catch(handleError) // handle network issues
      .then(checkStatus)
      .then(parseJSON)
      .catch(error => {
        throw error;
      });

const checkStatus = response => {
    if (response.status >= 200 && response.status < 300) {
      return response;
    }

    return response.json().then(json => {
      return Promise.reject({
        status: response.status,
        ok: false,
        statusText: response.statusText,
        body: json
      });
    });
  };

const parseJSON = response => {
    if (response.status === 204 || response.status === 205) {
      return null;
    }
    return response.json();
  };

const handleError = error => {
    error.response = {
      status: 0,
      statusText:
        "Cannot connect. Please make sure you are connected to internet."
    };
    throw error;
  };

use it like:

function* getUrl() {
    try {
        const response = yield fetch('some-url')
        // do something with response
    } catch (err) {
       // err is of type {status /*number*/, ok /*boolean*/, statusText /*string*/, body /*object*/}
   }
}
@tofflos

This comment has been minimized.

tofflos commented Jul 29, 2018

I'm not that familiar with Javascript and spent way too much time with this but I ended up with a solution that was similar to, but not quite as beautiful as, the solution provided by @Siilwyn. I actually stumbled upon this page quite early in my research but didn't know enough Javascript to recognize the solution staring in my face. So I'm going to take the time to explain it for future generations. ;-)

The way I see it any call to fetch will result in one of the following four outcomes:

  1. Network error
  2. Parsing error (Unreadable JSON from the server)
  3. Business logic error (Readable JSON error message from the server)
  4. Success

In order for our application to be robust it must be able to deal with all these outcomes. The original question stems from the problem that the response and json objects aren't available in the same context. Use Promise.all() to fix that.

Network- and parsing errors causes exceptions and it would be nice to unify our error handling. Throw an Error when you get a business logic error message from the server. That way all types of errors end up in the catch-method.

Finally I'm guessing the original question stems from a desire to provide good error messages. Business logic error messages will be provided by the server but the network- and parsing error messages must be provided by us. In the example below I've created a map with the Exception type as the key and I'm using exception.constructor to do the lookup. Note that network- and parsing error messages are provided in place and that business logic error messages are grabbed from the exception which in turn was created using the message provided by the server.

fetch(URL, OPTS)
    .then(response => Promise.all([response, response.json()]))
    .then(([response, json]) => {
        if (!response.ok) {
            throw new Error(json.message);
        }

        render(json);
    })
    .catch(exception => {
        console.log(new Map([
            [TypeError, "There was a problem fetching the response."],
            [SyntaxError, "There was a problem parsing the response."],
            [Error, exception.message]]).get(exception.constructor));
    });
@fatih108

This comment has been minimized.

fatih108 commented Oct 13, 2018

fetch('api/SampleData/WeatherForecasts')
            .then(response => {
                if (response.status === 200) {

                    response.json().then(data => {
                        this.setState({ forecasts: data, loading: false });
                    });
                }
                else if (response.status === 404) {
                    alert("")
                }
            });

@github github locked as resolved and limited conversation to collaborators Oct 15, 2018

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.