Gestion des erreurs

Écrit par Stanislas

Dernière mise à jour Il y a 7 mois


Meilleures pratiques et modèles pour gérer les erreurs lors de l'intégration de l'API Swiftask Public Bot. Apprenez à identifier les erreurs, à mettre en œuvre des stratégies de récupération et à fournir des messages d'erreur conviviaux.

Présentation

Les erreurs peuvent se produire à plusieurs niveaux : échecs d'authentification REST, erreurs de validation GraphQL, problèmes réseau, déconnexions WebSocket et erreurs serveur. Ce guide explique comment identifier chaque type d'erreur, mettre en œuvre des stratégies de récupération appropriées et maintenir une bonne expérience utilisateur lorsque des problèmes surviennent.

Une gestion adéquate des erreurs est essentielle pour les applications de production. Elle garantit que votre application ne plante pas, que les utilisateurs comprennent ce qui s'est passé et que l'application peut se rétablir automatiquement lorsque cela est possible.


Prérequis

Avant de mettre en œuvre la gestion des erreurs, assurez-vous de disposer des éléments suivants :

  1. Lisez les guides de base — Comprenez Authentification , Opérations de chat et Abonnements

  2. Apollo Client configuré — Avec des liens HTTP et WebSocket appropriés

  3. Compréhension de async/await — La gestion des erreurs utilise des blocs try-catch

  4. Configuration de base de la journalisation — Pour le débogage des erreurs en production


Pour commencer

Voici une configuration minimale de gestion des erreurs pour les erreurs les plus courantes :

Étape 1 : Envelopper les opérations dans try-catch

Enveloppez toujours les appels API dans des blocs try-catch.

async function sendMessageSafely(client, sessionId, message) {
  try {
    const { data } = await client.mutate({
      mutation: SEND_MESSAGE,
      variables: {
        newMessageData: {
          message,
          sessionId,
          isForAiReply: true,
        },
      },
    });

    console.log('Message sent:', data.sendNewMessage.id);
    return data.sendNewMessage;
  } catch (error) {
    console.error('Failed to send message:', error);
    throw error;
  }
}

Étape 2 : Identifiez le type d'erreur

Faites la distinction entre les erreurs réseau, GraphQL et autres.

function getErrorType(error) {
  if (error.networkError) {
    return 'NETWORK_ERROR';
  }

  if (error.graphQLErrors?.length > 0) {
    return 'GRAPHQL_ERROR';
  }

  if (error instanceof TypeError) {
    return 'TYPE_ERROR';
  }

  return 'UNKNOWN_ERROR';
}

// Usage
try {
  await sendMessage(sessionId, message);
} catch (error) {
  const errorType = getErrorType(error);
  console.log('Error type:', errorType);
}

Étape 3 : affichez des messages conviviaux

Associez les erreurs techniques à des messages conviviaux.

const ERROR_MESSAGES = {
  NETWORK_ERROR: 'Unable to connect. Please check your internet connection.',
  AUTH_ERROR: 'Authentication failed. Please refresh the page.',
  SESSION_NOT_FOUND: 'Chat session not found. Please start a new conversation.',
  MESSAGE_SEND_FAILED: 'Failed to send message. Please try again.',
  STREAM_ERROR: 'Connection interrupted. Reconnecting...',
  RATE_LIMIT: 'Too many requests. Please wait a moment.',
  SERVER_ERROR: 'Something went wrong on our end. Please try again later.',
};

function getUserFriendlyError(error) {
  if (error.networkError) {
    return ERROR_MESSAGES.NETWORK_ERROR;
  }

  if (error.graphQLErrors?.length > 0) {
    const code = error.graphQLErrors[0].extensions?.code;
    if (code === 'UNAUTHENTICATED') {
      return ERROR_MESSAGES.AUTH_ERROR;
    }
    if (code === 'NOT_FOUND') {
      return ERROR_MESSAGES.SESSION_NOT_FOUND;
    }
  }

  return ERROR_MESSAGES.SERVER_ERROR;
}

// Utilisation
try {
  await sendMessage(sessionId, message);
} catch (error) {
  const friendlyMessage = getUserFriendlyError(error);
  showErrorToUser(friendlyMessage);
}HTTP authentication errors

Codes d'erreur et causes

Code

Description

Cause

Correction

400

Demande incorrecte

Paramètres manquants ou non valides

Vérifiez les en-têtes clientToken et x-client-uuid

401

Non autorisé

Jeton invalide ou expiré

Vérifiez que clientToken est correct.

403

Interdit

L'agent n'est pas public

Activez l'accès public dans les paramètres de l'agent

404

Introuvable

L'agent n'existe pas

Vérifiez que l'agent existe dans l'espace de travail

500

Erreur interne du serveur

Problème côté serveur

Réessayer avec un délai exponentiel

Gestion des erreurs d'authentification

async function authenticateWithErrorHandling(clientToken, clientUuid) {
  try {
    const response = await fetch(
      `https://graphql.swiftask.ai/public/widget-bot/${clientToken}`,
      {
        method: 'GET',
        headers: {
          'x-client-uuid': clientUuid,
          'Content-Type': 'application/json',
        },
      }
    );

    if (!response.ok) {
      const error = await response.json();

      switch (response.status) {
        case 400:
          throw new Error('Invalid request: Check your parameters');
        case 401:
          throw new Error('Authentication failed: Invalid client token');
        case 403:
          throw new Error('Access denied: Agent may not be public');
        case 404:
          throw new Error('Agent not found');
        case 500:
          throw new Error('Server error: Try again later');
        default:
          throw new Error(`HTTP ${response.status}: ${error.message}`);
      }
    }

    return await response.json();
  } catch (error) {
    if (error instanceof TypeError) {
      // Network error (DNS, timeout, etc.)
      throw new Error('Network error: Please check your connection');
    }
    throw error;
  }
}

// Usage
try {
  const { data } = await authenticateWithErrorHandling(clientToken, clientUuid);
  console.log('Authenticated successfully');
} catch (error) {
  console.error('Authentication error:', error.message);
  showErrorToUser(error.message);
}

Erreurs GraphQL

Comprendre la structure des erreurs GraphQL

Les erreurs GraphQL contiennent des informations détaillées :

// Erreur structure GraphQL  
const graphQLError = {
  message: 'User not found',
  locations: [{ line: 1, column: 8 }],
  path: ['sendNewMessage'],
  extensions: {
    code: 'NOT_FOUND',
    exception: {
      stacktrace: [...],
    },
  },
};

Codes d'erreur GraphQL courants

Code

Signification

Comment les traiter

UNAUTHENTICATED

Jeton invalide ou expiré

Réauthentifier l'utilisateur

FORBIDDEN

Accès refusé

Vérifier les autorisations, afficher un message d'accès refusé

BAD_USER_INPUT

Données d'entrée non valides

Valider la saisie, afficher l'erreur à l'utilisateur

INTERNAL_SERVER_ERROR

Erreur serveur

Réessayer avec un délai exponentiel

NOT_FOUND

Ressource introuvable

Vérifier les identifiants, afficher un message d'erreur

RATE_LIMITED

Trop de requêtes

Attendez et réessayez

Gestion des erreurs GraphQL

import { ApolloError } from '@apollo/client';

async function sendMessageWithErrorHandling(client, sessionId, message) {
  try {
    const { data } = await client.mutate({
      mutation: SEND_MESSAGE,
      variables: {
        newMessageData: {
          message,
          sessionId,
          isForAiReply: true,
        },
      },
    });

    return data.sendNewMessage;
  } catch (error) {
    if (error instanceof ApolloError) {
      // GraphQL errors
      if (error.graphQLErrors?.length > 0) {
        error.graphQLErrors.forEach((err) => {
          const code = err.extensions?.code;
          const message = err.message;

          console.error('GraphQL Error:', {
            code,
            message,
            path: err.path,
          });

          // Gérer les codes d'erreur spécifiques
          switch (code) {
            case 'UNAUTHENTICATED':
              console.error('Token expired, re-authenticating...');
              handleAuthError();
              break;

            case 'FORBIDDEN':
              console.error('User does not have access');
              showErrorToUser('You do not have permission to perform this action');
              break;

            case 'BAD_USER_INPUT':
              console.error('Invalid input:', message);
              showErrorToUser(`Invalid input: ${message}`);
              break;

            case 'NOT_FOUND':
              console.error('Resource not found');
              showErrorToUser('The requested resource was not found');
              break;

            case 'RATE_LIMITED':
              console.error('Rate limited, retrying...');
              showErrorToUser('Too many requests. Please wait a moment.');
              break;

            default:
              console.error('GraphQL error:', message);
              showErrorToUser('An error occurred. Please try again.');
          }
        });
      }

      // Network errors
      if (error.networkError) {
        console.error('Network Error:', error.networkError);
        handleNetworkError();
      }
    } else {
      // Other errors
      console.error('Unexpected error:', error);
      showErrorToUser('An unexpected error occurred');
    }

    throw error;
  }
}

function handleAuthError() {
  // Clear stored tokens
  localStorage.removeItem('swiftask_access_token');
  // Redirect to login or refresh page
  window.location.reload();
}

function handleNetworkError() {
  showErrorToUser('Network error: Please check your connection');
}

function showErrorToUser(message) {
  // Display error in UI
  console.warn('User message:', message);
}

Erreurs WebSocket et d'abonnement

Erreurs de connexion

const wsLink = new WebSocketLink({
  uri: 'wss://graphql.swiftask.ai/graphql',
  options: {
    reconnect: true,
    reconnectionAttempts: 5,
    connectionCallback: (error) => {
      if (error) {
        console.error('WebSocket connection error:', error);
        showConnectionError('Unable to establish real-time connection');
      } else {
        console.log('WebSocket connected');
        hideConnectionError();
      }
    },
    connectionParams: async () => {
      // Refresh token if needed
      const token = await getValidToken();
      return {
        authorization: `Bearer ${token}`,
        workspaceId: workspaceId,
      };
    },
  },
});

function showConnectionError(message) {
  const banner = document.createElement('div');
  banner.className = 'connection-error-banner';
  banner.textContent = message;
  document.body.prepend(banner);
}

function hideConnectionError() {
  const banner = document.querySelector('.connection-error-banner');
  if (banner) banner.remove();
}

Gestion des erreurs d'abonnement

const MESSAGE_STREAM = gql`
  subscription OnMessageStream($sessionId: Float!) {
    onMessageStream(sessionId: $sessionId) {
      messageChunk
      botResponseMessageId
      isStoppable
    }
  }
`;

const subscribeToStreamWithErrorHandling = (client, sessionId, onChunk, onError) => {
  let fullMessage = '';
  let retryCount = 0;

  const subscribe = () => {
    return client
      .subscribe({
        query: MESSAGE_STREAM,
        variables: { sessionId },
      })
      .subscribe({
        next: (data) => {
          fullMessage += data.onMessageStream.messageChunk;
          onChunk(fullMessage, data.onMessageStream);
          retryCount = 0; // Reset on success
        },

        error: (error) => {
          console.error('Subscription error:', error);

          // Determine error type
          if (error.message?.includes('authorization')) {
            // Auth error - re-authenticate
            onError('Authentication expired. Please refresh.');
            handleAuthError();
          } else if (error.message?.includes('network')) {
            // Network error - try to reconnect
            retryCount++;
            if (retryCount < 3) {
              console.log(`Reconnecting... (${retryCount}/3)`);
              setTimeout(() => {
                subscribe();
              }, 5000);
            } else {
              onError('Connection lost. Please refresh the page.');
            }
          } else {
            // Generic error
            onError('Connection error. Please try again.');
          }
        },

        complete: () => {
          console.log('Subscription completed');
        },
      });
  };

  return subscribe();
};

// Usage
subscribeToStreamWithErrorHandling(
  client,
  sessionId,
  (fullMessage) => {
    updateUI(fullMessage);
  },
  (errorMessage) => {
    showErrorToUser(errorMessage);
  }
);

Stratégies de réessai

Retard exponentiel

Mettre en œuvre des réessais automatiques avec des délais croissants :

class RetryManager {
  constructor(maxRetries = 3, baseDelay = 1000) {
    this.maxRetries = maxRetries;
    this.baseDelay = baseDelay;
  }

  async retry(operation, retryCount = 0) {
    try {
      return await operation();
    } catch (error) {
      if (retryCount >= this.maxRetries) {
        throw new Error(
          `Operation failed after ${this.maxRetries} retries: ${error.message}`
        );
      }

      // Calculate delay with exponential backoff
      const delay = this.baseDelay * Math.pow(2, retryCount);
      const jitter = Math.random() * 1000; // Add randomness to prevent thundering herd

      console.log(
        `Retry ${retryCount + 1}/${this.maxRetries} after ${delay + jitter}ms`
      );

      await this.sleep(delay + jitter);
      return this.retry(operation, retryCount + 1);
    }
  }

  sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
}

// Usage
const retryManager = new RetryManager(3, 1000);

const result = await retryManager.retry(async () => {
  return await sendMessage(sessionId, message);
});

Réessai conditionnel

Ne réessayer que pour certains types d'erreurs spécifiques :

class ConditionalRetry {
  constructor(maxRetries = 3) {
    this.maxRetries = maxRetries;
  }

  shouldRetry(error) {
    // Don't retry client errors (400-499)
    if (error.status >= 400 && error.status < 500) {
      // Except 429 (rate limit)
      if (error.status !== 429) {
        return false;
      }
    }

    // Don't retry auth errors
    if (error.message?.includes('Unauthorized') || 
        error.message?.includes('Authentication failed')) {
      return false;
    }

    // Retry on network errors
    if (error instanceof TypeError || error.networkError) {
      return true;
    }

    // Retry on server errors (500+)
    if (error.status >= 500) {
      return true;
    }

    // Retry on rate limit
    if (error.status === 429) {
      return true;
    }

    return false;
  }

  async retry(operation, maxRetries = this.maxRetries) {
    let lastError;

    for (let i = 0; i < maxRetries; i++) {
      try {
        return await operation();
      } catch (error) {
        lastError = error;

        if (!this.shouldRetry(error)) {
          throw error;
        }

        if (i < maxRetries - 1) {
          const delay = 1000 * Math.pow(2, i);
          console.log(`Retrying in ${delay}ms...`);
          await new Promise((resolve) => setTimeout(resolve, delay));
        }
      }
    }

    throw lastError;
  }
}

// Usage
const retry = new ConditionalRetry(3);

try {
  const result = await retry.retry(() => sendMessage(sessionId, message));
} catch (error) {
  console.error('Failed after retries:', error);
}

Actualisation des jetons

Mise en œuvre du rafraîchissement automatique des jetons

class TokenManager {
  constructor(clientToken) {
    this.clientToken = clientToken;
    this.clientUuid = this.getOrCreateClientUuid();
    this.accessToken = null;
    this.tokenExpiry = null;
  }

  getOrCreateClientUuid() {
    let uuid = localStorage.getItem('swiftask_client_uuid');
    if (!uuid) {
      uuid = crypto.randomUUID();
      localStorage.setItem('swiftask_client_uuid', uuid);
    }
    return uuid;
  }

  async getValidToken() {
    // Check if token is still valid
    if (this.accessToken && this.tokenExpiry > Date.now()) {
      return this.accessToken;
    }

    // Refresh token
    return await this.refreshToken();
  }

  async refreshToken() {
    console.log('Refreshing authentication token...');

    try {
      const response = await fetch(
        `https://graphql.swiftask.ai/public/widget-bot/${this.clientToken}`,
        {
          method: 'GET',
          headers: {
            'x-client-uuid': this.clientUuid,
          },
        }
      );

      if (!response.ok) {
        throw new Error(`Token refresh failed: ${response.status}`);
      }

      const { data } = await response.json();

      this.accessToken = data.accessToken;
      // Assume token expires in 1 hour (adjust based on actual expiry)
      this.tokenExpiry = Date.now() + 60 * 60 * 1000;

      console.log('Token refreshed successfully');
      return this.accessToken;
    } catch (error) {
      console.error('Failed to refresh token:', error);
      throw error;
    }
  }
}

// Usage with Apollo Client
const tokenManager = new TokenManager('your_client_token');

const authLink = setContext(async (_, { headers }) => {
  try {
    const token = await tokenManager.getValidToken();
    return {
      headers: {
        ...headers,
        authorization: `Bearer ${token}`,
      },
    };
  } catch (error) {
    console.error('Failed to get valid token:', error);
    // Redirect to login or show error
    throw error;
  }
});

Classe complète de gestion des erreurs

Gestion des erreurs prête pour la production pour toutes les opérations :

class ErrorHandler {
  constructor(options = {}) {
    this.onAuthError = options.onAuthError || (() => {});
    this.onNetworkError = options.onNetworkError || (() => {});
    this.onDisplayError = options.onDisplayError || console.error;
    this.retryManager = new RetryManager(options.maxRetries || 3);
    this.shouldRetry = options.shouldRetry || (() => true);
  }

  async handleOperation(operation, context = {}) {
    try {
      return await this.retryManager.retry(operation);
    } catch (error) {
      return this.handleError(error, context);
    }
  }

  handleError(error, context = {}) {
    console.error('Error occurred:', error, 'Context:', context);

    // Network errors
    if (error.networkError || error instanceof TypeError) {
      this.onNetworkError();
      this.onDisplayError('Network error: Please check your connection');
      return null;
    }

    // GraphQL errors
    if (error.graphQLErrors?.length > 0) {
      const gqlError = error.graphQLErrors[0];
      const code = gqlError.extensions?.code;

      if (code === 'UNAUTHENTICATED') {
        this.onAuthError();
        this.onDisplayError('Authentication failed. Please refresh the page.');
      } else if (code === 'FORBIDDEN') {
        this.onDisplayError('You do not have permission to perform this action');
      } else if (code === 'BAD_USER_INPUT') {
        this.onDisplayError(`Invalid input: ${gqlError.message}`);
      } else if (code === 'NOT_FOUND') {
        this.onDisplayError('The requested resource was not found');
      } else if (code === 'RATE_LIMITED') {
        this.onDisplayError('Too many requests. Please wait a moment.');
      } else {
        this.onDisplayError('An error occurred. Please try again.');
      }

      return null;
    }

    // Generic error
    this.onDisplayError('An unexpected error occurred');
    return null;
  }

  logError(error, context) {
    // Send to logging service (e.g., Sentry, LogRocket)
    console.error('Logging error:', {
      message: error.message,
      stack: error.stack,
      context,
      timestamp: new Date().toISOString(),
    });
  }
}

// Usage
const errorHandler = new ErrorHandler({
  onAuthError: () => {
    // Re-authenticate or redirect to login
    window.location.reload();
  },
  onNetworkError: () => {
    // Show offline indicator
    showOfflineIndicator();
  },
  onDisplayError: (message) => {
    // Show error to user
    showToast(message, 'error');
  },
  maxRetries: 3,
});

// Use in your code
const result = await errorHandler.handleOperation(
  () => sendMessage(sessionId, message),
  {
    operation: 'sendMessage',
    sessionId,
    message,
  }
);

Modèles pratiques de gestion des erreurs

Validation des entrées

function validateMessage(message) {
  if (!message || message.trim().length === 0) {
    throw new Error('Message cannot be empty');
  }

  if (message.length > 5000) {
    throw new Error('Message is too long (max 5000 characters)');
  }

  return message.trim();
}

const handleSend = async (input) => {
  try {
    const validatedMessage = validateMessage(input);
    await sendMessage(sessionId, validatedMessage);
  } catch (error) {
    showErrorToUser(error.message);
  }
};

Validation de session

async function getSessionSafely(client, sessionId) {
  try {
    const { data } = await client.query({
      query: GET_SESSION,
      variables: { sessionId },
    });

    if (!data.getOneTodoChatSession) {
      throw new Error('Session not found');
    }

    return data.getOneTodoChatSession;
  } catch (error) {
    if (error.message === 'Session not found') {
      showErrorToUser('This chat session no longer exists');
    } else {
      showErrorToUser('Failed to load session');
    }
    throw error;
  }
}

Dégradation progressive

async function sendMessageWithFallback(client, sessionId, message) {
  try {
    // Try streaming first
    return await sendMessage(client, sessionId, message);
  } catch (error) {
    console.warn('Streaming failed, trying synchronous request:', error);

    try {
      // Fall back to synchronous request
      return await sendSyncMessage(client, sessionId, message);
    } catch (fallbackError) {
      console.error('Both methods failed:', fallbackError);
      throw fallbackError;
    }
  }
}

Meilleures pratiques

  1. Toujours gérer les erreurs avec élégance — Ne jamais laisser les erreurs planter votre application

  2. Fournissez des messages conviviaux — Les erreurs techniques doivent être consignées, et non affichées aux utilisateurs

  3. Implémentez une logique de réessai — Les problèmes réseau sont courants ; réessayez avec un délai exponentiel

  4. Enregistrez les erreurs à des fins de débogage — Incluez le contexte et les traces de pile pour le dépannage

  5. Surveillez les taux d'erreur — Suivez les erreurs pour identifier les problèmes systémiques

  6. Gérez l'expiration des jetons — Implémentez un rafraîchissement automatique des jetons

  7. Tester les scénarios d'erreur — Simuler des pannes réseau, des erreurs d'authentification, etc. pendant le développement

  8. Afficher les états de chargement — Tenir les utilisateurs informés pendant les réessais

  9. Fournir des options de secours — Permettre aux utilisateurs de réessayer manuellement

  10. Nettoyer les ressources — Se désabonner des WebSockets en cas d'erreurs

  11. Utiliser des limites d'erreur — Dans React, encapsuler les composants dans des limites d'erreur

  12. Mettre en place des disjoncteurs — Arrêter les tentatives si les erreurs persistent

Tester les scénarios d'erreur

// Simulate network error
const mockNetworkError = () => {
  throw new TypeError('Failed to fetch');
};

// Simulate GraphQL error
const mockGraphQLError = () => {
  const error = new Error('GraphQL Error');
  error.graphQLErrors = [
    {
      message: 'Unauthenticated',
      extensions: { code: 'UNAUTHENTICATED' },
    },
  ];
  throw error;
};

// Simulate rate limit
const mockRateLimitError = () => {
  const error = new Error('Rate Limited');
  error.graphQLErrors = [
    {
      message: 'Too many requests',
      extensions: { code: 'RATE_LIMITED' },
    },
  ];
  throw error;
};

// Test your error handler
test('handles network errors', async () => {
  const handler = new ErrorHandler();
  const result = await handler.handleOperation(mockNetworkError);
  expect(result).toBeNull();
});

test('handles auth errors', async () => {
  const handler = new ErrorHandler({
    onAuthError: jest.fn(),
  });
  await handler.handleOperation(mockGraphQLError);
  expect(handler.onAuthError).toHaveBeenCalled();
});

test('retries on transient errors', async () => {
  let attempts = 0;
  const operation = async () => {
    attempts++;
    if (attempts < 3) throw new Error('Temporary error');
    return 'success';
  };

  const result = await new RetryManager().retry(operation);
  expect(result).toBe('success');
  expect(attempts).toBe(3);
});

Résumé

Type d'erreur

Cause

Détection

Correction

Réseau

Problèmes de connexion

TypeError, networkError

Réessayer avec un délai

Auth

Jeton invalide/expiré

Code NON AUTHENTIFIÉ

Actualiser le jeton ou se réauthentifier

Entrée

Données non valides

Code BAD_USER_INPUT

Valider la saisie, afficher l'erreur

Introuvable

Ressource manquante

Code NOT_FOUND

Vérifier les identifiants, afficher un message

Limite de débit

Trop de requêtes

Code RATE_LIMITED

Attendre et réessayer

Serveur

Erreur interne

Statut 500+

Réessayer avec un délai

WebSocket

Connexion perdue

Erreur d'abonnement

Reconnecter automatiquement