From 383f8ea32124bef4276fafe0eadc44f7e7de0639 Mon Sep 17 00:00:00 2001 From: Repinoid Date: Wed, 30 Sep 2026 20:33:48 +0300 Subject: [PATCH] =?UTF-8?q?fix(core):=20=D1=82=D1=80=D0=B0=D0=BD=D0=B7?= =?UTF-8?q?=D0=B8=D0=B5=D0=BD=D1=82=D0=BD=D1=8B=D0=B9=20401=20=D1=82=D0=B5?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D1=8C=20=D1=80=D0=B5=D1=82=D1=80=D0=B0=D0=B8?= =?UTF-8?q?=D1=82=D1=81=D1=8F=20=D0=B4=D0=BB=D1=8F=20GET?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isRetryable (core/http.go): добавлен StatusUnauthorized (401) к {429,502,503,504}. Gateway может временно отклонять валидный JWT — без этого одиночный 401 ронял read/plan/поллинг. - client_test.go: юнит-тест TestIsRetryable (401/429/502/503/504 = true; 400/403/404/500 = false). - ARCHITECTURE.md: раздел API Resilience приведён к фактическому поведению (401 реализован; POST не ретраится намеренно). Проверено: go build ./... OK; go test ./internal/core/... -short OK. --- TOOLS/ARCHITECTURE.md | 8 +++++--- provider/internal/core/client_test.go | 27 +++++++++++++++++++++++++++ provider/internal/core/http.go | 5 ++++- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/TOOLS/ARCHITECTURE.md b/TOOLS/ARCHITECTURE.md index 0a3d12f..547c773 100644 --- a/TOOLS/ARCHITECTURE.md +++ b/TOOLS/ARCHITECTURE.md @@ -123,9 +123,11 @@ Each operation has a kind: - Core MUST retry transient 401 errors from Gateway (3 attempts, exponential backoff). Gateway may temporarily reject valid JWT tokens. -- **CURRENTLY NOT IMPLEMENTED** — `isRetryable` (`core/http.go`) retries only - {429, 502, 503, 504}, NOT 401, and only for GET. This is a known gap versus the - intent above; fix in `core/http.go` `isRetryable`. +- Implemented: `isRetryable` (`core/http.go`) includes 401 alongside {429, 502, 503, 504}; + retry applies to GET requests only. +- Network errors and HTTP retryable statuses are retried for **GET only**. POST is not + retried (a blind retry of the creating `POST /instanceOperations` could duplicate an + operation — see open question on idempotency). ### Generated Code Resilience diff --git a/provider/internal/core/client_test.go b/provider/internal/core/client_test.go index 1a7638f..0b0259b 100644 --- a/provider/internal/core/client_test.go +++ b/provider/internal/core/client_test.go @@ -52,6 +52,33 @@ func TestNormalizeValue_TrimSpace(t *testing.T) { } } +func TestIsRetryable(t *testing.T) { + retryable := []int{ + http.StatusUnauthorized, // 401 — транзиентный отказ Gateway + http.StatusTooManyRequests, // 429 + http.StatusServiceUnavailable, // 503 + http.StatusBadGateway, // 502 + http.StatusGatewayTimeout, // 504 + } + for _, code := range retryable { + if !isRetryable(code) { + t.Errorf("expected isRetryable(%d)=true", code) + } + } + + notRetryable := []int{ + http.StatusBadRequest, // 400 + http.StatusForbidden, // 403 + http.StatusNotFound, // 404 + http.StatusInternalServerError, // 500 + } + for _, code := range notRetryable { + if isRetryable(code) { + t.Errorf("expected isRetryable(%d)=false", code) + } + } +} + // ===== waitForOperationFinish tests ===== func makeTestClient(handler http.HandlerFunc) (*UniversalClient, func()) { diff --git a/provider/internal/core/http.go b/provider/internal/core/http.go index ba5f8c5..b310321 100644 --- a/provider/internal/core/http.go +++ b/provider/internal/core/http.go @@ -117,8 +117,11 @@ func (c *UniversalClient) doRequest(ctx context.Context, method, path string, pa } // isRetryable returns true for transient HTTP errors that can be retried. +// 401 включён: Gateway может транзиентно отклонять валидный JWT (см. ARCHITECTURE.md, +// «API Resilience»). Ретраится только для GET (см. условие в doRequest). func isRetryable(statusCode int) bool { - return statusCode == http.StatusTooManyRequests || // 429 + return statusCode == http.StatusUnauthorized || // 401 + statusCode == http.StatusTooManyRequests || // 429 statusCode == http.StatusServiceUnavailable || // 503 statusCode == http.StatusBadGateway || // 502 statusCode == http.StatusGatewayTimeout // 504