From fe2f5be81215a8bf8bd18d7ac8bec239c7bca995 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Tue, 23 Jun 2026 15:12:46 +0200 Subject: [PATCH] fix: capture jikan api error body in api error struct --- integrations/jikan/transport/client.go | 28 ++++++++--- integrations/jikan/transport/client_test.go | 55 +++++++++++++++++++++ 2 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 integrations/jikan/transport/client_test.go diff --git a/integrations/jikan/transport/client.go b/integrations/jikan/transport/client.go index cdc94b8..8d101b5 100644 --- a/integrations/jikan/transport/client.go +++ b/integrations/jikan/transport/client.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net" "net/http" "strconv" @@ -34,6 +35,7 @@ type Config struct { type APIError struct { StatusCode int URL string + Body json.RawMessage } func (e *APIError) Error() string { @@ -177,7 +179,7 @@ func handleRequestRetry(ctx context.Context, err error, attempt int, maxRetries func handleResponseRetry(ctx context.Context, resp *http.Response, urlStr string, out any, attempt int, maxRetries int) (int, bool, error) { if resp.StatusCode != http.StatusOK { - return handleStatusRetry(ctx, resp, urlStr, out, attempt, maxRetries) + return handleStatusRetry(ctx, resp, urlStr, attempt, maxRetries) } err := json.NewDecoder(resp.Body).Decode(out) @@ -195,7 +197,7 @@ func handleResponseRetry(ctx context.Context, resp *http.Response, urlStr string return resp.StatusCode, false, fmt.Errorf("failed to decode jikan response: %w", err) } -func handleStatusRetry(ctx context.Context, resp *http.Response, urlStr string, out any, attempt int, maxRetries int) (int, bool, error) { +func handleStatusRetry(ctx context.Context, resp *http.Response, urlStr string, attempt int, maxRetries int) (int, bool, error) { statusCode := resp.StatusCode apiErr := &APIError{StatusCode: statusCode, URL: urlStr} @@ -211,13 +213,27 @@ func handleStatusRetry(ctx context.Context, resp *http.Response, urlStr string, return statusCode, true, nil } - // Best-effort decode (often useful for debugging), but still treat non-200 as error. - if err := json.NewDecoder(resp.Body).Decode(out); err != nil { - return statusCode, false, errors.Join(apiErr, fmt.Errorf("failed to decode error response: %w", err)) - } + apiErr.Body = readErrorBody(resp) return statusCode, false, apiErr } +func readErrorBody(resp *http.Response) json.RawMessage { + if resp.Body == nil { + return nil + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil + } + body = []byte(strings.TrimSpace(string(body))) + if len(body) == 0 || !json.Valid(body) { + return nil + } + + return json.RawMessage(body) +} + func isRetryableStatus(statusCode int) bool { if statusCode == http.StatusTooManyRequests { return true diff --git a/integrations/jikan/transport/client_test.go b/integrations/jikan/transport/client_test.go new file mode 100644 index 0000000..61ca7c0 --- /dev/null +++ b/integrations/jikan/transport/client_test.go @@ -0,0 +1,55 @@ +package transport + +import ( + "context" + "encoding/json" + "errors" + "io" + "net/http" + "strings" + "testing" +) + +func TestHandleStatusRetryLeavesOutputUntouched(t *testing.T) { + out := struct { + Data []struct { + MalID int `json:"mal_id"` + } `json:"data"` + }{ + Data: []struct { + MalID int `json:"mal_id"` + }{{MalID: 123}}, + } + resp := &http.Response{ + StatusCode: http.StatusNotFound, + Body: io.NopCloser(strings.NewReader(`{"data":[{"mal_id":999}]}`)), + Header: make(http.Header), + } + + statusCode, retry, err := handleResponseRetry(context.Background(), resp, "https://example.test/anime/1", &out, 0, 1) + if statusCode != http.StatusNotFound { + t.Fatalf("statusCode = %d, want %d", statusCode, http.StatusNotFound) + } + if retry { + t.Fatal("retry = true, want false") + } + var apiErr *APIError + if !errors.As(err, &apiErr) { + t.Fatalf("err = %v, want APIError", err) + } + if len(out.Data) != 1 || out.Data[0].MalID != 123 { + t.Fatalf("out = %+v, want original value", out) + } + + var body struct { + Data []struct { + MalID int `json:"mal_id"` + } `json:"data"` + } + if err := json.Unmarshal(apiErr.Body, &body); err != nil { + t.Fatalf("unmarshal APIError body: %v", err) + } + if len(body.Data) != 1 || body.Data[0].MalID != 999 { + t.Fatalf("APIError body = %+v, want decoded error body", body) + } +}