feat(httpclient)!: promote to v1.0.0 — retry-on-429 with Retry-After, DoJSONRequest, bump deps
Extend retry loop to handle HTTP 429 Too Many Requests: when the server includes a Retry-After header, that duration is used as the retry delay; otherwise falls back to the configured BackOffDelay. Add DoJSONRequest[Req, Resp] free function that serialises the request body as JSON, sets Content-Type, and delegates response decoding to DoJSON. Bump logz and xerrors from v0.9.0 to v1.0.0. API committed as stable.
This commit is contained in:
@@ -111,6 +111,96 @@ func TestClient_Do_NoRequestID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Do_Retry429_WithRetryAfter(t *testing.T) {
|
||||
calls := 0
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
calls++
|
||||
w.Header().Set("Retry-After", "0")
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := New(newLogger(), Config{
|
||||
Name: "test", Timeout: 5 * time.Second, DialTimeout: 2 * time.Second,
|
||||
MaxRetries: 3, RetryDelay: 1 * time.Millisecond, CBThreshold: 100, CBTimeout: time.Minute,
|
||||
})
|
||||
req, _ := http.NewRequest(http.MethodGet, srv.URL, nil)
|
||||
_, err := client.Do(req)
|
||||
if err == nil {
|
||||
t.Fatal("expected error after retries")
|
||||
}
|
||||
if calls < 2 {
|
||||
t.Errorf("expected retries on 429, got %d calls", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Do_Retry429_NoRetryAfter(t *testing.T) {
|
||||
calls := 0
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
calls++
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := New(newLogger(), Config{
|
||||
Name: "test", Timeout: 5 * time.Second, DialTimeout: 2 * time.Second,
|
||||
MaxRetries: 3, RetryDelay: 1 * time.Millisecond, CBThreshold: 100, CBTimeout: time.Minute,
|
||||
})
|
||||
req, _ := http.NewRequest(http.MethodGet, srv.URL, nil)
|
||||
_, err := client.Do(req)
|
||||
if err == nil {
|
||||
t.Fatal("expected error after retries")
|
||||
}
|
||||
if calls < 2 {
|
||||
t.Errorf("expected retries on 429, got %d calls", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoJSONRequest_Success(t *testing.T) {
|
||||
type payload struct{ Name string }
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if ct := r.Header.Get("Content-Type"); ct != "application/json" {
|
||||
t.Errorf("want Content-Type application/json, got %q", ct)
|
||||
}
|
||||
var req payload
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
_ = json.NewEncoder(w).Encode(payload{Name: "echo:" + req.Name})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := New(newLogger(), Config{
|
||||
Name: "test", Timeout: 5 * time.Second, DialTimeout: 2 * time.Second,
|
||||
MaxRetries: 1, RetryDelay: time.Millisecond, CBThreshold: 10, CBTimeout: time.Minute,
|
||||
})
|
||||
result, err := DoJSONRequest[payload, payload](t.Context(), client, http.MethodPost, srv.URL, payload{Name: "alice"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result.Name != "echo:alice" {
|
||||
t.Errorf("want echo:alice, got %s", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoJSONRequest_4xx(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := New(newLogger(), Config{
|
||||
Name: "test", Timeout: 5 * time.Second, DialTimeout: 2 * time.Second,
|
||||
MaxRetries: 1, RetryDelay: time.Millisecond, CBThreshold: 10, CBTimeout: time.Minute,
|
||||
})
|
||||
_, err := DoJSONRequest[struct{}, struct{}](t.Context(), client, http.MethodPost, srv.URL, struct{}{})
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
var xe *xerrors.Err
|
||||
if !errors.As(err, &xe) || xe.Code() != xerrors.ErrNotFound {
|
||||
t.Errorf("want ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoJSON_Success(t *testing.T) {
|
||||
type payload struct{ Name string }
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user