ignite-bowser icon indicating copy to clipboard operation
ignite-bowser copied to clipboard

Global error handling use MST

Open etovladislav opened this issue 4 years ago β€’ 5 comments

Hello everyone!

I’ve been looking for a solution for a very long time, I can’t find it anywhere. How to make a global error handler from the api server response to show the user a modal window use MST or something else? Or for example, if the token has expired redirect to the authorization window, if the answer 500 came to show the Houston we have problems screen. depending on server response.

a single handler that will consider different cases and show info windows

πŸ™πŸ»

etovladislav avatar May 16 '20 19:05 etovladislav

Very interested in this topic as well. Maybe we could start by sharing our way to do it or our ideas? That would open up the discussion.

rdewolff avatar May 16 '20 21:05 rdewolff

Hi! I don't have a repo example, but this is the rough code for what I've done for this in another project, in ignite-bowser's file structure:

dialog-store.ts

export const DialogStoreModel = types
  .model("DialogStore")
  .props({
    dialog,
  })
  .extend(withEnvironment)
  .views(self => ({}))
  .actions(self => ({
    setDialog: flow(function * (dialog) {
      self.dialog = dialog
    }),
 })
  .actions(self => ({
    initializeStore: flow(function * () {
      self.environment.setDialog = dialog => self.setDialog(dialog)
    }),
  }))

in setup-root-store.ts

Object.keys(rootStore).forEach(key => {
    if (rootStore[key] && typeof rootStore[key].initializeStore === 'function') {
      rootStore[key].initializeStore()
    }
  })

environment.ts

export class Environment {
  constructor() {
    this.api = new Api({
      // this is a made up constructor! Whatever you use, you can set up your tooling here to do this on error
      onError: (error) => {
        if (this.onError) {
          this.setDialog(errorToDialog(error))
        }
      }
    })

  api: Api

  setDialog: (dialog: any) => void
}

app-wrapper.ts

export const AppWrapper: React.FunctionComponent<AppWrapperScreenProps> = observer(props => {
  const nextScreen = React.useMemo(() => () => props.navigation.navigate("demo"), [
    props.navigation,
  ])

  const { dialogStore } = useStores()

  return (
    <View>
      <SomeContent />
      {dialogStore.dialog}
    </View>
  )
}

The setup works like this:

  • the environment is created in setup-root-store.ts
  • it has a null (for now) onError function
  • the API is initialized with all requests calling onError if there is an error
  • when the dialogStore is created using withEnvironment, call initializeStore during setup-root-store so that environment's onError is hooked up to dialogStore
  • use the dialogStore on some app wrapper component that always shows the dialogStore's dialog if it has one

That way, the app wrapper is observing the dialogStore's dialog, and all requests made by the API library in environment are hooked up to dialogStore.

I'm sure someone who's more MST-savvy can make this even cleaner somehow!

Example repo incoming πŸ‘€

Edit: example repo at https://github.com/jksaunders/ignite-bowser-339

^ In this repo, the mobile app has a valid API request button (no errors shown) and an invalid API request button (error message briefly shown). It's all from a single handler, observable anywhere!

jksaunders avatar May 24 '20 02:05 jksaunders

for your ex, catch the token expires, please check api-problem.ts, and here how can i handle the token expires: `export function getErrorMessage(data: any): string { try { if (data.message != null) { return data.message } else { return translate('errors.serverError') } } catch (e) { return translate('errors.serverError') } } const doExpire = async (msg: string) => { storage.remove(common.TOKEN) rootStore.toast.show(msg) await delay(500) rootStore.auth.setAuth(false) RootNavigation.resetRoot({ routes: [] }) rootStore.resetAll() } export function getGeneralApiProblem(response: ApiResponse): GeneralApiProblem | void { switch (response.problem) { case "CONNECTION_ERROR": return { kind: "cannot-connect", temporary: true } case "NETWORK_ERROR": return { kind: "cannot-connect", temporary: true } case "TIMEOUT_ERROR": return { kind: "timeout", temporary: true } case "SERVER_ERROR": return { kind: "server", data: getErrorMessage(response.data) } case "UNKNOWN_ERROR": return { kind: "unknown", temporary: true } case "CLIENT_ERROR": switch (response.status) { case 401: doExpire(getErrorMessage(response.data)) return { kind: "unauthorized", data: getErrorMessage(response.data) } case 403: return { kind: "forbidden", data: getErrorMessage(response.data) } case 404: return { kind: "not-found", data: getErrorMessage(response.data) } default: return { kind: "rejected", data: getErrorMessage(response.data) } } case "CANCEL_ERROR": return null }

return null } `

viralS-tuanlv avatar Jun 04 '20 14:06 viralS-tuanlv

Hi! I don't have a repo example, but this is the rough code for what I've done for this in another project, in ignite-bowser's file structure:

dialog-store.ts

export const DialogStoreModel = types
  .model("DialogStore")
  .props({
    dialog,
  })
  .extend(withEnvironment)
  .views(self => ({}))
  .actions(self => ({
    setDialog: flow(function * (dialog) {
      self.dialog = dialog
    }),
 })
  .actions(self => ({
    initializeStore: flow(function * () {
      self.environment.setDialog = dialog => self.setDialog(dialog)
    }),
  }))

in setup-root-store.ts

Object.keys(rootStore).forEach(key => {
    if (rootStore[key] && typeof rootStore[key].initializeStore === 'function') {
      rootStore[key].initializeStore()
    }
  })

environment.ts

export class Environment {
  constructor() {
    this.api = new Api({
      // this is a made up constructor! Whatever you use, you can set up your tooling here to do this on error
      onError: (error) => {
        if (this.onError) {
          this.setDialog(errorToDialog(error))
        }
      }
    })

  api: Api

  setDialog: (dialog: any) => void
}

app-wrapper.ts

export const AppWrapper: React.FunctionComponent<AppWrapperScreenProps> = observer(props => {
  const nextScreen = React.useMemo(() => () => props.navigation.navigate("demo"), [
    props.navigation,
  ])

  const { dialogStore } = useStores()

  return (
    <View>
      <SomeContent />
      {dialogStore.dialog}
    </View>
  )
}

The setup works like this:

  • the environment is created in setup-root-store.ts
  • it has a null (for now) onError function
  • the API is initialized with all requests calling onError if there is an error
  • when the dialogStore is created using withEnvironment, call initializeStore during setup-root-store so that environment's onError is hooked up to dialogStore
  • use the dialogStore on some app wrapper component that always shows the dialogStore's dialog if it has one

That way, the app wrapper is observing the dialogStore's dialog, and all requests made by the API library in environment are hooked up to dialogStore.

I'm sure someone who's more MST-savvy can make this even cleaner somehow!

Example repo incoming πŸ‘€

Edit: example repo at https://github.com/jksaunders/ignite-bowser-339

^ In this repo, the mobile app has a valid API request button (no errors shown) and an invalid API request button (error message briefly shown). It's all from a single handler, observable anywhere!

Thank you so much! It really helped

etovladislav avatar Jun 13 '20 19:06 etovladislav

thanks for sharing \o/, another suggestion here instead of changing the logic to use axiosInstance just to catch the onError, we can simply add a monitor to sauceApi, and filter by response.problem,

import * as SecureStorage from "@utils/secure-storage"

import { Api } from "../services/api"
import { ACCESS_TOKEN_KEY } from "@utils/constants"

/**
 * The environment is a place where services and shared dependencies between
 * models live.  They are made available to every model via dependency injection.
 */
export class Environment {
  constructor() {
    this.api = new Api()
  }

  async setup() {
    await this.api.setup()
   
    this.api.apisauce.addMonitor((response) => {
      if (this.setResponse) {
        this.setResponse(response)
      }
    })
  }

  /**
   * Our api.
   */
  api: Api

  setResponse: (response: any) => void
}

rodgomesc avatar Dec 07 '21 14:12 rodgomesc