Q:

How to handle common fetch actions inside saga

I'm developping an API consuming web front site.
The problem
All my API saga were like this : 
export function* login(action) {
  const requestURL = "./api/auth/login"; // Endpoint URL
  //  Select the token if needed : const token = yield select(makeSelectToken());

  const options = {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + btoa(JSON.stringify({ login: action.email, password: action.password })),
    }
  };

  try {
    // The request helper from react-boilerplate
    const user = yield call(request, requestURL, options);
    yield put(loginActions.loginSuccess(user.token);
    yield put(push('/'));
  } catch (err) {
    yield put(loginActions.loginFailure(err.detailedMessage));
    yield put(executeErrorHandler(err.code, err.detailedMessage, err.key)); // Error handling
  }
}

And I had the same pattern with all my sagas : 

Select the token if I need to call a private function in the start of the saga

const token = yield select(makeSelectToken());

Handle errors on the catch part

export const executeErrorHandler = (code, detailedMessage, key) => ({
  type: HTTP_ERROR_HANDLER, status: code, detailedMessage, key
});

export function* errorHandler(action) {
  switch (action.status) {
    case 400:
      yield put(addError(action.key, action.detailedMessage));
      break;

    case 401:
      put(push('/login'));
      break;

    //other errors...
  }
}

export default function* httpError() {
  yield takeLatest(HTTP_ERROR_HANDLER, errorHandler);
}

The solution I came up with
Remove the token parts and error handling part and puth them inside the call helper :
export function* login(action) {
  const url = `${apiUrl.public}/signin`;

  const body = JSON.stringify({
    email: action.email,
    password: action.password,
  });

  try {
    const user = yield call(postRequest, { url, body });

    yield put(loginSuccess(user.token, action.email));
    yield put(push('/'));
  } catch (err) {
    yield put(loginFailure());
  }
}

// post request just call the default request with a "post" method
export function postRequest({ url, headers, body, auth = null }) {
  return request(url, 'post', headers, body, auth);
}

export default function request(url, method, headers, body, auth = null) {
  const options = { method, headers, body };

  return fetch(url, addHeader(options, auth)) // add header will add the token if auth == true
    .then(checkStatus)
    .then(parseJSON)
    .catch(handleError); // the error handler
}

function handleError(error) {
  if (error.code === 401) {
    put(push('/login')); // <-- Here this doesn't work
  }

  if (error.code == 400) {
    displayToast(error);
  }
}

function addHeader(options = {}, auth) {
  const newOptions = { ...options };
  if (!options.headers) {
    newOptions.headers = {
      Accept: 'application/json',
      'Content-Type': 'application/json',
      ...options.headers,
    };
  }

  if (auth) {
    const token =  yield select(makeSelectToken()); // <-- here it doesn't work
    newOptions.headers.Authorization = `Bearer ${auth}`;
  }

  return newOptions;
}

I know the solution is between generator functions, side effects, yield call / select but I tried so many things it didn't work. For example, if I wrap everything inside generator functions, the token load is executed after the code continues and call the API.
Your help would be appreciated.

A:

You need to run any and all effects (e.g. yield select) from a generator function, so you'll need generators all the way down to the point in your call stack where you yield an effect. Given that I would try to push those calls as high as possible. I assume you may have getRequest, putRequest etc. in addition to postRequest so if you want to avoid duplicating the yield select you'll want to do it in request. I can't fully test your snippet but I believe this should work:
export function* postRequest({ url, headers, body, auth = null }) {
  return yield call(request, url, 'post', headers, body, auth); // could yield directly but using `call` makes testing eaiser
}

export default function* request(url, method, headers, body, auth = null) {
  const options = { method, headers, body };
  const token = auth ? yield select(makeSelectToken()) : null;
  try {
      const response = yield call(fetch, url, addHeader(options, token));
      const checkedResponse = checkStatus(response);
      return parseJSON(checkedResponse);
  } catch (e) {
     const errorEffect = getErrorEffect(e); // replaces handleError
     if (errorEffect) {
        yield errorEffect;
     }
  }
}

function addHeader(options = {}, token) {
  const newOptions = { ...options };
  if (!options.headers) {
    newOptions.headers = {
      Accept: 'application/json',
      'Content-Type': 'application/json',
      ...options.headers,
    };
  }

  if (token) {
    newOptions.headers.Authorization = `Bearer ${token}`;
  }

  return newOptions;
}

function getErrorEffect(error) {
  if (error.code === 401) {
    return put(push('/login')); // returns the effect for the `request` generator to yeild
  }

  if (error.code == 400) {
    return displayToast(error); // assuming `displayToast` is an effect that can be yielded directly
  }
}

