fix: verify telegram webhook registration and fall back to polling on failure

SetWebhook returning OK is not sufficient proof the webhook is live —
the URL can end up unset if DeleteWebhook races in or an intermediary
rewrites the request, after which inbound updates are silently dropped.
Read back getWebhookInfo to confirm Telegram stored the expected URL,
and on any verification failure fall back to polling when an update
handler is configured so the bot keeps receiving updates.
This commit is contained in:
orris-inc
2026-04-29 14:07:17 +08:00
parent 9beb449393
commit c1eff8d9cf
2 changed files with 77 additions and 5 deletions

View File

@@ -543,6 +543,46 @@ func (s *BotService) GetBotUsername() string {
return s.botUsername
}
// getWebhookInfoResponse represents the response from getWebhookInfo API.
// We only extract the URL field; the full response has more diagnostic fields
// (pending_update_count, last_error_message, etc.) that we do not currently need.
type getWebhookInfoResponse struct {
OK bool `json:"ok"`
Result struct {
URL string `json:"url"`
} `json:"result"`
Description string `json:"description,omitempty"`
}
// GetWebhookInfo returns the webhook URL that Telegram currently has registered
// for this bot, or empty string if no webhook is set. Used to verify that a
// SetWebhook call took effect and to detect drift between our config and
// Telegram's view.
func (s *BotService) GetWebhookInfo() (string, error) {
apiURL := fmt.Sprintf("%s/getWebhookInfo", s.baseURL)
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
resp, err := s.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
var result getWebhookInfoResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", fmt.Errorf("failed to decode response: %w", err)
}
if !result.OK {
return "", fmt.Errorf("telegram API error: %s", result.Description)
}
return result.Result.URL, nil
}
// GetBotLink returns the Telegram bot link (https://t.me/username)
func (s *BotService) GetBotLink() string {
if s.botUsername == "" {

View File

@@ -84,7 +84,7 @@ func (m *BotServiceManager) Start(ctx context.Context) error {
m.logger.Infow("starting telegram bot in webhook mode",
"webhook_url", config.WebhookURL,
)
return m.startWebhookMode()
return m.startWebhookMode(ctx)
}
// startPollingMode starts the bot in polling mode
@@ -109,10 +109,24 @@ func (m *BotServiceManager) startPollingMode(ctx context.Context) error {
return nil
}
// startWebhookMode sets up webhook for the bot
func (m *BotServiceManager) startWebhookMode() error {
if err := m.botService.SetWebhook(m.currentConfig.WebhookURL); err != nil {
m.logger.Errorw("failed to set webhook", "error", err)
// startWebhookMode registers the webhook with Telegram and verifies the
// registration stuck. If registration or verification fails, inbound updates
// would otherwise be silently lost (Telegram has no webhook to push to, and
// polling is not running), so we fall back to polling when an update handler
// is available.
func (m *BotServiceManager) startWebhookMode(ctx context.Context) error {
if err := m.registerAndVerifyWebhook(); err != nil {
if m.updateHandler != nil {
m.logger.Errorw("webhook setup failed; falling back to polling mode so inbound updates are not lost",
"webhook_url", m.currentConfig.WebhookURL,
"error", err,
)
return m.startPollingMode(ctx)
}
m.logger.Errorw("webhook setup failed and no update handler is configured; bot cannot receive updates until the webhook URL is corrected",
"webhook_url", m.currentConfig.WebhookURL,
"error", err,
)
return err
}
@@ -126,6 +140,24 @@ func (m *BotServiceManager) startWebhookMode() error {
return nil
}
// registerAndVerifyWebhook calls setWebhook and then reads back getWebhookInfo
// to confirm Telegram actually accepted and stored the URL. A successful
// setWebhook response alone is not sufficient: the URL can end up unset if a
// later DeleteWebhook races in, or if an intermediary rewrites the request.
func (m *BotServiceManager) registerAndVerifyWebhook() error {
if err := m.botService.SetWebhook(m.currentConfig.WebhookURL); err != nil {
return fmt.Errorf("set webhook: %w", err)
}
registered, err := m.botService.GetWebhookInfo()
if err != nil {
return fmt.Errorf("verify webhook registration: %w", err)
}
if registered != m.currentConfig.WebhookURL {
return fmt.Errorf("webhook URL mismatch after setWebhook: registered=%q expected=%q", registered, m.currentConfig.WebhookURL)
}
return nil
}
// setupBotCommands sets up the bot command menu for auto-completion.
// Sets user commands as the default scope so regular users don't see admin commands.
func (m *BotServiceManager) setupBotCommands() {