Igor Shmukler
Igor Shmukler

Reputation: 2216

Detecting network is down from browser Fetch API

I made the below utility to handle the web requests for me:

import configureStore from '../configureStore';
import { logout } from '../containers/App/actions';

const store = configureStore({});

/**
 * Parses the JSON returned by a network request
 *
 * @param  {object} response A response from a network request
 *
 * @return {object}          The parsed JSON from the request
 */
function parseJSON(response) {
  if (
    response.status === 204 ||
    response.status === 205 ||
    parseInt(response.headers.get('content-length')) === 0
  ) {
    return null;
  }
  return response.json();
}

/**
 * Checks if a network request came back fine, and throws an error if not
 *
 * @param  {object} response   A response from a network request
 *
 * @return {object|undefined} Returns either the response, or throws an error
 */
function checkStatus(response) {
  if (response.status >= 200 && response.status < 300) {
    return response;
  }

  const error = new Error(response.statusText);
  error.response = response;
  throw error;
}

/**
 * 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 {object}           The response data
 */
export default function request(url, options) {
  const headers = {
    Accept: 'application/json',
    'Content-Type': 'application/json',
    'Access-Control-Request-Headers': 'Content-Type, Authorization'
  };
  const token = localStorage.getItem('token');
  if (token) {
    headers['Authorization'] = `Bearer ${token}`;
  }
  const newOptions = {
    ...options,
    mode: 'cors',
    headers
  };
  // FIXME
  // properly handle `net::ERR_CONNECTION_REFUSED`
  // currently, we return `cannot read properties of undefined (reading 'status')`
  return fetch(url, newOptions)
    .then(checkStatus)
    .then(parseJSON)
    .catch(err => {
      // check for 401 here and throw an action to clean the store and the logout.
      if (err.response.status === 401) {
        store.dispatch(logout);
      }
      throw err;
    });
}

This is working except when a network error happens. Since fetch does not throw on errors, I need to handle this case myself.

Only, it is NOT working. I tried adding if (!response.ok) to the checkStatus. It is however also not working, since the browser throws a CORS error.

How do I handle this, so my request() throws Failed to fetch instead of cannot read properties of undefined (reading 'status')?

Upvotes: 1

Views: 2637

Answers (3)

Igor Shmukler
Igor Shmukler

Reputation: 2216

ended-up with:

async function parseJSON(response) {
  if (
    response.status === 204 ||
    response.status === 205 ||
    parseInt(response.headers.get('content-length')) === 0
  ) {
    return null;
  }
  return await response.json();
}

function checkStatus(response) {
  if (response.status >= 200 && response.status < 300) {
    return response;
  }    
  const error = new Error(response.statusText);
  error.response = response;
  throw error;
}

export default async function request(url, options) {
  const headers = {
    Accept: 'application/json',
    'Content-Type': 'application/json',
    'Access-Control-Request-Headers': 'Content-Type, Authorization'
  };
  const token = localStorage.getItem('token');
  if (token) {
    headers['Authorization'] = `Bearer ${token}`;
  }
  const newOptions = {
    ...options,
    mode: 'cors',
    headers
  };
  try {
    const result = await fetch(url, newOptions);
    return await parseJSON(checkStatus(result));
  } catch (err) {
    // check for 401 here and throw an action to clean the store and the logout.
    if (err != null && 'response' in err && err.response.status === 401) {
      store.dispatch(logout);
    }
    throw err;
  }
}

Upvotes: 0

Evert
Evert

Reputation: 99505

fetch() does throw on network errors. The only error it doesn't throw is HTTP errors.

Upvotes: 2

Kyle
Kyle

Reputation: 4014

Since fetch does not throw on error

Fetch does throw on error:

A fetch() promise only rejects when a network error is encountered https://developer.mozilla.org/en-US/docs/Web/API/fetch

It won't tell you specifically that the network is down, but it will throw an error.

Your error is likely occurring here:

if (err.response.status === 401) {

That's because fetch is throwing an actual Error instance when the network is down, thus response is null, so you can't access status on null. So if response doesn't exist on err, just throw it:

return fetch(url, newOptions)
  .then(checkStatus)
  .then(parseJSON)
  .catch(err => {
    // check for 401 here and throw an action to clean the store and the logout.
    if (err != null && 'response' in err && err.response.status === 401) {
      store.dispatch(logout);
    }
    throw err;
  });

Upvotes: 3

Related Questions