как обрабатывать ошибки в наблюдаемом redux?

У меня такой код:

export const myEpic = (action$, store) =>
action$.ofType("SOME_ACTION")
    .switchMap(action => {
        const {siteId, selectedProgramId} = action;
        const state = store.getState();
        const siteProgram$ = Observable.fromPromise(axios.get(`/url/${siteId}/programs`))
       .catch(error =>{
            return Observable.of({
                type: 'PROGRAM_FAILURE'
                error
            });
        });

        const programType$ = Observable.fromPromise(axios.get('url2'))
       .catch(error =>{
            return Observable.of({
                type: "OTHER_FAILURE",
                error
            });
        });

пока все хорошо, когда есть ошибка, я ее ловлю и (возможно, это неправильно) сопоставляю ее с действием (указывающим на что-то не удалось).

Теперь начинается вопрос, у меня есть еще одна наблюдаемая, которая является результатом zip-оператора двух наблюдаемых сверху:

        const siteProgram$result$ = Observable.zip(siteProgram$, programType$)
       .map(siteProgramsAndProgramTypes => siteProgramsAndProgramTypesToFinalSiteProgramsActionMapper(siteProgramsAndProgramTypes, siteId));

проблема в том, что я до сих пор добираюсь до этого наблюдаемого, как будто все в порядке.

есть ли способ «понять», что одна из «заархивированных» наблюдаемых содержит ошибку, а затем не перейти к «следующей» в siteProgram $ result $. Я думаю, что упускаю что-то банальное ...

Я не хочу выполнять эту проверку:

const siteProgramsAndProgramTypesToFinalSiteProgramsActionMapper = (siteProgramsAndProgramTypesArray, siteId) => {
const [programsResponse, programTypesResponse] = siteProgramsAndProgramTypesArray;
if (programsResponse.error || programTypesResponse.error){
    return {
        type: 'GENERAL_ERROR',
    };
}

каждый раз, когда у меня есть наблюдаемый объект, который является результатом оператора другого наблюдаемого объекта, который мог иметь ошибку.

в чистом rxjs (не в наблюдаемом redux) я думаю, что мог бы подписаться на него, передав ему объект

{
    next: val => some logic,
    error: err => do what ever I want :) //this is what I am missing in redux observable,
    complete: () => some logic
}

// some more logic
return Observable.concat(programType$Result$, selectedProgramId$, siteProgram$result$);

Как правильно атаковать это в наблюдаемом сокращении?

Спасибо.


person JimmyBoy    schedule 10.01.2018    source источник


Ответы (1)


Вот подробный пример с оболочкой API, чтобы облегчить то, что вы пытаетесь достичь.

Суть доступен на GitHub, здесь

Вот оболочка API, которая обертывает Observable.ajax и позволяет отправлять отдельные действия или массив действий и обрабатывает ошибки, сгенерированные как XHR, так и на уровне приложения, которые происходят из запросов, сделанных с Observable.ajax

import * as Rx from 'rxjs';
import queryString from 'query-string';

/**
 * This function simply transforms any actions into an array of actions
 * This enables us to use the synthax Observable.of(...actions)
 * If an array is passed to this function it will be returned automatically instead
 * Example: mapObservables({ type: ACTION_1 }) -> will return: [{ type: ACTION_1 }]
 * Example2: mapObservables([{ type: ACTION_1 }, { type: ACTION_2 }]) -> will return: [{ type: ACTION_1 }, { type: ACTION_2 }]
 */
function mapObservables(observables) {
  if (observables === null) {
    return null;
  } else if (Array.isArray(observables)) {
    return observables;
  }
  return [observables];
}

/**
 * Possible Options:
 * params (optional): Object of parameters to be appended to query string of the uri e.g: { foo: bar } (Used with GET requests)
 * headers (optional): Object of headers to be appended to the request headers
 * data (optional): Any type of data you want to be passed to the body of the request (Used for POST, PUT, PATCH, DELETE requests)
 * uri (required): Uri to be appended to our API base url
 */
function makeRequest(method, options) {
  let uri = options.uri;
  if (method === 'get' && options.params) {
    uri += `?${queryString.stringify(options.params)}`;
  }

  return Rx.Observable.ajax({
    headers: {
      'Content-Type': 'application/json;charset=utf-8',
      ...options.headers,
    },
    responseType: 'json',
    timeout: 60000,
    body: options.data || null,
    method,
    url: `http://www.website.com/api/v1/${uri}`, 
    // Most often you have a fixed API url so we just append a URI here to our fixed URL instead of repeating the API URL everywhere.
  })
  .flatMap(({ response }) => {
    /**
     * Here we handle our success callback, anyt actions returned from it will be dispatched.
     * You can return a single action or an array of actions to be dispatched eg. [{ type: ACTION_1 }, { type: ACTION_2 }].
     */
    if (options.onSuccess) {
      const observables = mapObservables(options.onSuccess(response));
      if (observables) {
        // This is only being called if our onSuccess callback returns any actions in which case we have to dispatch them
        return Rx.Observable.of(...observables);
      }
    }
    return Rx.Observable.of();
  })
  .catch((error) => {
      /**
       * This if case is to handle non-XHR errors gracefully that may be coming from elsewhere in our application when we fire 
       * an Observable.ajax request
       */
    if (!error.xhr) {
      if (options.onError) {
        const observables = mapObservables(options.onError(null)); // Note we pass null to our onError callback because it's not an XHR error
        if (observables) {
          // This is only being called if our onError callback returns any actions in which case we have to dispatch them
          return Rx.Observable.of(...observables);
        }
      }

      // You always have to ensure that you return an Observable, even if it's empty from all your Observables.
      return Rx.Observable.of();
    }

    const { xhr } = error;
    const { response } = error.xhr;
    const actions = [];
    const resArg = response || null;
    let message = null;

    if (xhr.status === 0) {
      message = 'Server is not responding.';
    } else if (xhr.status === 401) {
      // For instance we handle a 401 here, if you use react-router-redux you can simply push actions here to your router
      actions.push(
        replace('/login'),
      );
    } else if (
      response
      && response.errorMessage
    ) {
      /*
       * In this case the errorMessage parameter would refer to SampleApiResponse.json 400 example
       * { "errorMessage": "Invalid parameter." }
       */
      message = response.errorMessage;
    }

    if (options.onError) {
      // Here if our options contain an onError callback we can map the returned Actions and push them into our action payload
      mapObservables(options.onError(resArg)).forEach(o => actions.push(o));
    }

    if (message) {
      actions.push(showMessageAction(message));
    }

    /** 
     * You can return multiple actions in one observable by adding arguments Rx.Observable.of(action1, action2, ...) 
     * The actions always have to have a type { type: 'ACTION_1' }
     */
    return Rx.Observable.of(...actions);
  });
}

const API = {
  get: options => makeRequest('get', options),
  post: options => makeRequest('post', options),
  put: options => makeRequest('put', options),
  patch: options => makeRequest('patch', options),
  delete: options => makeRequest('delete', options),
};

export default API;

Вот действия, создатели действий и эпики:

import API from 'API';

const FETCH_PROFILE = 'FETCH_PROFILE';
const FETCH_PROFILE_SUCCESS = 'FETCH_PROFILE_SUCCESS';
const FETCH_PROFILE_ERROR = 'FETCH_PROFILE_ERROR';
const FETCH_OTHER_THING = 'FETCH_OTHER_THING';
const FETCH_OTHER_THING_SUCCESS = 'FETCH_OTHER_THING_SUCCESS';
const FETCH_OTHER_THING_ERROR = 'FETCH_OTHER_THING_ERROR';

function fetchProfile(id) {
  return {
    type: FETCH_PROFILE,
    id,
  };
}

function fetchProfileSuccess(data) {
  return {
    type: FETCH_PROFILE_SUCCESS,
    data,
  };
}

function fetchProfileError(error) {
  return {
    type: FETCH_PROFILE_ERROR,
    error,
  };
}

function fetchOtherThing(id) {
  return {
    type: FETCH_OTHER_THING,
    id,
  };
}

const fetchProfileEpic = action$ => action$.
  ofType(FETCH_PROFILE)
  .switchMap(({ id }) => API.get({
    uri: 'profile',
    params: {
      id,
    },
    /*
     * We could also dispatch multiple actions using an array here you could dispatch another API request if needed
     * We can redispatch another action to fire another epic if we want also.
     * In both onSuccess and onError you can return a single action, an array of actions or null
     * Note here we fire fetchOtherThing(data.someOtherThingId) which will trigger our fetchOtherThingEpic!
     * In this case the data parameter would refer to SampleApiResponse.json 200 example
     * { firstName: "John", lastName: "Doe" }
     */
    onSuccess: ({ data }) => [fetchProfileSuccess(data), fetchOtherThing(data.someOtherThingId)],
    onError: error => fetchProfileError(error),
  })

const fetchOtherThingEpic = action$ => action$.
  ofType(FETCH_OTHER_THING)
  .switchMap(({ id }) => API.get({
    uri: 'other-thing',
    params: {
      id,
    },
    onSuccess: ...
    onError: ...
  });

Вот образец данных, который работает с примерами, показанными выше:

/*
 * If possible, you should standardize your API response which will make error/data handling a lot easier on the client side
 * Note, this data format is to work with the example code above
 */

/**
  * Status code: 401
  * This error would be caught in the .catch() method of our API wrapper
  */
{
  "errorMessage": "Please login to perform this operation",
  "data": null,
}

/**
  * Status code: 400
  * This error would be caught in the .catch() method of our API wrapper
  *
  */
{
  "errorMessage": "Invalid parameter.",
  "data": null
}

/**
  * Status code: 200
  * This would be passed to our onSuccess function specified in our API options
  */
{
  "errorMessage": 'Please login to perform this operation',
  "data": { 
    "firstName": "John", 
    "lastName": "Doe" 
  }
}
person Logicgate    schedule 10.01.2018
comment
Спасибо за ответ, но я до сих пор не понимаю: if Observable.ajax ('/ first'). Catch (...); бросает или Observable.ajax ('/ second'). catch (...); бросает. Я не доберусь до улова объединенного наблюдаемого? const fetchBoth = () = ›(action, store) =› Observable.merge (fetchFirst () (action, store), fetchSecond () (action, store),) .catch (err = ›{доберусь ли я до этой части ? из того, что я видел в моем примере, я не дохожу до этой уловки, потому что мы поймали наблюдаемый источник и сопоставили его с действием}) Я поиграю с ним и выясню это :) ваша идея API великолепна. - person JimmyBoy; 11.01.2018
comment
Есть разные способы сделать это. Я лично улавливаю обертку API, поэтому мне не нужно реализовывать уловку для каждого наблюдаемого объекта, который запускает запрос. Если у вас есть API, который имеет согласованное соглашение об ошибках, и вы можете использовать коды состояния для обработки своих ошибок, я бы отказался от перехвата ошибок уровня Observable и направил все это в оболочку API. - person Logicgate; 11.01.2018
comment
Вот суть с полным подробным примером. Надеюсь, это более подробно объясняет, как структурировать оболочку API и обрабатывать несколько асинхронных запросов / диспетчеризацию действий / обработку ошибок. Нажмите здесь, чтобы увидеть суть на Github - person Logicgate; 11.01.2018