diff --git a/.github/workflows/cli-checks.yml b/.github/workflows/cli-checks.yml index b5245fec..6dd3acdf 100644 --- a/.github/workflows/cli-checks.yml +++ b/.github/workflows/cli-checks.yml @@ -6,10 +6,12 @@ on: paths: - 'cli/**' - 'js/**' + - '.github/workflows/cli-checks.yml' push: paths: - 'cli/**' - 'js/**' + - '.github/workflows/cli-checks.yml' jobs: cli-checks: @@ -22,9 +24,9 @@ jobs: - uses: actions/checkout@v4 - name: Setup Bun - uses: oven-sh/setup-bun@v1 + uses: oven-sh/setup-bun@v2 with: - bun-version: latest + bun-version-file: ".bun-version" - name: Install workspace dependencies working-directory: . diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 3fd29ce3..a9f6c30a 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -198,7 +198,7 @@ jobs: - name: Run Go tests (exclude live-API e2e) working-directory: go - run: go test $(go list ./... | grep -v /e2e) -count=1 + run: go test -race $(go list ./... | grep -v /e2e) -count=1 compatibility-checks: name: Compatibility Checks @@ -276,7 +276,8 @@ jobs: browser-compatibility: name: Browser Compatibility - runs-on: ${{ vars.CI_RUNNER || 'ubuntu-latest' }} + # Match the Ubuntu 24.04 ABI used by Playwright's WebKit binary. + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -463,4 +464,4 @@ jobs: name: browser-check-results path: | js/browser_report.txt - pr-metadata/ \ No newline at end of file + pr-metadata/ diff --git a/go/CHANGELOG.md b/go/CHANGELOG.md index 02573463..9b7901b6 100644 --- a/go/CHANGELOG.md +++ b/go/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased + +- Retry transient failures on bodyless GET/HEAD requests using the existing retry budget, `Retry-After` handling, and context cancellation. + ## [](https://github.com/Phala-Network/phala-cloud/compare/sdks/go/v0.2.0...sdks/go/v) (2026-11-07) ### ⚠ BREAKING CHANGE diff --git a/go/read_retry_test.go b/go/read_retry_test.go new file mode 100644 index 00000000..bac1c340 --- /dev/null +++ b/go/read_retry_test.go @@ -0,0 +1,66 @@ +package phala + +import ( + "context" + "errors" + "io" + "net/http" + "strings" + "testing" + "testing/synctest" + "time" +) + +type retryTransport func(*http.Request) (*http.Response, error) + +func (f retryTransport) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } + +func TestAutomaticReadRetries(t *testing.T) { + for _, tc := range []struct { + name, method string + timeout time.Duration + }{ + {"GET", "GET", 5 * time.Minute}, + {"HEAD", "HEAD", 5 * time.Minute}, + {"POST", "POST", 5 * time.Minute}, + {"deadline", "GET", 45 * time.Second}, + } { + t.Run(tc.name, func(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + calls, start := 0, time.Now() + client, err := NewClient(WithAPIKey("test-key"), WithMaxRetries(1), WithHTTPClient(&http.Client{ + Timeout: 30 * time.Second, + Transport: retryTransport(func(*http.Request) (*http.Response, error) { + calls++ + status := http.StatusOK + if calls == 1 { + status = http.StatusTooManyRequests + } + return &http.Response{StatusCode: status, Header: http.Header{"Retry-After": {"60"}}, Body: io.NopCloser(strings.NewReader("{}"))}, nil + }), + })) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithTimeout(context.Background(), tc.timeout) + defer cancel() + err = client.doJSON(ctx, tc.method, "/test", nil, nil) + switch tc.name { + case "POST": + var apiErr *APIError + if !errors.As(err, &apiErr) || apiErr.StatusCode != 429 || calls != 1 { + t.Fatalf("calls=%d err=%v", calls, err) + } + case "deadline": + if !errors.Is(err, context.DeadlineExceeded) || calls != 1 { + t.Fatalf("calls=%d err=%v", calls, err) + } + default: + if err != nil || calls != 2 || time.Since(start) != time.Minute { + t.Fatalf("calls=%d elapsed=%s err=%v", calls, time.Since(start), err) + } + } + }) + }) + } +} diff --git a/go/request.go b/go/request.go index 1c743fcb..78ec0304 100644 --- a/go/request.go +++ b/go/request.go @@ -45,6 +45,19 @@ func (c *Client) newRequest(ctx context.Context, method, path string, body io.Re // do executes an HTTP request and returns the response. Non-2xx responses return *APIError. func (c *Client) do(ctx context.Context, req *http.Request) (*http.Response, error) { + if (req.Method != http.MethodGet && req.Method != http.MethodHead) || + (req.Body != nil && req.Body != http.NoBody) { + return c.doOnce(req) + } + var resp *http.Response + err := c.doWithRetry(ctx, func() (err error) { + resp, err = c.doOnce(req) + return err + }) + return resp, err +} + +func (c *Client) doOnce(req *http.Request) (*http.Response, error) { resp, err := c.httpClient.Do(req) if err != nil { return nil, err