Telegram Bot长轮询GetUpdates接口无限返回重复更新问题
问题诱因
- 核心根因:Telegram Bot API的
getUpdates接口是GET请求,现有代码将offset、timeout等请求参数放在了请求体Content中,而HTTP规范中GET请求的请求体不会被绝大多数服务端、代理节点解析,Telegram服务器全程无法收到你传入的offset参数,每次请求都会默认按offset=0返回最早的未确认更新,因此会无限重复拉取到相同内容。这也对应了你提到的现象:手动在浏览器地址栏拼接offset参数调用接口就能终止异常——浏览器发起GET请求时会将参数拼接在URL查询串中,Telegram可以正常读取到offset值,标记对应更新为已消费后就不会重复返回。 - 异步调用隐患:代码中调用
HandleUpdatesAsync(update)时未添加await关键字,属于非等待的异步调用,更新处理逻辑不会被阻塞,程序会直接进入下一轮轮询,既可能导致多更新处理逻辑并发错乱,也可能在处理逻辑抛出异常时无法捕获,导致offset更新逻辑中断。 - 超时配置隐患:当前代码传给Telegram的长轮询
timeout值直接取HttpClient的默认超时时间,长轮询机制要求Telegram服务端在无新消息时会保持连接直到timeout时间才返回响应,如果HttpClient的超时时间小于等于传给Telegram的timeout值,请求会在长轮询完成前就被本地客户端主动断开,此时offset还未更新就会发起下一轮请求,触发重复拉取。 - offset更新逻辑隐患:当前offset在遍历单条更新时逐个赋值,如果某条更新处理过程中抛出未捕获异常,后续的offset赋值逻辑不会执行,下一轮轮询还是会从旧offset开始拉取,重复收到已经处理过的消息。
修复方案
- 修正GET请求的参数传递逻辑:所有GET类型的API接口,参数必须拼接在URL的查询字符串中,不能放在请求体里;非GET请求再将参数放入请求体。
- 调整超时配置:给长轮询设置的timeout值要比HttpClient的超时时间短5-10秒,避免本地客户端提前断开请求。
- 补全异步等待:调用异步的更新处理方法时必须加
await,保证单条更新处理完成后再进入后续流程。 - 优化offset更新逻辑:一批次拉取到的所有更新全部处理完成后,再取这批更新里最大的UpdateId+1作为新的offset,避免单条更新处理报错导致offset无法推进。
- 添加异常重试和延迟:轮询过程中出现异常时添加短暂延迟再重试,避免出错时高频请求接口被限流。
修正后的核心代码参考:
public async Task StartPolling(string[] allowedUpdates = default!) { int messageOffset = 0; // 长轮询超时设为30秒,HttpClient超时多留5秒缓冲 const int pollingTimeout = 30; s_httpClient.Timeout = TimeSpan.FromSeconds(pollingTimeout + 5); while (true) { try { Console.WriteLine($"Sending GetUpdate with offset {messageOffset}"); var updates = await GetUpdatesAsync( offset: messageOffset, timeout: pollingTimeout, allowedUpdates: allowedUpdates); if (updates == null || updates.Length == 0) { Console.WriteLine("No new updates, continue polling..."); continue; } Console.WriteLine($"Receving {updates.Length} updates..."); foreach (var update in updates) { // 等待单条更新处理完成 await HandleUpdatesAsync(update); Console.WriteLine($"Processed update, UpdateId = {update.UpdateId}"); } // 全量处理完成后统一更新offset messageOffset = updates.Max(u => u.UpdateId) + 1; Console.WriteLine($"Current offset = {messageOffset}"); } catch (Exception ex) { Console.WriteLine($"Polling error: {ex.Message}, retry after 2s..."); // 异常后延迟重试 await Task.Delay(2000); } } }
private async Task<T?> SendRequestAsync<T>(IRequest request) { var requestUri = $"{_baseUrl}/{request.MethodName}"; // GET请求将参数拼接为URL查询串 if (request.Method == HttpMethod.Get) { // 自行实现GetQueryParameters方法,将请求对象的公共属性转为URL编码的键值对集合 var queryParams = request.GetQueryParameters(); var queryString = await new FormUrlEncodedContent(queryParams).ReadAsStringAsync(); requestUri = $"{requestUri}?{queryString}"; } var httpRequestMessage = new HttpRequestMessage(method: request.Method, requestUri: requestUri) { // 非GET请求才传递请求体 Content = request.Method != HttpMethod.Get ? request.Content : null }; try { var httpResponseMessage = await s_httpClient.SendAsync(httpRequestMessage); // 先校验HTTP响应状态 httpResponseMessage.EnsureSuccessStatusCode(); var responseStream = await httpResponseMessage.Content.ReadAsStreamAsync(); var response = await JsonSerializer.DeserializeAsync<Response<T>>(responseStream, options); if (response == null || !response.Ok) { throw new InvalidOperationException($"API request failed: {response?.Description}"); } return response.Result; } catch (Exception ex) { Console.WriteLine(ex.Message); throw; } }
说明:你需要自行在
IRequest接口和对应请求实现类中补充GetQueryParameters方法,作用是将请求类的配置项(比如Offset、Limit、Timeout、AllowedUpdates)转为URL编码的键值对集合,确保参数能被Telegram服务端正常解析。
内容的提问来源于stack exchange,提问作者Never
相关产品推荐
相关产品推荐

