Interceptors HTTP — Angular

HTTP Interceptors mencegat dan memodifikasi HTTP request/response. Functional Interceptor export const authInterceptor: HttpInterceptorFn = (req, next) => {…

HTTP interceptor adalah fungsi yang dilewati setiap request HttpClient sebelum sampai ke server, dan setiap response dalam perjalanan pulang. Ditulis sekali, berlaku untuk semua service. Bayangin loket di depan dapur Martabak Bang Jali. Setiap kertas pesanan, dari pelayan mana pun, lewat loket itu dulu. Petugas loket gak mencoret kertas aslinya: dia membuat salinan yang sudah dicap "LUNAS", lalu salinan itu yang masuk dapur. Dia juga menghitung di papan berapa pesanan yang sedang dimasak, dan kalau dapur teriak "kompor mati, ulang!", dia menyerahkan pesanannya sekali lagi sebelum bilang ke pelayan bahwa pesanannya gagal.

🧪 Tebakan cepat: authInterceptor menulis const authReq = req.clone({ setHeaders: { Authorization: 'Bearer jali-123' } }), lalu return next(req). Header Authorization apa yang diterima server? Jawabannya ada di bagian "Request gak bisa diubah: teruskan salinannya".

Bentuk sebuah interceptor

// src/app/interceptors.ts
import { HttpInterceptorFn } from '@angular/common/http'

export const logInterceptor: HttpInterceptorFn = (req, next) => {
  console.log(req.method, req.url) // GET /api/pesanan/17
  return next(req)
}

Request gak bisa diubah: teruskan salinannya

HttpRequest itu immutable: setelah dibuat, isinya gak bisa diubah. Untuk menambah header, buat salinan dengan req.clone():

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = inject(AuthService).token() // 'jali-123', atau null kalau belum login
  if (!token) {
    return next(req)
  }
  const authReq = req.clone({
    setHeaders: { Authorization: `Bearer ${token}` },
  })
  return next(authReq)
}

Tiga versi ini dijalankan di Playground. Ini yang diterima server:

authInterceptor Server menerima Authorization Jawaban server
return next(authReq) Bearer jali-123 200
return next(req) null 401
req.headers.set('Authorization', ...) lalu next(req) null 401

Versi tebakan tadi ada di baris kedua: salinannya dibuat, lalu dibuang. Server menerima Authorization = null dan menjawab 401. Itu jawaban tebakannya. Baris ketiga gagal karena alasan yang sama: HttpHeaders juga immutable, set() mengembalikan objek header baru, dan hasilnya gak dipakai. Mengisi langsung req.headers = ... bahkan gak lolos kompilasi: TS2540: Cannot assign to 'headers' because it is a read-only property.

Mendaftarkan, dan urutannya

Interceptor didaftarkan sekali di app.config.ts, di dalam provideHttpClient:

// src/app/app.config.ts
import { provideHttpClient, withInterceptors } from '@angular/common/http'
import { authInterceptor, loadingInterceptor, errorInterceptor } from './interceptors'

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withInterceptors([authInterceptor, loadingInterceptor, errorInterceptor])),
  ],
}

Satu klik Cek status pesanan #17 (GET /api/pesanan/17) mencetak ini di console Playground, berurutan:

auth: salinan request diberi Authorization
loading: 1 request jalan
server: GET /api/pesanan/17, Authorization = Bearer jali-123
server: jawab 200
halaman: pesanan #17 dimasak
loading: 0 request jalan

Loading counter

@Injectable({ providedIn: 'root' })
export class LoadingService {
  jalan = signal(0)
}

export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
  const loading = inject(LoadingService)
  loading.jalan.update((n) => n + 1)
  return next(req).pipe(
    finalize(() => loading.jalan.update((n) => n - 1)),
  )
}

Template header cukup menulis @if (loading.jalan() > 0) { <p>Memproses...</p> }. Kenapa angka, bukan true/false? Klik Cek status dua kali dengan cepat: counter-nya 1, 2, 1, 0. Dengan boolean, request pertama yang selesai mematikan tanda Memproses... padahal yang kedua masih jalan. finalize dipanggil di semua akhir: sukses, gagal, atau dibatalkan.

Error: catchError dan retry

export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  const router = inject(Router)
  return next(req).pipe(
    retry(1),
    catchError((err: HttpErrorResponse) => {
      if (err.status === 401) {
        router.navigate(['/login'])
      }
      return throwError(() => err)
    }),
  )
}
Keadaan Percobaan ke server Yang tampil di halaman
Server sibuk (percobaan pertama 500), tanpa retry 500 Gagal: Http failure response for /api/pesanan/17: 500 Internal Server Error
Server sibuk, dengan retry(1) 500, 200 Pesanan #17: dimasak
Belum login, dengan retry(1) 401, 401 Gagal: Http failure response for /api/pesanan/17: 401 Unauthorized

Baris terakhir boros: 401 pasti ditolak lagi. Untuk mengulang cuma error server, pakai bentuk config: retry({ count: 1, delay: (err: HttpErrorResponse) => (err.status >= 500 ? timer(500) : throwError(() => err)) }). Dengan ini 401 langsung diteruskan, dan 500 dicoba lagi setelah 500 ms.

📌 Di Playground: server palsu

Playground gak punya server dan gak membaca app.config.ts. Karena itu kode Lab memasang interceptor keempat, serverPalsu, yang gak memanggil next() dan menjawab seperti server: 401 tanpa header, 500 di percobaan pertama kalau server sibuk, selain itu 200. Isinya dibungkus defer() supaya menjawab sekali per subscribe, seperti request sungguhan; itulah yang diulang retry(1). Daftar interceptor-nya dipasang lewat createEnvironmentInjector([provideHttpClient(withInterceptors([...]))], ...). Di proyek CLI, cukup app.config.ts dan hapus serverPalsu.

Kode lama kadang memakai interceptor berbentuk class (implements HttpInterceptor, didaftarkan lewat HTTP_INTERCEPTORS dan withInterceptorsFromDi()). Idenya sama; untuk kode baru, pakai bentuk fungsi.

⚠️ Jebakan yang sering ketemu

TL;DR: Interceptor adalah fungsi HttpInterceptorFn = (req, next) => ... yang dilewati setiap request HttpClient, didaftarkan sekali dengan provideHttpClient(withInterceptors([...])). Request lewat sesuai urutan array, response kembali terbalik. HttpRequest immutable: tambah header dengan req.clone({ setHeaders: { Authorization: ... } }) dan teruskan salinannya ke next(); next(req) mengirim request tanpa header (server menjawab 401). Pola umum: auth menambah token, loading counter naik saat request lewat dan turun di finalize, error interceptor memakai catchError (misalnya 401 ke /login) dan retry untuk error server.

Yang akan kamu pelajari