fix(telegram): bump timeout for media uploads to 3 min

Photo/video/audio/document uploads were hitting "context deadline
exceeded" mid-flight on slow or mobile networks. Two layers capped
the budget at 60s:

  - sendOverallTimeout (wraps the whole retry sequence)
  - http.Client.Timeout (caps a single HTTP request)

Both are fine for text sends but too tight for multi-MB uploads
(sketchnote images, PDFs, long voice clips).

Split the retry budget by method: text sends keep 60s, media sends
(sendPhoto/Video/Audio/Voice/Document) get a new sendMediaOverallTimeout
of 3 min. Bump the shared http.Client.Timeout to 3 min so a single
upload attempt can actually consume that budget — getUpdates long-poll
is 25s, so the extra headroom doesn't affect polling.

Closes #628
This commit is contained in:
viettranx committed 2026-04-18 15:53:25 +07:00
1 parent 3b445d6856
commit bfedf8e1d4
3 files changed
+32 -3

No files matched your search

+7 -1
View File
@@ -110,7 +110,13 @@ func New(cfg config.TelegramConfig, msgBus *bus.MessageBus, pairingSvc store.Pai
}
httpClient := &http.Client{
Timeout: 60 * time.Second, // Must exceed getUpdates Timeout to avoid long-poll race (#361)
// Must exceed getUpdates long-poll Timeout (25s, #361) AND cover the
// longest per-attempt media upload. A 60s cap was killing multi-MB
// photo uploads on slow networks mid-flight (#628), even when the
// per-call ctx deadline was generous. 3 min matches
// sendMediaOverallTimeout so a single upload attempt can consume the
// full media budget when needed.
Timeout: 3 * time.Minute,
Transport: transport,
}
// Apply ForceIPv4 at init if configured (explicit, predictable, no runtime heuristic).
+9 -1
View File
@@ -13,9 +13,17 @@ const (
// pairingReplyDebounce is the minimum interval between pairing replies to the same user.
pairingReplyDebounce = 60 * time.Second
// sendOverallTimeout is the maximum duration for a multi-retry send sequence.
// sendOverallTimeout is the maximum duration for a multi-retry text send
// sequence. Text rarely needs more than a couple of seconds per attempt.
sendOverallTimeout = 60 * time.Second
// sendMediaOverallTimeout covers photo/video/audio/document uploads, where
// a single attempt can take 30–90s on slow/mobile networks for larger
// files. 60s was triggering "context deadline exceeded" mid-upload (#628)
// for multi-MB sketchnote/PDF attachments. 3 min gives three realistic
// retry attempts even on poor links.
sendMediaOverallTimeout = 3 * time.Minute
// probeOverallTimeout is the maximum duration for initial bot status check and command sync.
probeOverallTimeout = 60 * time.Second
)
+16 -1
View File
@@ -92,11 +92,26 @@ func isPostConnectNetworkErr(err error) bool {
strings.Contains(s, "EOF")) && !strings.Contains(s, "lookup")
}
// mediaSendMethods lists sendX calls that upload a file body and therefore
// need the longer sendMediaOverallTimeout budget. Keyed by the `name` argument
// passed to retrySend so no call site has to change when we bump the budget.
var mediaSendMethods = map[string]struct{}{
"sendPhoto": {},
"sendVideo": {},
"sendAudio": {},
"sendVoice": {},
"sendDocument": {},
}
// retrySend wraps a Telegram send call with retry logic for transient network errors.
// Parse errors are NOT retried (handled by caller's HTML fallback).
// resetFn is called before each retry (e.g. to seek file handles back to start). Can be nil.
func (c *Channel) retrySend(ctx context.Context, name string, resetFn func(), fn func(context.Context) error) error {
ctx, cancel := context.WithTimeout(ctx, sendOverallTimeout)
overall := sendOverallTimeout
if _, isMedia := mediaSendMethods[name]; isMedia {
overall = sendMediaOverallTimeout
}
ctx, cancel := context.WithTimeout(ctx, overall)
defer cancel()
var err error