fix(telegram): tolerate empty webhook delete response

This commit is contained in:
tiennm99 committed 2026-06-27 16:55:46 +07:00
1 parent 259b2dfdbd
commit 76d7495b4b
2 files changed
+142 -2

No files matched your search

+39 -1
View File
@@ -2,7 +2,9 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
@@ -13,6 +15,7 @@ import (
"time"
tgbot "github.com/go-telegram/bot"
tgmodels "github.com/go-telegram/bot/models"
"github.com/tiennm99/openai-status-bot/internal/bot"
"github.com/tiennm99/openai-status-bot/internal/config"
"github.com/tiennm99/openai-status-bot/internal/health"
@@ -24,6 +27,11 @@ import (
"go.mongodb.org/mongo-driver/v2/mongo/options"
)
type telegramWebhookManager interface {
DeleteWebhook(context.Context, *tgbot.DeleteWebhookParams) (bool, error)
GetWebhookInfo(context.Context) (*tgmodels.WebhookInfo, error)
}
func main() {
cfg, err := config.LoadFromEnv()
if err != nil {
@@ -103,7 +111,7 @@ func main() {
os.Exit(1)
}
requestCtx, cancelRequest := contextWithOptionalTimeout(ctx, cfg.HTTPTimeout)
_, err = telegramBot.DeleteWebhook(requestCtx, &tgbot.DeleteWebhookParams{DropPendingUpdates: false})
err = clearTelegramWebhook(requestCtx, telegramBot)
cancelRequest()
if err != nil {
logger.Error("delete telegram webhook", "error", redactTelegramRuntimeError(cfg.TelegramBotToken, err))
@@ -163,3 +171,33 @@ func frameworkInitialOffset(nextOffset int64) (int64, bool) {
}
return nextOffset - 1, true
}
func clearTelegramWebhook(ctx context.Context, telegramBot telegramWebhookManager) error {
_, err := telegramBot.DeleteWebhook(ctx, &tgbot.DeleteWebhookParams{DropPendingUpdates: false})
if err == nil {
return nil
}
if !isDeleteWebhookEmptyResponseError(err) {
return err
}
info, infoErr := telegramBot.GetWebhookInfo(ctx)
if infoErr != nil {
return fmt.Errorf("deleteWebhook returned an empty response and getWebhookInfo failed: %w", errors.Join(err, infoErr))
}
if info == nil {
return fmt.Errorf("deleteWebhook returned an empty response and getWebhookInfo returned no webhook info: %w", err)
}
if info.URL != "" {
return fmt.Errorf("deleteWebhook returned an empty response and webhook is still configured: %w", err)
}
return nil
}
func isDeleteWebhookEmptyResponseError(err error) bool {
var syntaxErr *json.SyntaxError
if !errors.As(err, &syntaxErr) {
return false
}
return strings.Contains(err.Error(), "error decode response body for method deleteWebhook, ,")
}
+103 -1
View File
@@ -1,6 +1,16 @@
package main
import "testing"
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
tgbot "github.com/go-telegram/bot"
tgmodels "github.com/go-telegram/bot/models"
)
func TestFrameworkInitialOffsetSeedsLastSeenUpdate(t *testing.T) {
tests := []struct {
@@ -23,3 +33,95 @@ func TestFrameworkInitialOffsetSeedsLastSeenUpdate(t *testing.T) {
})
}
}
func TestClearTelegramWebhookReturnsNilOnSuccessfulDelete(t *testing.T) {
manager := &fakeWebhookManager{}
if err := clearTelegramWebhook(context.Background(), manager); err != nil {
t.Fatalf("clearTelegramWebhook returned error: %v", err)
}
if manager.deleteCalls != 1 || manager.infoCalls != 0 {
t.Fatalf("calls = delete %d info %d, want delete 1 info 0", manager.deleteCalls, manager.infoCalls)
}
}
func TestClearTelegramWebhookAcceptsEmptyDeleteResponseWhenWebhookGone(t *testing.T) {
manager := &fakeWebhookManager{
deleteErr: emptyDeleteWebhookResponseError(t),
webhookInfo: &tgmodels.WebhookInfo{},
}
if err := clearTelegramWebhook(context.Background(), manager); err != nil {
t.Fatalf("clearTelegramWebhook returned error: %v", err)
}
if manager.deleteCalls != 1 || manager.infoCalls != 1 {
t.Fatalf("calls = delete %d info %d, want delete 1 info 1", manager.deleteCalls, manager.infoCalls)
}
}
func TestClearTelegramWebhookFailsWhenWebhookStillConfiguredAfterEmptyDeleteResponse(t *testing.T) {
manager := &fakeWebhookManager{
deleteErr: emptyDeleteWebhookResponseError(t),
webhookInfo: &tgmodels.WebhookInfo{URL: "https://example.com/webhook"},
}
err := clearTelegramWebhook(context.Background(), manager)
if err == nil || !strings.Contains(err.Error(), "webhook is still configured") {
t.Fatalf("error = %v, want webhook still configured", err)
}
}
func TestClearTelegramWebhookFailsWhenWebhookInfoCheckFails(t *testing.T) {
infoErr := errors.New("getWebhookInfo unavailable")
manager := &fakeWebhookManager{
deleteErr: emptyDeleteWebhookResponseError(t),
webhookInfoErr: infoErr,
}
err := clearTelegramWebhook(context.Background(), manager)
if !errors.Is(err, infoErr) {
t.Fatalf("error = %v, want wrapping info error", err)
}
}
func TestClearTelegramWebhookReturnsNonEmptyDeleteError(t *testing.T) {
deleteErr := errors.New("telegram unauthorized")
manager := &fakeWebhookManager{deleteErr: deleteErr}
err := clearTelegramWebhook(context.Background(), manager)
if !errors.Is(err, deleteErr) {
t.Fatalf("error = %v, want delete error", err)
}
if manager.infoCalls != 0 {
t.Fatalf("infoCalls = %d, want 0", manager.infoCalls)
}
}
type fakeWebhookManager struct {
deleteErr error
webhookInfo *tgmodels.WebhookInfo
webhookInfoErr error
deleteCalls int
infoCalls int
}
func (f *fakeWebhookManager) DeleteWebhook(context.Context, *tgbot.DeleteWebhookParams) (bool, error) {
f.deleteCalls++
return f.deleteErr == nil, f.deleteErr
}
func (f *fakeWebhookManager) GetWebhookInfo(context.Context) (*tgmodels.WebhookInfo, error) {
f.infoCalls++
return f.webhookInfo, f.webhookInfoErr
}
func emptyDeleteWebhookResponseError(t *testing.T) error {
t.Helper()
var response struct{}
err := json.Unmarshal([]byte{}, &response)
if err == nil {
t.Fatal("expected json decode error")
}
return fmt.Errorf("error decode response body for method deleteWebhook, , %w", err)
}