You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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开始拉取,重复收到已经处理过的消息。
修复方案
  1. 修正GET请求的参数传递逻辑:所有GET类型的API接口,参数必须拼接在URL的查询字符串中,不能放在请求体里;非GET请求再将参数放入请求体。
  2. 调整超时配置:给长轮询设置的timeout值要比HttpClient的超时时间短5-10秒,避免本地客户端提前断开请求。
  3. 补全异步等待:调用异步的更新处理方法时必须加await,保证单条更新处理完成后再进入后续流程。
  4. 优化offset更新逻辑:一批次拉取到的所有更新全部处理完成后,再取这批更新里最大的UpdateId+1作为新的offset,避免单条更新处理报错导致offset无法推进。
  5. 添加异常重试和延迟:轮询过程中出现异常时添加短暂延迟再重试,避免出错时高频请求接口被限流。

修正后的核心代码参考:

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 10:09:20