diff --git a/internal/topdata/wiki_deploy.go b/internal/topdata/wiki_deploy.go index 23b8762..8f87e46 100644 --- a/internal/topdata/wiki_deploy.go +++ b/internal/topdata/wiki_deploy.go @@ -1587,13 +1587,48 @@ func (c *nodeBBClient) acquireEditLock(tid int) (nodeBBEditLock, error) { } func (c *nodeBBClient) request(method, path string, payload any, out any) error { - var body io.Reader + var rawPayload []byte if payload != nil { - raw, err := json.Marshal(payload) + var err error + rawPayload, err = json.Marshal(payload) if err != nil { return err } - body = bytes.NewReader(raw) + } + const maxAttempts = 3 + backoff := []time.Duration{5 * time.Second, 15 * time.Second} + for attempt := 0; attempt < maxAttempts; attempt++ { + err := c.doRequest(method, path, rawPayload, out) + if err == nil { + return nil + } + if attempt == maxAttempts-1 || !isNodeBBRetryable(err) { + return err + } + time.Sleep(backoff[attempt]) + } + return nil +} + +func isNodeBBRetryable(err error) bool { + if err == nil { + return false + } + var httpErr nodeBBHTTPError + if errors.As(err, &httpErr) { + return httpErr.Status == http.StatusTooManyRequests || + httpErr.Status == http.StatusBadGateway || + httpErr.Status == http.StatusServiceUnavailable || + httpErr.Status == http.StatusGatewayTimeout + } + // Retry on network/timeout errors (e.g. context deadline exceeded) + return true +} + +func (c *nodeBBClient) doRequest(method, path string, rawPayload []byte, out any) error { + var body io.Reader + if rawPayload != nil { + body = bytes.NewReader(rawPayload) } req, err := http.NewRequest(method, c.endpoint+path, body) if err != nil { @@ -1601,7 +1636,7 @@ func (c *nodeBBClient) request(method, path string, payload any, out any) error } req.Header.Set("Authorization", "Bearer "+c.token) req.Header.Set("Accept", "application/json") - if payload != nil { + if rawPayload != nil { req.Header.Set("Content-Type", "application/json") } client := http.Client{Timeout: c.timeout}