fix: capture jikan api error body in api error struct
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -34,6 +35,7 @@ type Config struct {
|
|||||||
type APIError struct {
|
type APIError struct {
|
||||||
StatusCode int
|
StatusCode int
|
||||||
URL string
|
URL string
|
||||||
|
Body json.RawMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *APIError) Error() string {
|
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) {
|
func handleResponseRetry(ctx context.Context, resp *http.Response, urlStr string, out any, attempt int, maxRetries int) (int, bool, error) {
|
||||||
if resp.StatusCode != http.StatusOK {
|
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)
|
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)
|
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
|
statusCode := resp.StatusCode
|
||||||
apiErr := &APIError{StatusCode: statusCode, URL: urlStr}
|
apiErr := &APIError{StatusCode: statusCode, URL: urlStr}
|
||||||
|
|
||||||
@@ -211,13 +213,27 @@ func handleStatusRetry(ctx context.Context, resp *http.Response, urlStr string,
|
|||||||
return statusCode, true, nil
|
return statusCode, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Best-effort decode (often useful for debugging), but still treat non-200 as error.
|
apiErr.Body = readErrorBody(resp)
|
||||||
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))
|
|
||||||
}
|
|
||||||
return statusCode, false, apiErr
|
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 {
|
func isRetryableStatus(statusCode int) bool {
|
||||||
if statusCode == http.StatusTooManyRequests {
|
if statusCode == http.StatusTooManyRequests {
|
||||||
return true
|
return true
|
||||||
|
|||||||
55
integrations/jikan/transport/client_test.go
Normal file
55
integrations/jikan/transport/client_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user