diff --git a/CHANGELOG.md b/CHANGELOG.md index c75a8d1..cfe51bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v1.0.2 + +### Fixed +- Fixed long-polling stopping permanently when the HTTP client's internal timeout fired. The polling loop was checking `errors.Is(err, context.DeadlineExceeded)`, which matched HTTP client timeout errors (`*url.Error` wraps `context.DeadlineExceeded`), causing the goroutine to exit as if the bot context was canceled. The check is now `ctx.Err() != nil` so only a real context cancellation stops polling. +- Fixed the HTTP client timeout (45 s) being too close to the long-poll `getUpdates` timeout (30 s default), leaving insufficient margin for connection setup and response transfer. The client timeout is now derived from the configured `PollTimeout` plus a 60-second buffer. + ## v1.0.1 ### Fixed diff --git a/bot.go b/bot.go index 31f2177..c1fd1e4 100644 --- a/bot.go +++ b/bot.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "net/http" "sync" "time" @@ -166,21 +167,6 @@ func NewBot[T any](opts *BotOpts) (*Bot[T], error) { limiter := utils.NewRateLimiter() limiter.SetGlobalRate(opts.RateLimit) - apiOpts := tgapi.NewAPIOpts(opts.Token). - SetAPIURL(opts.APIURL). - UseTestServer(opts.UseTestServer). - SetLimiter(limiter). - SetDropRateLimitOverflow(opts.DropRateLimitOverflow). - SetLogFormat(opts.LogFormat). - SetLogFormatter(opts.LogFormatter) - api := tgapi.NewAPI(apiOpts) - uploader := tgapi.NewUploader(api) - - prefixes := opts.Prefixes - if len(prefixes) == 0 { - prefixes = []string{"/"} - } - workers := 32 if opts.MaxWorkers > 0 { workers = opts.MaxWorkers @@ -191,6 +177,25 @@ func NewBot[T any](opts *BotOpts) (*Bot[T], error) { pollTimeout = opts.PollTimeout } + // HTTP client timeout must exceed pollTimeout to avoid spurious deadline + // errors that the polling loop would misinterpret as context cancellation. + httpTimeout := time.Duration(pollTimeout)*time.Second + 60*time.Second + apiOpts := tgapi.NewAPIOpts(opts.Token). + SetAPIURL(opts.APIURL). + UseTestServer(opts.UseTestServer). + SetLimiter(limiter). + SetDropRateLimitOverflow(opts.DropRateLimitOverflow). + SetLogFormat(opts.LogFormat). + SetLogFormatter(opts.LogFormatter). + SetHTTPClient(&http.Client{Timeout: httpTimeout}) + api := tgapi.NewAPI(apiOpts) + uploader := tgapi.NewUploader(api) + + prefixes := opts.Prefixes + if len(prefixes) == 0 { + prefixes = []string{"/"} + } + bot := &Bot[T]{ updateOffset: 0, errorTemplate: "%s", @@ -472,7 +477,7 @@ func (bot *Bot[T]) RunWithContext(ctx context.Context) error { default: updates, err := bot.Updates(ctx) if err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + if ctx.Err() != nil { return } retryDelay, ok := pollRetryAfterDelay(err) diff --git a/utils/version.go b/utils/version.go index 1b463bd..c4717c5 100644 --- a/utils/version.go +++ b/utils/version.go @@ -2,13 +2,13 @@ package utils const ( // VersionString is the module version string. - VersionString = "1.0.1" + VersionString = "1.0.2" // VersionMajor is the module major version. VersionMajor = 1 // VersionMinor is the module minor version. VersionMinor = 0 // VersionPatch is the module patch version. - VersionPatch = 1 + VersionPatch = 2 // VersionBeta is the prerelease counter for the current version. VersionBeta = 0 )