Annulation des requêtes Fetch en cours avec AbortController

Les applications web modernes effectuent fréquemment des requêtes HTTP que les utilisateurs n’ont pas nécessairement besoin de voir aboutir. Un utilisateur qui tape dans une zone de recherche génère plusieurs requêtes, mais seule la plus récente importe. Un utilisateur qui navigue vers une autre page rend toutes les requêtes en attente pour cette page obsolètes. Sans annulation appropriée, ces requêtes inutiles gaspillent la bande passante, consomment les ressources du serveur et peuvent provoquer l’affichage de données périmées dans votre interface utilisateur.
L’API AbortController fournit une méthode propre et standardisée pour annuler les requêtes fetch et autres opérations asynchrones. Cet article vous montre comment implémenter l’annulation de requêtes en utilisant AbortController, couvrant des modèles pratiques comme le debouncing de recherche, le nettoyage de composants et la gestion des timeouts.
Points clés à retenir
- AbortController crée un signal que fetch surveille pour les événements d’annulation
- Vérifiez toujours la présence d’AbortError dans les blocs catch pour distinguer les annulations des échecs
- Annulez les requêtes précédentes dans les interfaces de recherche pour éviter les conditions de course
- Utilisez les fonctions de nettoyage dans les composants de framework pour annuler les requêtes lors du démontage
- Combinez AbortController avec setTimeout pour la fonctionnalité de timeout des requêtes
- Chaque AbortController est à usage unique - créez-en de nouveaux pour la logique de nouvelle tentative
- Node.js 18+ inclut le support natif d’AbortController
Ce que font AbortController et AbortSignal
AbortController crée un objet contrôleur qui gère l’annulation via un AbortSignal associé. Le contrôleur a un seul rôle : appeler abort()
quand vous voulez annuler une opération. Le signal agit comme un canal de communication que fetch et d’autres APIs surveillent pour les événements d’annulation.
const controller = new AbortController()
const signal = controller.signal
// Le signal commence comme non annulé
console.log(signal.aborted) // false
// Appeler abort() change l'état du signal
controller.abort()
console.log(signal.aborted) // true
Quand vous passez un signal à fetch et appelez plus tard abort()
, la promesse fetch est rejetée avec une DOMException
nommée AbortError
. Cela vous permet de distinguer entre les annulations et les véritables échecs réseau.
Implémentation de base d’AbortController
Pour annuler une requête fetch, créez un AbortController, passez son signal à fetch, puis appelez abort()
quand nécessaire :
const controller = new AbortController()
fetch('/api/data', { signal: controller.signal })
.then(response => response.json())
.then(data => console.log('Données reçues:', data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('La requête a été annulée')
} else {
console.error('Échec de la requête:', error)
}
})
// Annuler la requête
controller.abort()
Les points clés :
- Créez le contrôleur avant de faire la requête
- Passez
controller.signal
aux options de fetch - Appelez
controller.abort()
pour annuler - Vérifiez la présence d’
AbortError
dans votre bloc catch
Comment annuler les requêtes de recherche quand les utilisateurs tapent
Les interfaces de recherche déclenchent souvent des requêtes à chaque frappe. Sans annulation, les réponses lentes peuvent arriver dans le désordre, affichant des résultats pour des requêtes obsolètes. Voici comment annuler les requêtes de recherche précédentes :
let searchController = null
function performSearch(query) {
// Annuler toute recherche existante
if (searchController) {
searchController.abort()
}
// Créer un nouveau contrôleur pour cette recherche
searchController = new AbortController()
fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: searchController.signal
})
.then(response => response.json())
.then(results => {
console.log('Résultats de recherche:', results)
updateSearchUI(results)
})
.catch(error => {
if (error.name === 'AbortError') {
console.log('Recherche annulée')
} else {
console.error('Échec de la recherche:', error)
showSearchError()
}
})
}
// Usage : chaque nouvelle recherche annule la précédente
performSearch('javascript')
performSearch('javascript frameworks') // Annule la recherche 'javascript'
Ce modèle garantit que seuls les résultats de recherche les plus récents apparaissent dans votre interface utilisateur, évitant les conditions de course où des requêtes plus lentes écrasent des plus rapides.
Annulation des requêtes lors du démontage de composants
Dans les frameworks frontend, les composants font souvent des requêtes fetch qui devraient être annulées quand le composant est démonté. Cela évite les fuites mémoire et les erreurs dues aux tentatives de mise à jour de composants démontés.
Exemple React
import { useEffect, useState } from 'react'
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const controller = new AbortController()
async function fetchUser() {
try {
const response = await fetch(`/api/users/${userId}`, {
signal: controller.signal
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const userData = await response.json()
setUser(userData)
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Échec du chargement utilisateur:', error)
}
} finally {
if (!controller.signal.aborted) {
setLoading(false)
}
}
}
fetchUser()
// La fonction de nettoyage annule la requête
return () => controller.abort()
}, [userId])
if (loading) return <div>Chargement...</div>
return <div>{user?.name}</div>
}
Exemple Vue
export default {
data() {
return {
user: null,
controller: null
}
},
async mounted() {
this.controller = new AbortController()
try {
const response = await fetch(`/api/users/${this.userId}`, {
signal: this.controller.signal
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
this.user = await response.json()
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Échec du chargement utilisateur:', error)
}
}
},
beforeUnmount() {
if (this.controller) {
this.controller.abort()
}
}
}
Implémentation de timeouts de requête avec AbortController
Les requêtes réseau peuvent rester suspendues indéfiniment. AbortController combiné avec setTimeout
fournit un mécanisme de timeout propre :
function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
const controller = new AbortController()
const timeoutId = setTimeout(() => {
controller.abort()
}, timeoutMs)
return fetch(url, {
...options,
signal: controller.signal
}).finally(() => {
clearTimeout(timeoutId)
})
}
// Usage
fetchWithTimeout('/api/slow-endpoint', {}, 3000)
.then(response => response.json())
.then(data => console.log('Données:', data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Timeout de la requête')
} else {
console.error('Échec de la requête:', error)
}
})
Pour une logique de timeout réutilisable, créez une fonction utilitaire :
function createTimeoutSignal(timeoutMs) {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
// Nettoyer le timeout quand le signal est utilisé
controller.signal.addEventListener('abort', () => {
clearTimeout(timeoutId)
}, { once: true })
return controller.signal
}
// Usage
fetch('/api/data', { signal: createTimeoutSignal(5000) })
.then(response => response.json())
.catch(error => {
if (error.name === 'AbortError') {
console.log('Timeout de la requête ou annulation')
}
})
Gestion d’erreur appropriée pour les requêtes annulées
Distinguez toujours entre les annulations et les véritables erreurs. AbortError indique une annulation intentionnelle, pas un échec :
async function handleRequest(url) {
const controller = new AbortController()
try {
const response = await fetch(url, { signal: controller.signal })
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return await response.json()
} catch (error) {
if (error.name === 'AbortError') {
// C'est attendu quand on annule - ne pas logger comme erreur
console.log('La requête a été annulée')
return null
}
// C'est une véritable erreur qui nécessite une gestion
console.error('Échec de la requête:', error)
throw error
}
}
Pour les applications avec suivi d’erreurs, excluez AbortError des rapports d’erreur :
.catch(error => {
if (error.name === 'AbortError') {
// Ne pas signaler les annulations au suivi d'erreurs
return
}
// Signaler les véritables erreurs
errorTracker.captureException(error)
throw error
})
Gestion de requêtes multiples
Quand vous traitez plusieurs requêtes concurrentes, vous pourriez avoir besoin de toutes les annuler d’un coup ou de les gérer individuellement :
class RequestManager {
constructor() {
this.controllers = new Map()
}
async fetch(key, url, options = {}) {
// Annuler la requête existante avec la même clé
this.cancel(key)
const controller = new AbortController()
this.controllers.set(key, controller)
try {
const response = await fetch(url, {
...options,
signal: controller.signal
})
return response
} finally {
this.controllers.delete(key)
}
}
cancel(key) {
const controller = this.controllers.get(key)
if (controller) {
controller.abort()
this.controllers.delete(key)
}
}
cancelAll() {
for (const controller of this.controllers.values()) {
controller.abort()
}
this.controllers.clear()
}
}
// Usage
const requestManager = new RequestManager()
// Ces requêtes peuvent être gérées indépendamment
requestManager.fetch('user-profile', '/api/user/123')
requestManager.fetch('user-posts', '/api/user/123/posts')
// Annuler une requête spécifique
requestManager.cancel('user-profile')
// Annuler toutes les requêtes en attente
requestManager.cancelAll()
Support navigateur et compatibilité Node.js
AbortController a un excellent support navigateur :
- Chrome 66+
- Firefox 57+
- Safari 12.1+
- Edge 16+
Pour Node.js, AbortController est disponible nativement dans Node 18+. Pour les versions antérieures, utilisez le polyfill abort-controller :
npm install abort-controller
// Pour Node.js < 18
const { AbortController } = require('abort-controller')
// Utiliser normalement
const controller = new AbortController()
Conclusion
AbortController fournit une méthode propre et standardisée pour annuler les requêtes fetch dans les applications web modernes. Les modèles clés sont : créer un contrôleur avant chaque requête, passer son signal à fetch, et appeler abort() quand l’annulation est nécessaire. Gérez toujours AbortError séparément des véritables échecs réseau pour éviter de traiter les annulations intentionnelles comme des erreurs.
Les cas d’usage les plus courants—gestion des requêtes de recherche, nettoyage de composants et gestion des timeouts—suivent des modèles simples que vous pouvez adapter à vos besoins spécifiques. Avec une implémentation appropriée, l’annulation de requêtes améliore à la fois l’expérience utilisateur et les performances de l’application en évitant l’activité réseau inutile et les mises à jour de données périmées.
Prêt à implémenter l’annulation de requêtes dans votre application ? Commencez par le modèle de base pour votre cas d’usage le plus courant, puis étendez vers des scénarios plus complexes selon les besoins. L’investissement dans une gestion appropriée des requêtes porte ses fruits en termes de performances d’application et d’expérience utilisateur.
FAQ
Non, une fois abort() appelé, le signal du contrôleur reste définitivement dans l'état annulé. Vous devez créer un nouveau AbortController pour chaque nouvelle requête ou tentative de retry.
Non, abort() empêche seulement votre code JavaScript de traiter la réponse. Si la requête a déjà été envoyée sur le réseau, le serveur peut encore la recevoir et la traiter.
Créez plusieurs contrôleurs et stockez-les dans un tableau ou une Map, puis appelez abort() sur chacun. Alternativement, utilisez un seul contrôleur et passez son signal à plusieurs appels fetch - appeler abort() une fois les annulera tous.
Il ne se passe rien. L'appel abort() est ignoré si la requête s'est déjà terminée avec succès ou a échoué pour d'autres raisons.
Oui, AbortController fonctionne de manière identique avec les chaînes de Promise et async/await. La promesse fetch sera rejetée avec une AbortError quand annulée, que vous pouvez capturer avec des blocs try/catch.