Axios Interceptor — token qo'shish, xatolarni markaziy ushlash

~20 daq · React · Ekotizim va Amaliyot

Muammodan boshlaylik

Ilovada 40 ta API so'rov bor. Har biriga:

const token = localStorage.getItem('token');
const j = await axios.get('https://api.dokon.uz/v1/buyurtmalar', {
  headers: { Authorization: `Bearer ${token}` },
});

Va har birida:

} catch (err) {
  if (err.response?.status === 401) {
    localStorage.removeItem('token');
    window.location.href = '/login';
  }
  if (err.response?.status === 500) toast.error('Server xatosi');
}

40 marta takrorlangan kod. Bir kun backend token formatini o'zgartiradi — 40 ta faylni tahrirlash kerak. Bitta joyda unutsangiz — xato.

Interceptor (ushlab qoluvchi) — barcha so'rovlar va javoblar o'tadigan markaziy nuqta. Umumiy mantiqni bir marta yozasiz.


Axios instance

Avval — loyiha uchun alohida Axios nusxasi:

// src/api/client.js
import axios from 'axios';

export const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL,     // https://api.dokon.uz/v1
  timeout: 10_000,
  headers: { 'Content-Type': 'application/json' },
});
// Endi har joyda
const { data } = await api.get('/buyurtmalar');
  • baseURL — har so'rovda to'liq manzil yozish shart emas
  • timeout — 10 soniyada javob bo'lmasa — xato (standart holatda cheksiz kutadi)
  • Global axios ga tegmaymiz — boshqa API lar (masalan, uchinchi tomon xizmati) uchun alohida instance bo'lishi mumkin

Request interceptor — har so'rovdan oldin

import { useAuthStore } from '@/store/authStore';

api.interceptors.request.use((config) => {
  const token = useAuthStore.getState().token;      // Zustand — React dan tashqarida (57-dars)
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  config.headers['Accept-Language'] = i18n.language;   // 86-dars
  return config;                                       // ← majburiy
});

Funksiya so'rov sozlamalarini (config) oladi, o'zgartiradi va qaytaradi. return config ni unutsangiz — so'rov ketmaydi.

Endi 40 ta so'rovning hech birida token haqida o'ylash shart emas.


Response interceptor — har javobdan keyin

api.interceptors.response.use(
  (javob) => javob,                     // 2xx — muvaffaqiyat: o'zgarishsiz
  (xato) => {                           // 4xx, 5xx, tarmoq xatosi
    const status = xato.response?.status;

    if (!xato.response) {
      toast.error("Internet aloqasi yo'q");
    } else if (status === 403) {
      toast.error("Bu amal uchun ruxsatingiz yo'q");
    } else if (status >= 500) {
      toast.error("Serverda xatolik. Keyinroq urinib ko'ring");
    }

    return Promise.reject(xato);        // ← xatoni chaqiruvchiga uzatish
  }
);
  • Birinchi funksiya — muvaffaqiyatli javob uchun
  • Ikkinchi — xato uchun
  • Promise.reject(xato) — xatoni davom ettirish: TanStack Query, try/catch yoki Error Boundary uni baribir oladi

[!WARNING] Xato interceptor ida return Promise.reject(xato) ni unutsangiz, xato yutib yuboriladi: chaqiruvchi kod undefined ni "muvaffaqiyatli javob" deb oladi. TanStack Query isSuccess: true ko'rsatadi, data esa undefined — topish qiyin xato.

Ma'lumotni "ochish"

Ko'p API lar javobni o'raydi: { success: true, data: {...} }. Interceptor bilan:

api.interceptors.response.use((javob) => javob.data.data);
// Endi: const buyurtmalar = await api.get('/buyurtmalar');   — to'g'ridan-to'g'ri ma'lumot

Lekin ehtiyot bo'ling: bu barcha so'rovlarga ta'sir qiladi va header lar, status kabi ma'lumotlarga yetib bo'lmay qoladi. Ko'pincha API funksiyalarida (return data) ochish aniqroq (89-dars).


401 — token muddati tugadi

Eng muhim stsenariy. Odatda ikki token bo'ladi:

  • Access token — qisqa muddatli (15 daqiqa), har so'rovda yuboriladi
  • Refresh token — uzoq muddatli (30 kun), faqat yangi access token olish uchun

Access token muddati tugadi → server 401 qaytaradi. Foydalanuvchini login sahifasiga haydash o'rniga — jimgina yangi token olib, so'rovni qaytadan yuborish:

let yangilanmoqda = null;      // bir vaqtda faqat bitta refresh so'rovi

api.interceptors.response.use(
  (javob) => javob,
  async (xato) => {
    const asl = xato.config;

    if (xato.response?.status === 401 && !asl._qaytaUrinish && !asl.url.includes('/auth/refresh')) {
      asl._qaytaUrinish = true;                       // cheksiz siklni oldini olish

      try {
        yangilanmoqda ??= tokenniYangila();           // hamma bitta Promise ni kutadi
        const yangiToken = await yangilanmoqda;
        asl.headers.Authorization = `Bearer ${yangiToken}`;
        return api(asl);                              // so'rovni qaytadan yuborish
      } catch {
        useAuthStore.getState().chiqish();
        window.location.assign('/login');
        return Promise.reject(xato);
      } finally {
        yangilanmoqda = null;
      }
    }

    return Promise.reject(xato);
  }
);

async function tokenniYangila() {
  const { data } = await axios.post(                  // instance emas — interceptor siz
    `${import.meta.env.VITE_API_URL}/auth/refresh`,
    {},
    { withCredentials: true }                         // refresh token — httpOnly cookie da
  );
  useAuthStore.getState().setToken(data.accessToken);
  return data.accessToken;
}

Muhim nuqtalar:

Nuqta Nima uchun
_qaytaUrinish bayrog'i Yangi token bilan ham 401 bo'lsa — cheksiz siklga tushmaslik
/auth/refresh ni istisno qilish Refresh ning o'zi 401 bersa — qayta refresh qilmaslik
Umumiy yangilanmoqda Promise Sahifa 5 ta so'rov yubordi, hammasi 401 oldi → bitta refresh so'rovi, beshalasi uni kutadi
Refresh — oddiy axios bilan Instance interceptor lari refresh so'roviga aralashmasin
Muvaffaqiyatsiz bo'lsa — chiqish Refresh token ham eskirgan — qayta kirish kerak

Foydalanuvchi hech narsani sezmaydi — so'rov biroz kechikib, lekin muvaffaqiyatli bajariladi.


Interceptor ni olib tashlash

const id = api.interceptors.request.use(fn);
api.interceptors.request.eject(id);

React komponentida interceptor qo'shilsa (masalan, Context dagi token bilan), effekt cleanup ida olib tashlash shart — aks holda har renderda yangi interceptor qo'shiladi (84-dars):

useEffect(() => {
  const id = api.interceptors.request.use((config) => { ... });
  return () => api.interceptors.request.eject(id);
}, [token]);

Lekin yaxshiroq — interceptor larni modul darajasida bir marta yozish va token ni store dan getState() bilan olish (yuqoridagidek).


fetch bilan

Axios siz ham xuddi shu g'oyani o'z funksiyangiz bilan amalga oshirish mumkin:

// src/api/client.js
export async function apiFetch(yol, options = {}) {
  const token = useAuthStore.getState().token;

  const j = await fetch(`${import.meta.env.VITE_API_URL}${yol}`, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...(token && { Authorization: `Bearer ${token}` }),
      ...options.headers,
    },
    signal: options.signal ?? AbortSignal.timeout(10_000),
  });

  if (j.status === 401) { /* refresh mantiqi */ }
  if (!j.ok) throw new ApiXato(j.status, await j.json().catch(() => null));

  return j.status === 204 ? null : j.json();
}

"Interceptor" — bu shunchaki barcha so'rovlar o'tadigan bitta funksiya. Axios uni tayyor holda beradi.

[!TIP] Token ni qayerda saqlash — muhim xavfsizlik savoli. localStorage dagi token sahifadagi har qanday JavaScript (XSS hujumi) tomonidan o'qilishi mumkin. Keng tarqalgan xavfsizroq yondashuv: access token — xotirada (Zustand, persist siz), refresh token — server o'rnatgan httpOnly cookie da (JavaScript uni umuman ko'rmaydi). Sahifa yangilanganda ilova refresh orqali yangi access token oladi.


Nima uchun shunday?

Nima uchun autentifikatsiya va xatolarni qayta ishlashni markazlashtirish shunchalik muhim?

Chunki bu — "cross-cutting concern": ilovaning har bir qismiga tegishli, lekin hech qaysi birining asosiy vazifasi emas. Buyurtmalar sahifasi buyurtmalar haqida o'ylashi kerak, token formati yoki refresh mantiqi haqida emas. Bunday mantiq har joyda takrorlansa, uch muammo paydo bo'ladi: o'zgartirish qimmat (40 ta fayl), izchillik yo'q (bir joyda 403 boshqacha ko'rsatiladi) va xavfsizlik zaif (bir joyda token qo'shishni unutish). Interceptor — "middleware" g'oyasining frontend versiyasi: Express dagi middleware lar kabi, u har bir so'rov va javobni o'z qo'lidan o'tkazadi va umumiy qoidalarni bir joyda qo'llaydi.


Xulosa

Eslab qoling:

  • axios.create({ baseURL, timeout }) — loyiha uchun alohida instance
  • interceptors.request.use(config => ...; return config) — token, til, umumiy header lar
  • interceptors.response.use(ok, xato => ...; return Promise.reject(xato)) — markaziy xatolar
  • Promise.reject ni unutmang — aks holda xato yutiladi
  • 401 + refresh token: _qaytaUrinish bayrog'i, refresh ni istisno qilish, umumiy Promise, so'rovni qayta yuborish
  • Interceptor lar — modul darajasida; komponentda bo'lsa eject bilan cleanup
  • Token: access — xotirada, refresh — httpOnly cookie da

Keyingi dars

86-dars: i18n — react-i18next bilan ko'p tilli ilova