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:
authInterceptormenulisconst authReq = req.clone({ setHeaders: { Authorization: 'Bearer jali-123' } }), lalureturn next(req). HeaderAuthorizationapa 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)
}
HttpInterceptorFnadalah tipe untuk interceptor berbentuk fungsi (functional interceptor), gaya yang dipakai Angular modern.reqadalah request yang sedang lewat (HttpRequest): method, URL, header, body.nextmeneruskan request ke interceptor berikutnya, atau ke server kalau sudah yang terakhir. Hasilnya Observable berisi response.- Interceptor wajib mengembalikan hasil
next(...). Lupareturn, dan proyek CLI menolak dikompilasi:TS2322: Type '(req: HttpRequest<unknown>, next: HttpHandlerFn) => void' is not assignable to type 'HttpInterceptorFn'.
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)
}
req.clone({ setHeaders: { ... } })membuat request baru yang sama persis, plus header yang disebut.reqsendiri tetap tanpa header.next(authReq)meneruskan salinannya. Cuma yang diteruskan kenextyang sampai ke server.
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
- Request lewat interceptor sesuai urutan array: auth, loading, error, lalu server. Response kembali dalam urutan terbalik: error, loading, auth, lalu komponen.
- Urutan itu penting. Interceptor yang mencetak header, kalau ditaruh sebelum
authInterceptor, mencetaknull: salinan bertokennya belum dibuat. loading: 0tercetak setelah halaman menerima jawabannya, karenafinalizejalan setelah subscriber selesai.
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)
}),
)
}
inject(Router)dipanggil di badan interceptor, bukan di dalam callbackcatchError.retry(1): kalau gagal, subscribe kenext(req)sekali lagi. Yang diulang cuma bagian setelah errorInterceptor (di sini: server), jadi auth dan loading gak jalan dua kali.catchErrormenangani error di satu tempat untuk semua request, laluthrowError(() => err)meneruskannya supaya halamannya tetap tahu ada yang gagal.
| 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
- Meneruskan
req, bukan salinannya.req.clone()gak mengubahreq. Teruskannext(authReq), atau langsungnext(req.clone({ ... })). - Mengubah header di tempat.
req.headers.set(...)hasilnya dibuang,req.headers = ...ditolak TS2540. Selalu lewatclone. - Lupa
return next(...). Interceptor tanpa return gak lolos kompilasi (TS2322), karena request-nya gak pernah diteruskan. retryuntuk semua error. 401 dan 404 gak sembuh dengan diulang; batasi ke error server.- Detail latihan. Tulis
import { HttpInterceptorFn } from '@angular/common/http'danexport const authInterceptor: HttpInterceptorFn = (req, next) => { ... }. Di dalamnyaconst token = 'my-token',const authReq = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }), lalureturn next(authReq). Penilai membaca kodenya: tipeHttpInterceptorFn,Authorizationdi dalamsetHeaders, dannext()yang menerima hasilclone.
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.