Angular HttpClient và Interceptor năm 2026: Xử lý Request và Câu hỏi Phỏng vấn

Tìm hiểu cách sử dụng Angular HttpClient và functional interceptor để xử lý HTTP request, xác thực token, retry logic và caching. Bao gồm các câu hỏi phỏng vấn Angular thường gặp.

Angular HttpClient và Interceptor cho xử lý HTTP request

Angular HttpClient interceptor thay đổi cách ứng dụng xử lý HTTP request, từ việc thêm header xác thực đến triển khai retry logic. Angular 20 đã chuẩn hóa functional interceptor như phương pháp được khuyến nghị, thay thế pattern dựa trên class bằng API dễ dự đoán hơn và có khả năng tree-shakable tốt hơn.

Functional Interceptor Là Tiêu Chuẩn

Angular 20 khuyến nghị sử dụng functional interceptor với withInterceptors() thay vì class-based interceptor. Functional interceptor có thứ tự thực thi dễ dự đoán hơn và tích hợp tốt hơn với standalone component.

Thiết lập HttpClient với provideHttpClient

Trước khi viết interceptor, HttpClient cần được cấu hình trong bootstrap ứng dụng. Kiến trúc standalone của Angular sử dụng provideHttpClient() thay vì HttpClientModule cũ.

Việc thiết lập được thực hiện trong cấu hình ứng dụng:

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 được thực thi theo thứ tự mảng
      withInterceptors([authInterceptor, loggingInterceptor])
    ),
  ],
});

Interceptor được thực thi theo thứ tự được chỉ định trong mảng. Interceptor đầu tiên xử lý outgoing request trước, và interceptor cuối cùng nhìn thấy response trước.

Cấu trúc Functional Interceptor

Functional interceptor nhận request và hàm next. Gọi next(req) chuyển request đến interceptor tiếp theo trong chuỗi hoặc đến backend nếu đây là interceptor cuối cùng.

Cấu trúc cơ bản như sau:

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 này ghi lại method và URL của request, sau đó đo thời gian response. Operator tap quan sát response mà không sửa đổi nó.

Authentication Interceptor với Token Injection

Hầu hết các ứng dụng cần đính kèm authentication token vào outgoing request. Interceptor clone request với header Authorization, vì đối tượng HttpRequest là immutable.

Pattern này lấy token từ service và đính kèm nó:

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 service sử dụng hàm inject() của Angular
  const authService = inject(AuthService);
  const token = authService.getAccessToken();

  // Bỏ qua auth header cho public endpoint
  if (req.url.includes('/public/') || !token) {
    return next(req);
  }

  // Clone request với header Authorization
  const authReq = req.clone({
    setHeaders: {
      Authorization: `Bearer ${token}`,
    },
  });

  return next(authReq);
};

Hàm inject() hoạt động vì interceptor được thực thi trong injection context của Angular. Điều này loại bỏ boilerplate constructor injection từ class-based interceptor.

Xử lý Error và Retry Logic

Lỗi mạng có thể xảy ra bất cứ lúc nào. Retry interceptor có thể tự động thử lại các request thất bại với exponential backoff, cải thiện độ tin cậy mà không cần thay đổi mọi HTTP call.

Các operator RxJS xử lý retry logic:

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

export const retryInterceptor: HttpInterceptorFn = (req, next) => {
  // Chỉ retry GET request (idempotent)
  if (req.method !== 'GET') {
    return next(req);
  }

  return next(req).pipe(
    retry({
      count: 3,
      delay: (error: HttpErrorResponse, retryCount: number) => {
        // Không retry client error (4xx)
        if (error.status >= 400 && error.status < 500) {
          throw error;
        }
        // Exponential backoff: 1s, 2s, 4s
        const delayMs = Math.pow(2, retryCount - 1) * 1000;
        console.log(`Retry #${retryCount} in ${delayMs}ms`);
        return timer(delayMs);
      },
    })
  );
};

Interceptor này chỉ thử lại GET request vì POST, PUT và DELETE không phải là idempotent. Client error (4xx) bỏ qua retry vì server đã từ chối request một cách rõ ràng.

Sẵn sàng chinh phục phỏng vấn Angular?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Response Caching với HttpContext

HttpClient cung cấp HttpContext để truyền metadata giữa interceptor và code gọi. Caching interceptor có thể đọc context token để xác định hành vi cache.

Cache token và interceptor hoạt động cùng nhau:

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

// Context token để kiểm soát caching theo từng request
export const CACHE_REQUEST = new HttpContextToken<boolean>(() => false);

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

export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
  // Chỉ cache nếu được yêu cầu rõ ràng qua context
  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());
      }
    })
  );
};

Code gọi chọn bật caching với context token:

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),
    });
  }
}

Pattern này làm cho hành vi caching rõ ràng tại call site thay vì ẩn trong cấu hình global.

Token Refresh với Xử lý 401

Khi access token hết hạn, server trả về response 401. Interceptor có thể bắt điều này, refresh token, và thử lại request gốc một cách trong suốt.

Logic refresh phối hợp nhiều concurrent request:

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) {
        // Đợi refresh hoàn thành
        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 ngăn chặn nhiều refresh call khi nhiều request thất bại đồng thời. Sau khi token được refresh, tất cả request đang chờ thử lại với token mới.

Câu hỏi Phỏng vấn: HttpClient và Interceptor

Các cuộc phỏng vấn kỹ thuật thường kiểm tra hiểu biết về các pattern xử lý HTTP. Những câu hỏi này xuất hiện thường xuyên trong các vị trí Angular.

Q: Tại sao functional interceptor được ưu tiên hơn class-based interceptor?

Functional interceptor có thứ tự thực thi dễ dự đoán hơn. Tài liệu Angular nêu rõ rằng functional interceptor với withInterceptors() duy trì thứ tự mảng nghiêm ngặt, trong khi DI-based interceptor có thể gặp vấn đề về thứ tự trong các phân cấp module phức tạp. Functional interceptor cũng tạo ra bundle nhỏ hơn vì tree-shake tốt hơn.

Q: Làm thế nào để ngăn interceptor chạy trên các request cụ thể?

Có hai cách tiếp cận. Thứ nhất, kiểm tra pattern URL trong interceptor và gọi next(req) mà không sửa đổi cho các path được loại trừ. Thứ hai, sử dụng token HttpContext mà code gọi thiết lập để opt-out khỏi hành vi interceptor. Cách tiếp cận context giữ quyết định loại trừ tại call site.

Q: Điều gì xảy ra nếu quên gọi next() trong interceptor?

Request không bao giờ đến server. Observable được trả về bởi next() đại diện cho phần còn lại của interceptor chain và HTTP call thực tế. Nếu không gọi nó, các interceptor downstream và backend không bao giờ nhìn thấy request. Điều này có chủ đích cho caching interceptor trả về cached response.

Q: Làm thế nào để xử lý concurrent request trong quá trình token refresh?

Sử dụng BehaviorSubject để phối hợp. Khi 401 đầu tiên kích hoạt refresh, đặt flag và emit null trên subject. Các 401 tiếp theo chờ trên subject với filter cho đến khi token mới đến. Điều này ngăn chặn nhiều refresh call và đảm bảo tất cả pending request thử lại với cùng token mới.

Q: Interceptor có thể sửa đổi response body không?

Có. Sử dụng map() trên Observable được trả về bởi next() để transform đối tượng HttpResponse. Vì response là immutable, clone chúng với body đã sửa đổi. Pattern này hoạt động để thêm computed field, unwrap API envelope, hoặc normalize cấu trúc dữ liệu.

Để chuẩn bị phỏng vấn Angular thêm, xem module câu hỏi phỏng vấn HttpClient.

Testing Functional Interceptor

Interceptor yêu cầu testing với mocked HTTP handler. HttpTestingController của Angular hoạt động với interceptor được đăng ký qua provideHttpClient().

Thiết lập test như sau:

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' });
  });
});

Test xác minh cả header injection và ngoại lệ public endpoint mà không thực hiện network request thực tế.

Pattern Production: Kết hợp Interceptor

Các ứng dụng thực tế xếp chồng nhiều interceptor. Thứ tự quan trọng: authentication nên chạy trước logging, và caching nên chạy trước retry logic.

Cấu hình production có thể như sau:

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([
        // Thứ tự: auth -> refresh -> cache -> retry -> logging
        authInterceptor,
        tokenRefreshInterceptor,
        cacheInterceptor,
        retryInterceptor,
        loggingInterceptor,
      ])
    ),
  ],
});

Thứ tự này đảm bảo token được đính kèm trước khi xử lý refresh, kiểm tra cache xảy ra trước retry, và logging capture timing cuối cùng.

Để tìm hiểu các pattern Angular rộng hơn, xem module cơ bản RxJS bao gồm các operator được sử dụng trong interceptor.

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Điểm Chính cho Angular HttpClient Interceptor

  • Functional interceptor với withInterceptors() là tiêu chuẩn Angular 20, cung cấp thứ tự dễ dự đoán và tree-shaking tốt hơn so với các phương án dựa trên class
  • Sử dụng inject() bên trong interceptor để truy cập service mà không cần boilerplate constructor
  • Clone request với req.clone() để sửa đổi header vì đối tượng HttpRequest là immutable
  • Token HttpContext làm cho hành vi theo từng request rõ ràng tại call site thay vì ẩn trong cấu hình global
  • Interceptor token refresh nên phối hợp concurrent request với BehaviorSubject để ngăn chặn nhiều refresh call
  • Thứ tự interceptor quan trọng: authentication trước caching, caching trước retry, logging cuối cùng
Thử thách hôm nay

Bạn có tìm ra lỗi trong Angular không?

Một đoạn mã thật, một lỗi ẩn, mỗi ngày một lượt. Không cần tài khoản để thử.

Anthony Fillion-Maillet

Viết bởi

Anthony Fillion-Maillet

Người sáng lập SharpSkill

Lập trình viên fullstack hơn 10 năm. Anh điều hành SharpSkill và chịu trách nhiệm về mọi nội dung đăng tại đây.

Cập nhật ngày 22 tháng 8, 2026

Thẻ

#angular
#httpclient
#interceptor
#http
#typescript

Chia sẻ

Bài viết liên quan