From fb473262ba22fc522509e31dcf4f68bd9d23a767 Mon Sep 17 00:00:00 2001 From: Mathias Bergqvist Date: Mon, 4 May 2026 23:04:55 +0200 Subject: [PATCH] feat(gitea): read retry once on 5xx GET --- internal/gitea/client.go | 11 ++++++++++- internal/gitea/client_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/internal/gitea/client.go b/internal/gitea/client.go index 6590f17..b65dc6f 100644 --- a/internal/gitea/client.go +++ b/internal/gitea/client.go @@ -22,7 +22,7 @@ func NewClient(baseURL, token string) *Client { } } -func (c *Client) do(ctx context.Context, method, path string, body []byte) ([]byte, int, error) { +func (c *Client) doOnce(ctx context.Context, method, path string, body []byte) ([]byte, int, error) { var reader io.Reader if body != nil { reader = bytes.NewReader(body) @@ -48,6 +48,15 @@ func (c *Client) do(ctx context.Context, method, path string, body []byte) ([]by return b, resp.StatusCode, err } +func (c *Client) do(ctx context.Context, method, path string, body []byte) ([]byte, int, error) { + b, status, err := c.doOnce(ctx, method, path, body) + if err == nil && method == http.MethodGet && status >= 500 && status < 600 { + time.Sleep(250 * time.Millisecond) + return c.doOnce(ctx, method, path, body) + } + return b, status, err +} + func (c *Client) GetJSON(ctx context.Context, path string) ([]byte, int, error) { return c.do(ctx, http.MethodGet, path, nil) } diff --git a/internal/gitea/client_test.go b/internal/gitea/client_test.go index cdeb976..8292a1f 100644 --- a/internal/gitea/client_test.go +++ b/internal/gitea/client_test.go @@ -4,6 +4,7 @@ import ( "context" "net/http" "net/http/httptest" + "sync/atomic" "testing" "gitea.d-ma.be/mathias/gitea-mcp/internal/gitea" @@ -27,3 +28,37 @@ func TestClientGetsTokenInHeader(t *testing.T) { assert.Contains(t, string(body), `"ok":true`) assert.Equal(t, "token test-token", gotAuth) } + +func TestRetryOn5xxGetSucceedsOnSecondAttempt(t *testing.T) { + var attempts int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n := atomic.AddInt32(&attempts, 1) + if n == 1 { + http.Error(w, "boom", http.StatusServiceUnavailable) + return + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"ok":true}`)) + })) + defer srv.Close() + + c := gitea.NewClient(srv.URL, "tok") + body, status, err := c.GetJSON(context.Background(), "/api/v1/test") + require.NoError(t, err) + assert.Equal(t, 200, status) + assert.Contains(t, string(body), `"ok":true`) + assert.Equal(t, int32(2), atomic.LoadInt32(&attempts)) +} + +func TestRetryOnPostNotRetried(t *testing.T) { + var attempts int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + atomic.AddInt32(&attempts, 1) + http.Error(w, "boom", http.StatusServiceUnavailable) + })) + defer srv.Close() + + c := gitea.NewClient(srv.URL, "tok") + _, _, _ = c.PostJSON(context.Background(), "/api/v1/test", []byte(`{}`)) + assert.Equal(t, int32(1), atomic.LoadInt32(&attempts), "POST should not retry") +}