Skip to content

Gestion des erreurs [Proposition]

Ivan Gabriele edited this page Nov 24, 2023 · 11 revisions

Cas d'erreurs

Erreurs inattendues et non gérées

Erreur internes ou provenant d'une librairie dont on ne gère pas le cas :

  • soit parce qu'on ne l'attrape pas,
  • soit parce qu'on l'attrape mais de manière générique,
  • soit parce qu'on l'attrape de manière ciblée mais ne la transformons pas.

Erreurs inattendues et gérées

Erreurs internes ou provenant d'une librairie dont on gère le cas en la transformant en une erreur typée et manipulée.

Erreurs attendues et non gérées

Il ne devrait pas exister d'erreurs attendues et non gérées. S'il y en a, elles tomberont dans le cas Erreurs inattendues et non gérées qu'il faudra alors corriger pour qu'elles deviennent des Erreurs attendues et gérées .

Erreurs attendues et gérées

Erreurs qui ne sont pas des erreurs de code mais représentent des actions impossibles (ex: tentative de suppression d'une entité liée à une autre et protégée par une clé étrangère).

Proposition de conventions

Réponse Backend

Note

Besoin d'autres props ?

{
  type: ApiErrorCode | null
}

Pour les erreurs inattendues, gérées ou non, type doit être égal à null.
Pour les erreurs attendues, type doit être égal à ApiErrorCode.

ApiErrorCode peut être :

Note

Liste à compléter.

  • CHILD_ALREADY_ATTACHED : Tentative de ré-attachement d'un enfant à une entité qui a déjà cet enfant.
  • FOREIGN_KEY_CONSTRAINT : Tentative de suppression d'une entité liée à d'autres entités.
  • UNARCHIVED_CHILD : Tentative d'archivage d'une entité dont les enfants ne sont pas tous archivés.

Sentry

Note

Tableau à compléter.

Erreurs génériques inattendues Erreurs d'API
Backend { side: "backend", type: "error" } { side: "backend", type: "api_error" }
Frontend { side: "frontend", type: "error" } { side: "frontend", type: "api_error" }

UX & UI

Erreur d'usage

  • Modal affichée à l'utilisateur expliquant pourquoi l'action est impossible.
  • Pas de log console.
  • Pas de log Sentry.

Erreur inattendue d'API

  • Modal ou Toast affichée à l'utilisateur explicitant quelle action a échouée.
  • Log console.
  • Log Sentry.

Erreur inattendue interne au code Frontend

  • ??? (=> décider de comment gérer ça).
  • Log console.
  • Log Sentry.

Implémentation

Backend

class ControllersExceptionHandler {
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(ForeignKeyConstraintException::class)
    fun handleForeignKeyConstraintException(e: ForeignKeyConstraintException): ApiError {
        return ApiError(ErrorCode.FOREIGN_KEY_CONSTRAINT)
    }

    // TODO À compléter !
}


class ApiError(val errorCode: ErrorCode) {
    // TODO À compléter !
}

enum class ErrorCode {
    /** Thrown when attempting to attach the same child to an entity. */
    CHILD_ALREADY_ATTACHED,
    /** Thrown when attempting to delete an entity linked to other entities. */
    FOREIGN_KEY_CONSTRAINT,
    /** Thrown when attempting to archive an entity linked to non-archived children. */
    UNARCHIVED_CHILD,

    // TODO À compléter !
}

Frontend

Erreur générique

class FrontendError extends Error {
  /** Technical error message for logs and debugging purpose. */
  public override message: string,
  /** Originally thrown error. */
  public originalError?: any,
  /** Sentry `Scope` object (tags, context, extras, etc). */
  public scope?: Scope
}

Erreur d'API

class FrontendApiError extends FrontendError {
  /** User-friendly message describing which operation failed. */
  public userMessage: string,
}

interface CustomRTKResponseError {
  path: string
  requestData: AnyObject | undefined
  responseData: BackendApiErrorResponse
  status: number | 'FETCH_ERROR' | 'PARSING_ERROR' | 'TIMEOUT_ERROR' | 'CUSTOM_ERROR'
}

interface BackendApiErrorResponse {
  type: ApiErrorCode | null
}

enum ApiErrorCode {
  /** Thrown when attempting to attach the same child to an entity. */
  CHILD_ALREADY_ATTACHED = 'CHILD_ALREADY_ATTACHED',
  /** Thrown when attempting to delete an entity linked to other entities. */
  FOREIGN_KEY_CONSTRAINT = 'FOREIGN_KEY_CONSTRAINT',
  /** Thrown when attempting to archive an entity linked to non-archived children. */
  UNARCHIVED_CHILD = 'UNARCHIVED_CHILD'
}

Erreur d'usage

class UsageError {
  /** User-friendly message explaining why the operation couldn't be processed. */
  public userMessage: string
}

Personnalisation des erreurs RTK

type RTKBaseQueryArgs =
  // Query
  | string
  // Mutation
  | {
      body?: AnyObject
      method: 'DELETE' | 'POST' | 'PUT'
      /** URL Path (and not full URL). */
      url: string
    }

const myApiBaseQuery = retry(fetchBaseQuery({ baseUrl }, { maxRetries })

export const myApi = createApi({
  baseQuery: async (args: RTKBaseQueryArgs, api, extraOptions) => {
    const result = await normalizeRtkBaseQuery(myApiBaseQuery)(args, api, extraOptions)
    if (result.error) {
      const error: CustomRTKResponseError = {
        path: typeof args === 'string' ? args : args.url,
        requestData: typeof args === 'string' ? undefined : args.body,
        responseData: result.error.data as BackendApiErrorResponse,
        status: result.error.status
      }

      return { error }
    }

    return result
  },
  // ...
})

Requêtes RTK

Important

TOUTES les fonctions d'endpoint doivent renvoyer soit une FrontendApiError soit une UsageError.

// Unexpected errors
const CREATE_CONTROL_UNIT_RESOURCE_ERROR_MESSAGE = "Nous n'avons pas pu créer ce moyen."
const CAN_DELETE_CONTROL_UNIT_RESOURCE_ERROR_MESSAGE = "Nous n'avons pas pu vérifier si ce moyen est supprimable."
const DELETE_CONTROL_UNIT_RESOURCE_ERROR_MESSAGE = "Nous n'avons pas pu supprimé ce moyen."
const GET_CONTROL_UNIT_RESOURCE_ERROR_MESSAGE = "Nous n'avons pas pu récupérer ce moyen."

// Usage errors
const IMPOSSIBLE_CONTROL_UNIT_RESOURCE_DELETION_ERROR_MESSAGE =
  "Ce moyen est rattaché à des missions. Veuillez l'en détacher avant de le supprimer."

export const monitorenvControlUnitResourceApi = monitorenvApi.injectEndpoints({
  endpoints: builder => ({
    canDeleteControlUnitResource: builder.query<boolean, number>({
      transformErrorResponse: response =>
        new FrontendApiError(CAN_DELETE_CONTROL_UNIT_RESOURCE_ERROR_MESSAGE, response),
    }),

    createControlUnitResource: builder.mutation<void, ControlUnit.NewControlUnitResourceData>({
      transformErrorResponse: response =>
        new FrontendApiError(CREATE_CONTROL_UNIT_RESOURCE_ERROR_MESSAGE, response)
    }),

    // Deux cas ici : l'erreur d'usage attendue et l'erreur inattendue
    deleteControlUnitResource: builder.mutation<void, number>({
      transformErrorResponse: response => {
        if (response.responseData.type === ApiErrorCode.FOREIGN_KEY_CONSTRAINT) {
          return new UsageError(IMPOSSIBLE_CONTROL_UNIT_RESOURCE_DELETION_ERROR_MESSAGE)
        }

        return new FrontendApiError(DELETE_CONTROL_UNIT_RESOURCE_ERROR_MESSAGE, response)
      }
    }),

    getControlUnitResource: builder.query<ControlUnit.ControlUnitResource, number>({
      transformErrorResponse: response =>
        new FrontendApiError(GET_CONTROL_UNIT_RESOURCE_ERROR_MESSAGE, response)
    }),

    // etc...
  })
})