-
Notifications
You must be signed in to change notification settings - Fork 7
Gestion des erreurs [Proposition]
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 internes ou provenant d'une librairie dont on gère le cas en la transformant en une erreur typée et manipulée.
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 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).
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.
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" } |
- Modal affichée à l'utilisateur expliquant pourquoi l'action est impossible.
- Pas de log console.
- Pas de log Sentry.
- Modal ou Toast affichée à l'utilisateur explicitant quelle action a échouée.
- Log console.
- Log Sentry.
- ??? (=> décider de comment gérer ça).
- Log console.
- Log Sentry.
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 !
}
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
}
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'
}
class UsageError {
/** User-friendly message explaining why the operation couldn't be processed. */
public userMessage: string
}
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
},
// ...
})
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...
})
})