4.2
Siklus Request dan Retry
Semua method pembantu pada httpclient.Client berakhir di satu pintu: Get, Post, Put, Patch, dan Delete membangun *http.Request lalu memanggil c.Do(request) tanpa logika retry sendiri (httpclient/client.go:57-671). Karena itu seluruh siklus percobaan, pengecekan status code, dan perakitan error hidup di dalam Client.Do. NewClient memulai dengan retryCount bernilai 0 dan heimdall.NewNoRetrier(), sehingga tanpa option hanya ada satu percobaan (httpclient/client.go:27-392). Cara memasang WithRetryCount, WithRetrier, dan option lain dibahas di Option pada httpclient.
| Tahap | Fungsi | Perilaku |
|---|---|---|
| Penyiapan body | internal.SetRequestGetBody | body dibaca ke buffer dan GetBody dipasang; hanya dijalankan bila retryCount > 0 (internal/request_helper.go:11-313) |
| Reset body | internal.CloneRequest | shallow clone lalu Body diganti dari reqGetBody (internal/request_helper.go:33-454) |
| Jeda | internal.SleepInterruptible | menunggu c.retrier.NextInterval(i-1) dan batal bila context selesai (internal/sleep.go:9-165) |
| Notifikasi plugin | reportRequestStart, reportError, reportRequestEnd | dipanggil pada setiap percobaan (httpclient/client.go:204-2206) |
| Keputusan berhenti | skipRetry | context selesai atau budget retry habis (httpclient/client.go:195-2027) |
| Perakitan error | internal.BuildMultiError | menggabungkan errs menjadi satu error (internal/multi_error.go:5-148) |
Penyiapan body request
Do langsung menutup body request asli lewat defer, karena SetRequestGetBody akan membungkus body itu dengan noop closer (httpclient/client.go:122-1399). Penyiapan body hanya dijalankan bila c.retryCount > 0, sehingga request tanpa retry tidak membayar overhead pembacaan body. Hasilnya juga disalin ke variabel lokal reqGetBody, sebab request.GetBody bisa ditimpa plugin atau middleware di antara percobaan.
SetRequestGetBody melewati request yang nil, tidak punya body, atau sudah memiliki GetBody. Untuk http.NoBody ia memasang getter ringan tanpa alokasi buffer. Untuk body biasa ia membaca seluruh isi ke buf, mengganti r.Body dengan newBytesBody(buf), lalu memasang GetBody yang membuat reader baru dari buffer yang sama (internal/request_helper.go:11-313). Efeknya satu salinan body dapat diputar ulang sebanyak jumlah percobaan.
Siklus satu request
sequenceDiagram
autonumber
participant Caller
participant Client as "Client.Do"
participant Helper as "internal request helper"
participant Retrier as "c.retrier"
participant Sleeper as "internal.SleepInterruptible"
participant Doer as "c.client.Do"
participant Plugins as "c.plugins"
participant Budget as "retryErrorBudget"
participant Multi as "internal.BuildMultiError"
Caller->>Client: Do request
Note over Client: defer close body request asli
alt retryCount lebih besar dari 0
Client->>Helper: SetRequestGetBody
Helper-->>Client: GetBody disalin ke reqGetBody
end
loop i = 0 sampai retryCount
opt i lebih besar dari 0 dan response sebelumnya ada
Client->>Client: drain body lama lalu Close
end
opt i lebih besar dari 0
Client->>Retrier: NextInterval i-1
Retrier-->>Client: durasi jeda
Client->>Sleeper: tunggu sampai timer atau ctx selesai
Sleeper-->>Client: ctx.Err bila context dibatalkan
Client->>Helper: CloneRequest dengan reqGetBody
end
Client->>Plugins: OnRequestStart
Client->>Doer: Do request
Doer-->>Client: response dan err
alt err tidak nil
Client->>Plugins: OnError
Client->>Budget: Failure
Note over Client: skipRetry lalu lanjut percobaan berikutnya
else status code masuk daftar retry
Client->>Plugins: OnRequestEnd
Client->>Budget: Failure
Note over Client: skipRetry lalu lanjut percobaan berikutnya
else status code lain
Client->>Budget: Success
Note over Client: errs dikosongkan lalu break
end
end
Client->>Multi: BuildMultiError errs
Multi-->>Caller: response dan error
%% fig: Siklus Client.Do dari penyiapan body sampai BuildMultiError
%% desc: Do menyiapkan body yang dapat diputar ulang, menjalankan loop percobaan dengan jeda dan clone request, lalu merakit error akhir.Batas loop for i := 0; i <= c.retryCount; i++ berarti c.client.Do dipanggil paling banyak retryCount + 1 kali, dan body response percobaan sebelumnya dibuang lewat io.Copy(io.Discard, …) lalu ditutup sebelum percobaan baru dimulai (httpclient/client.go:144-16310). Pada setiap iterasi selain yang pertama, Do menjeda dengan c.retrier.NextInterval(i-1) sehingga indeks retry yang diterima retrier dimulai dari 0 (retry.go:5-811). Jeda itu melewati internal.SleepInterruptible, yang mengembalikan ctx.Err() bila context selesai sebelum timer menyala; error tersebut masuk ke errs, dilaporkan ke plugin, lalu loop berhenti (internal/sleep.go:9-165). noRetrier.NextInterval selalu mengembalikan 0, jadi client tanpa retrier tidak pernah menunggu sama sekali (retry.go:49-5112).
Setelah jeda, internal.CloneRequest membuat shallow clone lewat request.WithContext(request.Context()) dan mengganti Body dengan hasil reqGetBody(); body lama tidak di-clone dalam, sehingga memori tetap satu buffer (internal/request_helper.go:33-454). Strategi backoff yang mengisi NextInterval dijelaskan di Retry dan Backoff.
Pengecekan status code
Setelah c.client.Do mengembalikan response tanpa error, Do memanggil reportRequestEnd lalu memutuskan apakah percobaan diulang (httpclient/client.go:165-19013). Ada dua pemicu: kode status ditemukan di c.retryableCodes, atau kode status >= http.StatusInternalServerError. Pencarian memakai slices.BinarySearch, jadi daftar wajib terurut; WithRetryableStatusCodes mengurutkan ulang daftar setiap kali option dipasang (httpclient/options.go:48-5514).
Hasil c.client.Do | Isi errs | Keputusan |
|---|---|---|
| error transport | ditambah satu entry | skipRetry lalu lanjut percobaan berikutnya |
status code ada di retryableCodes | tidak diubah | skipRetry lalu lanjut percobaan berikutnya |
status code >= 500 | tidak diubah | skipRetry lalu lanjut percobaan berikutnya |
| status code lain | dikosongkan dengan errs = nil | Success() lalu break |
Ketika tidak ada kondisi yang cocok, errs dikosongkan, error budget menerima Success(), dan loop berhenti; response itulah yang dikembalikan (httpclient/client.go:165-19013).
⚠️ Jalur status code tidak pernah menambahkan entry ke
errs. Bila semua percobaan berakhir dengan 5xx dan tidak ada error transport,Domengembalikan response 5xx bersamanilsebagai error (httpclient/client.go:165-19013). Pemeriksaanresponse.StatusCodetetap tanggung jawab pemanggil.
Berhenti lebih awal
Dua jalur di dalam loop memanggil c.skipRetry(request.Context()) sebelum lanjut ke iterasi berikutnya (httpclient/client.go:144-16310). skipRetry mengembalikan true bila internal.IsCtxDone melihat context sudah selesai; pada kasus itu Success() dipanggil dan loop berhenti tanpa percobaan tambahan. IsCtxDone hanya melakukan pengecekan non-blocking pada ctx.Done() (internal/ctx.go:5-1215). Bila context masih hidup, keputusan diserahkan ke retryErrorBudget.Failure() (httpclient/client.go:195-2027).
Failure() mencatat satu token kegagalan dan melaporkan over budget ketika sisa token sudah menyentuh threshold (internal/error_budget.go:96-11116). Bila error budget tidak dipasang, receiver nil membuat Success() dan Failure() sama-sama mengembalikan false, sehingga retry tidak pernah diblokir budget (internal/error_budget.go:77-9217). Ambang dan rasio token diatur lewat option; rincian kebijakannya ada di Error Budget untuk Retry.
Perakitan error akhir
Baris terakhir Do mengembalikan internal.BuildMultiError(errs) bersama response terakhir (httpclient/client.go:19218). Fungsi itu memetakan panjang errs ke tiga bentuk hasil: nil, error tunggal, atau *multiError (internal/multi_error.go:5-148).
graph TD
A["errs []error"] --> B{"len errs"}
B -->|"0"| C["return nil"]
B -->|"1"| D["return errors[0]"]
B -->|"lebih dari 1"| E["multiError"]
E --> F["Error: gabung dengan koma"]
E --> G["Unwrap []error"]
style A fill:#8b5cf6,color:#fff
style B fill:#f59e0b,color:#000
style C fill:#2d7d46,color:#fff
style D fill:#dc2626,color:#fff
style E fill:#dc2626,color:#fff
style F fill:#dc2626,color:#fff
style G fill:#8b5cf6,color:#fff
%% fig: Pemetaan errs ke bentuk error akhir
%% desc: BuildMultiError memilih nil, error tunggal, atau multiError berdasarkan panjang errs.multiError.Error() merangkai pesan setiap error dengan pemisah ", ", sedangkan Unwrap() []error mengembalikan seluruh isi slice sehingga errors.Is dan errors.As dapat menelusuri tiap error percobaan (internal/multi_error.go:20-3419). Dengan begitu kegagalan berulang tetap terbaca satu per satu, tidak hanya sebagai pesan gabungan.