@@ -53,6 +53,9 @@ func (b *Bot) url(method string) string {
5353
5454// doJSON marshals the request body, sends a POST request, and unmarshals
5555// the "result" field of the response into the provided destination.
56+ // Retries on transient errors: network errors, 429 (rate limit), and 5xx
57+ // server errors, with exponential backoff (1s, 2s, 4s, 8s; max 4 retries).
58+ // Does NOT retry on 4xx errors (except 429) — those are client errors.
5659func (b * Bot ) doJSON (method string , body any , dest any ) error {
5760 var reqBody []byte
5861 if body != nil {
@@ -65,43 +68,70 @@ func (b *Bot) doJSON(method string, body any, dest any) error {
6568 }
6669
6770 url := b .url (method )
68- resp , err := b .Client .Post (url , "application/json" , bytes .NewReader (reqBody ))
69- if err != nil {
70- b .log .Error ("http post failed" , "method" , method , "error" , err )
71- return fmt .Errorf ("telegram: post %s: %w" , method , err )
72- }
73- defer resp .Body .Close ()
71+ var lastErr error
7472
75- respBody , err := io .ReadAll (resp .Body )
76- if err != nil {
77- b .log .Error ("read response body failed" , "method" , method , "error" , err )
78- return fmt .Errorf ("telegram: read response: %w" , err )
79- }
73+ for attempt := 0 ; attempt < 5 ; attempt ++ {
74+ if attempt > 0 {
75+ backoff := time .Duration (1 << (attempt - 1 )) * time .Second // 1s, 2s, 4s, 8s
76+ b .log .Warn ("retrying request" , "method" , method , "attempt" , attempt , "backoff" , backoff )
77+ time .Sleep (backoff )
78+ }
8079
81- var apiResp struct {
82- OK bool `json:"ok"`
83- Result json.RawMessage `json:"result"`
84- Description string `json:"description"`
85- ErrorCode int `json:"error_code"`
86- }
87- if err := json .Unmarshal (respBody , & apiResp ); err != nil {
88- b .log .Error ("unmarshal response failed" , "method" , method , "error" , err )
89- return fmt .Errorf ("telegram: unmarshal response: %w" , err )
90- }
80+ resp , err := b .Client .Post (url , "application/json" , bytes .NewReader (reqBody ))
81+ if err != nil {
82+ b .log .Error ("http post failed" , "method" , method , "error" , err )
83+ lastErr = fmt .Errorf ("telegram: post %s: %w" , method , err )
84+ continue // network error — retry
85+ }
9186
92- if ! apiResp .OK {
93- b .log .Error ("api error" , "method" , method , "description" , apiResp .Description , "error_code" , apiResp .ErrorCode )
94- return fmt .Errorf ("telegram: %s failed: %s (code %d)" , method , apiResp .Description , apiResp .ErrorCode )
95- }
87+ respBody , err := io .ReadAll (resp .Body )
88+ resp .Body .Close ()
89+ if err != nil {
90+ b .log .Error ("read response body failed" , "method" , method , "error" , err )
91+ lastErr = fmt .Errorf ("telegram: read response: %w" , err )
92+ continue // read error — retry
93+ }
9694
97- if dest != nil && len (apiResp .Result ) > 0 {
98- if err := json .Unmarshal (apiResp .Result , dest ); err != nil {
99- b .log .Error ("unmarshal result failed" , "method" , method , "error" , err )
100- return fmt .Errorf ("telegram: unmarshal result: %w" , err )
95+ var apiResp struct {
96+ OK bool `json:"ok"`
97+ Result json.RawMessage `json:"result"`
98+ Description string `json:"description"`
99+ ErrorCode int `json:"error_code"`
100+ }
101+ if err := json .Unmarshal (respBody , & apiResp ); err != nil {
102+ b .log .Error ("unmarshal response failed" , "method" , method , "error" , err )
103+ return fmt .Errorf ("telegram: unmarshal response: %w" , err ) // parse error — don't retry
104+ }
105+
106+ if ! apiResp .OK {
107+ // 429 (rate limit) — retry
108+ if apiResp .ErrorCode == 429 {
109+ b .log .Warn ("rate limited" , "method" , method , "description" , apiResp .Description )
110+ lastErr = fmt .Errorf ("telegram: %s failed: %s (code %d)" , method , apiResp .Description , apiResp .ErrorCode )
111+ continue
112+ }
113+ // 5xx (server error) — retry
114+ if apiResp .ErrorCode >= 500 && apiResp .ErrorCode < 600 {
115+ b .log .Warn ("server error" , "method" , method , "error_code" , apiResp .ErrorCode , "description" , apiResp .Description )
116+ lastErr = fmt .Errorf ("telegram: %s failed: %s (code %d)" , method , apiResp .Description , apiResp .ErrorCode )
117+ continue
118+ }
119+ // 4xx (client error, not 429) — don't retry
120+ b .log .Error ("api error" , "method" , method , "description" , apiResp .Description , "error_code" , apiResp .ErrorCode )
121+ return fmt .Errorf ("telegram: %s failed: %s (code %d)" , method , apiResp .Description , apiResp .ErrorCode )
122+ }
123+
124+ if dest != nil && len (apiResp .Result ) > 0 {
125+ if err := json .Unmarshal (apiResp .Result , dest ); err != nil {
126+ b .log .Error ("unmarshal result failed" , "method" , method , "error" , err )
127+ return fmt .Errorf ("telegram: unmarshal result: %w" , err )
128+ }
101129 }
130+
131+ return nil
102132 }
103133
104- return nil
134+ return lastErr
105135}
106136
107137// doUpload sends a multipart/form-data POST request with a file and optional parameters.
0 commit comments