Angular HttpClient та Interceptor'и у 2026: Обробка HTTP-запитів та Питання на Співбесідах

Повний посібник з функціональних interceptor'ів Angular 20, від впровадження токенів автентифікації до логіки повторних спроб з експоненціальною затримкою. Містить питання для співбесід та production-патерни.

Angular HttpClient та Interceptor'и у 2026: Обробка HTTP-запитів та Питання на Співбесідах

Interceptor'и Angular HttpClient трансформують спосіб обробки HTTP-запитів у додатках — від додавання заголовків автентифікації до реалізації логіки повторних спроб. Angular 20 стандартизував функціональні interceptor'и як рекомендований підхід, замінивши старіший патерн на основі класів більш передбачуваним та оптимізованим для tree-shaking API.

Функціональні Interceptor'и — це Стандарт

Angular 20 рекомендує функціональні interceptor'и з withInterceptors() замість interceptor'ів на основі класів. Функціональні interceptor'и мають більш передбачуваний порядок виконання та краще інтегруються зі standalone-компонентами.

Налаштування HttpClient з provideHttpClient

Перед написанням interceptor'ів необхідно налаштувати HttpClient у bootstrap додатку. Standalone-архітектура Angular використовує provideHttpClient() замість застарілого HttpClientModule.

Налаштування відбувається у конфігурації додатку:

main.tstypescript
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { authInterceptor } from './interceptors/auth.interceptor';
import { loggingInterceptor } from './interceptors/logging.interceptor';

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(
      // Interceptor'и виконуються у порядку масиву
      withInterceptors([authInterceptor, loggingInterceptor])
    ),
  ],
});

Interceptor'и виконуються у порядку, визначеному у масиві. Перший interceptor обробляє вихідний запит першим, а останній interceptor першим бачить відповідь.

Анатомія Функціонального Interceptor'а

Функціональний interceptor отримує запит та функцію next. Виклик next(req) передає запит наступному interceptor'у у ланцюжку або до backend'у, якщо це останній interceptor.

Базова структура виглядає так:

interceptors/logging.interceptor.tstypescript
import { HttpInterceptorFn, HttpRequest, HttpHandlerFn, HttpEvent } from '@angular/common/http';
import { Observable, tap } from 'rxjs';

export const loggingInterceptor: HttpInterceptorFn = (
  req: HttpRequest<unknown>,
  next: HttpHandlerFn
): Observable<HttpEvent<unknown>> => {
  const startTime = performance.now();
  console.log(`[HTTP] ${req.method} ${req.url}`);

  return next(req).pipe(
    tap({
      next: () => {
        const duration = Math.round(performance.now() - startTime);
        console.log(`[HTTP] ${req.method} ${req.url} completed in ${duration}ms`);
      },
      error: (err) => {
        console.error(`[HTTP] ${req.method} ${req.url} failed:`, err.message);
      },
    })
  );
};

Interceptor логує метод запиту та URL, потім вимірює час відповіді. Оператор tap спостерігає за відповіддю без її модифікації.

Interceptor Автентифікації з Впровадженням Токена

Більшість додатків потребують додавання токенів автентифікації до вихідних запитів. Interceptor клонує запит із заголовком Authorization, оскільки об'єкти HttpRequest є незмінними.

Цей патерн отримує токен із сервісу та додає його:

interceptors/auth.interceptor.tstypescript
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  // Впровадження сервісів за допомогою функції inject()
  const authService = inject(AuthService);
  const token = authService.getAccessToken();

  // Пропуск заголовка auth для публічних endpoint'ів
  if (req.url.includes('/public/') || !token) {
    return next(req);
  }

  // Клонування запиту із заголовком Authorization
  const authReq = req.clone({
    setHeaders: {
      Authorization: `Bearer ${token}`,
    },
  });

  return next(authReq);
};

Функція inject() працює, оскільки interceptor'и виконуються у контексті впровадження залежностей Angular. Це усуває шаблонний код конструктора з interceptor'ів на основі класів.

Обробка Помилок та Логіка Повторних Спроб

Мережеві помилки трапляються. Interceptor повторних спроб може автоматично повторювати невдалі запити з експоненціальною затримкою, покращуючи надійність без необхідності змін у кожному HTTP-виклику.

Оператори RxJS обробляють логіку повторних спроб:

interceptors/retry.interceptor.tstypescript
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { retry, timer } from 'rxjs';

export const retryInterceptor: HttpInterceptorFn = (req, next) => {
  // Повторювати лише GET-запити (ідемпотентні)
  if (req.method !== 'GET') {
    return next(req);
  }

  return next(req).pipe(
    retry({
      count: 3,
      delay: (error: HttpErrorResponse, retryCount: number) => {
        // Не повторювати помилки клієнта (4xx)
        if (error.status >= 400 && error.status < 500) {
          throw error;
        }
        // Експоненціальна затримка: 1с, 2с, 4с
        const delayMs = Math.pow(2, retryCount - 1) * 1000;
        console.log(`Повторна спроба #${retryCount} через ${delayMs}мс`);
        return timer(delayMs);
      },
    })
  );
};

Interceptor повторює лише GET-запити, оскільки POST, PUT та DELETE не є ідемпотентними. Помилки клієнта (4xx) пропускають повторні спроби, оскільки сервер явно відхилив запит.

Готовий до співбесід з Angular?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Кешування Відповідей з HttpContext

HttpClient надає HttpContext для передачі метаданих між interceptor'ами та кодом, що викликає. Кешуючий interceptor може читати токени контексту для визначення поведінки кешу.

Токен кешу та interceptor працюють разом:

interceptors/cache.interceptor.tstypescript
import { HttpInterceptorFn, HttpContextToken, HttpResponse } from '@angular/common/http';
import { of, tap } from 'rxjs';

// Токен контексту для контролю кешування на рівні запиту
export const CACHE_REQUEST = new HttpContextToken<boolean>(() => false);

const cache = new Map<string, HttpResponse<unknown>>();

export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
  // Кешувати лише якщо явно запитано через контекст
  if (!req.context.get(CACHE_REQUEST) || req.method !== 'GET') {
    return next(req);
  }

  const cacheKey = req.urlWithParams;
  const cachedResponse = cache.get(cacheKey);

  if (cachedResponse) {
    console.log(`[Cache] HIT: ${cacheKey}`);
    return of(cachedResponse.clone());
  }

  return next(req).pipe(
    tap((event) => {
      if (event instanceof HttpResponse) {
        console.log(`[Cache] STORE: ${cacheKey}`);
        cache.set(cacheKey, event.clone());
      }
    })
  );
};

Код, що викликає, вмикає кешування за допомогою токена контексту:

services/user.service.tstypescript
import { HttpClient, HttpContext } from '@angular/common/http';
import { CACHE_REQUEST } from '../interceptors/cache.interceptor';

export class UserService {
  constructor(private http: HttpClient) {}

  getUser(id: string) {
    return this.http.get(`/api/users/${id}`, {
      context: new HttpContext().set(CACHE_REQUEST, true),
    });
  }
}

Цей патерн зберігає поведінку кешування явною у місці виклику, замість приховування у глобальній конфігурації.

Оновлення Токена з Обробкою 401

Коли токени доступу закінчуються, сервер повертає відповідь 401. Interceptor може перехопити це, оновити токен та прозоро повторити оригінальний запит.

Логіка оновлення координує декілька одночасних запитів:

interceptors/token-refresh.interceptor.tstypescript
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, switchMap, throwError, BehaviorSubject, filter, take } from 'rxjs';
import { AuthService } from '../services/auth.service';

let isRefreshing = false;
const refreshTokenSubject = new BehaviorSubject<string | null>(null);

export const tokenRefreshInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);

  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status !== 401 || req.url.includes('/auth/refresh')) {
        return throwError(() => error);
      }

      if (isRefreshing) {
        // Чекати на завершення оновлення
        return refreshTokenSubject.pipe(
          filter((token) => token !== null),
          take(1),
          switchMap((token) => {
            const retryReq = req.clone({
              setHeaders: { Authorization: `Bearer ${token}` },
            });
            return next(retryReq);
          })
        );
      }

      isRefreshing = true;
      refreshTokenSubject.next(null);

      return authService.refreshToken().pipe(
        switchMap((response) => {
          isRefreshing = false;
          refreshTokenSubject.next(response.accessToken);
          const retryReq = req.clone({
            setHeaders: { Authorization: `Bearer ${response.accessToken}` },
          });
          return next(retryReq);
        }),
        catchError((refreshError) => {
          isRefreshing = false;
          authService.logout();
          return throwError(() => refreshError);
        })
      );
    })
  );
};

BehaviorSubject запобігає багаторазовим викликам оновлення, коли декілька запитів завершуються невдачею одночасно. Після оновлення токена всі запити у черзі повторюються з новим токеном.

Питання для Співбесід: HttpClient та Interceptor'и

Технічні співбесіди часто досліджують розуміння патернів обробки HTTP. Ці питання часто зустрічаються на позиціях Angular.

П: Чому функціональні interceptor'и переважають над interceptor'ами на основі класів?

Функціональні interceptor'и мають більш передбачуваний порядок виконання. Документація Angular зазначає, що функціональні interceptor'и з withInterceptors() зберігають строгий порядок масиву, тоді як interceptor'и на основі DI можуть мати проблеми з порядком у складних ієрархіях модулів. Функціональні interceptor'и також створюють менші bundle'и, оскільки краще піддаються tree-shaking.

П: Як запобігти виконанню interceptor'а для конкретних запитів?

Існують два підходи. По-перше, перевірка патерну URL в interceptor'і та виклик next(req) без модифікації для виключених шляхів. По-друге, використання токенів HttpContext, які код, що викликає, встановлює для відмови від поведінки interceptor'а. Підхід з контекстом зберігає рішення про виключення у місці виклику.

П: Що відбувається, якщо забути викликати next() в interceptor'і?

Запит ніколи не досягне сервера. Observable, повернутий next(), представляє решту ланцюжка interceptor'ів та фактичний HTTP-виклик. Без його виклику downstream interceptor'и та backend ніколи не побачать запит. Це навмисно для кешуючих interceptor'ів, які повертають кешовані відповіді.

П: Як обробляти одночасні запити під час оновлення токена?

Використовуйте BehaviorSubject для координації. Коли перша помилка 401 запускає оновлення, встановіть прапорець та випустіть null у subject. Наступні помилки 401 чекають на subject з filter, поки не надійде новий токен. Це запобігає множинним викликам оновлення та гарантує, що всі очікуючі запити повторяться з тим самим свіжим токеном.

П: Чи можуть interceptor'и змінювати тіло відповіді?

Так. Використовуйте map() на Observable, повернутому next(), щоб трансформувати об'єкти HttpResponse. Оскільки відповіді незмінні, клонуйте їх зі зміненими тілами. Цей патерн працює для додавання обчислених полів, розпаковування API-обгорток або нормалізації структур даних.

Більше матеріалів для підготовки до співбесід з Angular у модулі питань для співбесід HttpClient.

Тестування Функціональних Interceptor'ів

Interceptor'и вимагають тестування з mock'аними HTTP-handler'ами. HttpTestingController Angular працює з interceptor'ами, зареєстрованими через provideHttpClient().

Налаштування тесту виглядає так:

interceptors/auth.interceptor.spec.tstypescript
import { TestBed } from '@angular/core/testing';
import { HttpClient, provideHttpClient, withInterceptors } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { authInterceptor } from './auth.interceptor';
import { AuthService } from '../services/auth.service';

describe('authInterceptor', () => {
  let httpClient: HttpClient;
  let httpTesting: HttpTestingController;
  let authServiceSpy: jasmine.SpyObj<AuthService>;

  beforeEach(() => {
    authServiceSpy = jasmine.createSpyObj('AuthService', ['getAccessToken']);

    TestBed.configureTestingModule({
      providers: [
        provideHttpClient(withInterceptors([authInterceptor])),
        provideHttpClientTesting(),
        { provide: AuthService, useValue: authServiceSpy },
      ],
    });

    httpClient = TestBed.inject(HttpClient);
    httpTesting = TestBed.inject(HttpTestingController);
  });

  it('should add Authorization header when token exists', () => {
    authServiceSpy.getAccessToken.and.returnValue('test-token');

    httpClient.get('/api/data').subscribe();

    const req = httpTesting.expectOne('/api/data');
    expect(req.request.headers.get('Authorization')).toBe('Bearer test-token');
    req.flush({ data: 'test' });
  });

  it('should skip Authorization for public endpoints', () => {
    authServiceSpy.getAccessToken.and.returnValue('test-token');

    httpClient.get('/public/health').subscribe();

    const req = httpTesting.expectOne('/public/health');
    expect(req.request.headers.has('Authorization')).toBeFalse();
    req.flush({ status: 'ok' });
  });
});

Тест перевіряє як впровадження заголовка, так і виключення для публічних endpoint'ів без виконання реальних мережевих запитів.

Production-патерни: Поєднання Interceptor'ів

Реальні додатки складають декілька interceptor'ів у стек. Порядок має значення: автентифікація повинна бути перед логуванням, а кешування — перед логікою повторних спроб.

Production-конфігурація може виглядати так:

main.tstypescript
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { authInterceptor } from './interceptors/auth.interceptor';
import { tokenRefreshInterceptor } from './interceptors/token-refresh.interceptor';
import { cacheInterceptor } from './interceptors/cache.interceptor';
import { retryInterceptor } from './interceptors/retry.interceptor';
import { loggingInterceptor } from './interceptors/logging.interceptor';

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(
      withInterceptors([
        // Порядок: auth -> refresh -> cache -> retry -> logging
        authInterceptor,
        tokenRefreshInterceptor,
        cacheInterceptor,
        retryInterceptor,
        loggingInterceptor,
      ])
    ),
  ],
});

Цей порядок гарантує, що токени додаються перед обробкою оновлення, перевірки кешу відбуваються перед повторними спробами, а логування фіксує кінцевий час.

Більше патернів Angular у модулі основ RxJS, який охоплює оператори, що використовуються в interceptor'ах.

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Ключові Висновки для Interceptor'ів Angular HttpClient

  • Функціональні interceptor'и з withInterceptors() є стандартом Angular 20, пропонуючи передбачуваний порядок та кращу оптимізацію tree-shaking порівняно з альтернативами на основі класів
  • Використовуйте inject() всередині interceptor'ів для доступу до сервісів без шаблонного коду конструктора
  • Клонуйте запити за допомогою req.clone() для модифікації заголовків, оскільки об'єкти HttpRequest незмінні
  • Токени HttpContext роблять поведінку на рівні запиту явною у місці виклику, замість приховування у глобальній конфігурації
  • Interceptor'и оновлення токенів повинні координувати одночасні запити за допомогою BehaviorSubject, щоб запобігти множинним викликам оновлення
  • Порядок interceptor'ів має значення: автентифікація перед кешуванням, кешування перед повторними спробами, логування в кінці
Щоденний виклик

Чи знайдеш ти помилку в Angular?

Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Anthony Fillion-Maillet

Автор:

Anthony Fillion-Maillet

Засновник SharpSkill

Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.

Оновлено 22 серпня 2026 р.

Поділитися

Пов'язані статті