• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bots-go-framework / bots-fw / 13466578417

21 Feb 2025 10:50PM UTC coverage: 3.762% (-0.02%) from 3.784%
13466578417

push

github

trakhimenok
fix: better error handling for callbacks

0 of 27 new or added lines in 2 files covered. (0.0%)

1 existing line in 1 file now uncovered.

115 of 3057 relevant lines covered (3.76%)

0.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

1.13
/botswebhook/driver.go
1
package botswebhook
2

3
import (
4
        "bytes"
5
        "context"
6
        "errors"
7
        "fmt"
8
        "github.com/bots-go-framework/bots-fw-store/botsfwmodels"
9
        "github.com/bots-go-framework/bots-fw/botinput"
10
        "github.com/bots-go-framework/bots-fw/botsdal"
11
        "github.com/bots-go-framework/bots-fw/botsfw"
12
        "github.com/bots-go-framework/bots-fw/botsfwconst"
13
        "github.com/dal-go/dalgo/dal"
14
        "github.com/dal-go/dalgo/record"
15
        "github.com/strongo/gamp"
16
        "github.com/strongo/logus"
17
        "net/http"
18
        "runtime/debug"
19
        "strings"
20
        "time"
21
)
22

23
// ErrorIcon is used to report errors to user
24
var ErrorIcon = "🚨"
25

26
// BotDriver keeps information about bots and map requests to appropriate handlers
27
type BotDriver struct {
28
        Analytics       AnalyticsSettings
29
        botHost         botsfw.BotHost
30
        panicTextFooter string
31
}
32

33
var _ botsfw.WebhookDriver = (*BotDriver)(nil) // Ensure BotDriver is implementing interface WebhookDriver
34

35
// AnalyticsSettings keeps data for Google Analytics
36
type AnalyticsSettings struct {
37
        GaTrackingID string // TODO: Refactor to list of analytics providers
38
        Enabled      func(r *http.Request) bool
39
}
40

41
// NewBotDriver registers new bot driver (TODO: describe why we need it)
42
func NewBotDriver(gaSettings AnalyticsSettings, botHost botsfw.BotHost, panicTextFooter string) BotDriver {
1✔
43
        if botHost == nil {
2✔
44
                panic("required argument botHost == nil")
1✔
45
        }
46
        return BotDriver{
×
47
                Analytics:       gaSettings,
×
48
                botHost:         botHost,
×
49
                panicTextFooter: panicTextFooter,
×
50
        }
×
51
}
52

53
// RegisterWebhookHandlers adds handlers to a bot driver
54
func (d BotDriver) RegisterWebhookHandlers(httpRouter botsfw.HttpRouter, pathPrefix string, webhookHandlers ...botsfw.WebhookHandler) {
×
55
        for _, webhookHandler := range webhookHandlers {
×
56
                webhookHandler.RegisterHttpHandlers(d, d.botHost, httpRouter, pathPrefix)
×
57
        }
×
58
}
59

60
// HandleWebhook takes and HTTP request and process it
61
func (d BotDriver) HandleWebhook(w http.ResponseWriter, r *http.Request, webhookHandler botsfw.WebhookHandler) {
×
62

×
63
        ctx := d.botHost.Context(r)
×
64

×
65
        //log.Debugf(c, "BotDriver.HandleWebhook()")
×
66
        if w == nil {
×
67
                panic("Parameter 'w http.ResponseWriter' is nil")
×
68
        }
69
        if r == nil {
×
70
                panic("Parameter 'r *http.Request' is nil")
×
71
        }
72
        if webhookHandler == nil {
×
73
                panic("Parameter 'webhookHandler WebhookHandler' is nil")
×
74
        }
75

76
        // A bot can receiver multiple messages in a single request
77
        botContext, entriesWithInputs, err := webhookHandler.GetBotContextAndInputs(ctx, r)
×
78

×
79
        if d.invalidContextOrInputs(ctx, w, r, botContext, entriesWithInputs, err) {
×
80
                return
×
81
        }
×
82

83
        if len(entriesWithInputs) > 1 {
×
84
                log.Debugf(ctx, "BotDriver.HandleWebhook() => botCode=%v, len(entriesWithInputs): %d", botContext.BotSettings.Code, len(entriesWithInputs))
×
85
        }
×
86

87
        //botCoreStores := webhookHandler.CreateBotCoreStores(d.appContext, r)
88
        //defer func() {
89
        //        if whc != nil { // TODO: How do deal with Facebook multiple entries per request?
90
        //                //log.Debugf(c, "Closing BotChatStore...")
91
        //                //chatData := whc.ChatData()
92
        //                //if chatData != nil && chatData.GetPreferredLanguage() == "" {
93
        //                //        chatData.SetPreferredLanguage(whc.DefaultLocale().Code5)
94
        //                //}
95
        //        }
96
        //}()
97

NEW
98
        handleErrorAndReturnHttpError := func(err error, message string) {
×
99
                logus.Errorf(ctx, "%s: %v", message, err)
×
100
                errText := fmt.Sprintf("%s: %s: %v", http.StatusText(http.StatusInternalServerError), message, err)
×
101
                http.Error(w, errText, http.StatusInternalServerError)
×
102
        }
×
103

NEW
104
        handleErrorAndReturnHttpOK := func(err error, message string) {
×
NEW
105
                logus.Errorf(ctx, "%s: %v\nHTTP will return status OK", message, err)
×
NEW
106
                w.WriteHeader(http.StatusOK)
×
NEW
107
        }
×
108

109
        for _, entryWithInputs := range entriesWithInputs {
×
110
                for i, input := range entryWithInputs.Inputs {
×
NEW
111
                        var handleError func(err error, message string)
×
NEW
112
                        if input.InputType() == botinput.WebhookInputCallbackQuery {
×
NEW
113
                                handleError = handleErrorAndReturnHttpOK
×
NEW
114
                        } else {
×
NEW
115
                                handleError = handleErrorAndReturnHttpError
×
NEW
116
                        }
×
117
                        if err = d.processWebhookInput(ctx, w, r, webhookHandler, botContext, i, input, handleError); err != nil {
×
118
                                log.Errorf(ctx, "Failed to process input[%v]: %v", i, err)
×
119
                        }
×
120
                }
121
        }
122
}
123

124
var isMissingGATrackingAlreadyReported bool
125

126
func (d BotDriver) processWebhookInput(
127
        ctx context.Context,
128
        w http.ResponseWriter, r *http.Request, webhookHandler botsfw.WebhookHandler,
129
        botContext *botsfw.BotContext,
130
        i int,
131
        input botinput.WebhookInput,
132
        handleError func(err error, message string),
133
) (
134
        err error,
135
) {
×
136
        var (
×
137
                whc               botsfw.WebhookContext // TODO: How do deal with Facebook multiple entries per request?
×
138
                measurementSender *gamp.BufferedClient
×
139
        )
×
140

×
141
        // Initiate Google Analytics Measurement API client
×
142
        sendStats := d.Analytics.Enabled != nil && d.Analytics.Enabled(r) ||
×
143
                botContext.BotSettings.Env == botsfw.EnvProduction
×
144

×
145
        if sendStats {
×
146
                if d.Analytics.GaTrackingID == "" {
×
147
                        sendStats = false
×
148
                        if !isMissingGATrackingAlreadyReported {
×
149
                                log.Warningf(ctx, "driver.Analytics.GaTrackingID is not set")
×
150
                        }
×
151
                } else {
×
152
                        botHost := botContext.BotHost
×
153
                        measurementSender = gamp.NewBufferedClient("", botHost.GetHTTPClient(ctx), func(err error) {
×
154
                                log.Errorf(ctx, "Failed to log to GA: %v", err)
×
155
                        })
×
156
                }
157
        } else {
×
158
                log.Debugf(ctx, "botContext.BotSettings.Env=%s, sendStats=%t", botContext.BotSettings.Env, sendStats)
×
159
        }
×
160

161
        started := time.Now()
×
162

×
163
        defer func() {
×
164
                log.Debugf(ctx, "driver.deferred(recover) - checking for panic & flush GA")
×
165
                if sendStats {
×
166
                        timing := gamp.NewTiming(time.Since(started))
×
167
                        timing.TrackingID = d.Analytics.GaTrackingID // TODO: What to do if different FB bots have different Tacking IDs? Can FB handler get messages for different bots? If not (what probably is the case) can we get ID from bot settings instead of driver?
×
168
                        if err := measurementSender.Queue(timing); err != nil {
×
169
                                log.Errorf(ctx, "Failed to log timing to GA: %v", err)
×
170
                        }
×
171
                }
172

173
                reportError := func(recovered interface{}) {
×
174
                        messageText := fmt.Sprintf("Server error (panic): %v\n\n%v", recovered, d.panicTextFooter)
×
175
                        stack := string(debug.Stack())
×
176
                        log.Criticalf(ctx, "Panic recovered: %s\n%s", messageText, stack)
×
177

×
178
                        if sendStats { // Zero if GA is disabled
×
179
                                d.reportErrorToGA(ctx, whc, measurementSender, messageText)
×
180
                        }
×
181

182
                        if whc != nil {
×
183
                                var chatID string
×
184
                                if chatID, err = whc.Input().BotChatID(); err == nil && chatID != "" {
×
185
                                        if responder := whc.Responder(); responder != nil {
×
186
                                                if _, err = responder.SendMessage(ctx, whc.NewMessage(ErrorIcon+" "+messageText), botsfw.BotAPISendMessageOverResponse); err != nil {
×
187
                                                        log.Errorf(ctx, fmt.Errorf("failed to report error to user: %w", err).Error())
×
188
                                                }
×
189
                                        }
190
                                }
191
                        }
192
                }
193

194
                if recovered := recover(); recovered != nil {
×
195
                        reportError(recovered)
×
196
                } else if sendStats {
×
197
                        //log.Debugf(ctx, "Flushing GA...")
×
198
                        if err = measurementSender.Flush(); err != nil {
×
199
                                log.Warningf(ctx, "Failed to flush to GA: %v", err)
×
200
                        } else if queueDepth := measurementSender.QueueDepth(); queueDepth > 0 {
×
201
                                log.Debugf(ctx, "Sent to GA: %v items", queueDepth)
×
202
                        }
×
203
                } else {
×
204
                        log.Debugf(ctx, "GA: sendStats=false")
×
205
                }
×
206
        }()
207

208
        if input == nil {
×
209
                panic(fmt.Sprintf("entryWithInputs.Inputs[%d] == nil", i))
×
210
        }
211
        d.logInput(ctx, i, input)
×
212
        var db dal.DB
×
213
        if db, err = botContext.BotSettings.GetDatabase(ctx); err != nil {
×
214
                err = fmt.Errorf("failed to get bot database: %w", err)
×
215
                return
×
216
        }
×
217

218
        whcArgs := botsfw.NewCreateWebhookContextArgs(r, botContext.AppContext, *botContext, input, db, measurementSender)
×
219
        if whc, err = webhookHandler.CreateWebhookContext(whcArgs); err != nil {
×
220
                handleError(err, "Failed to create WebhookContext")
×
221
                return
×
222
        }
×
223
        chatData := whc.ChatData()
×
224

×
225
        if chatData != nil && chatData.GetAppUserID() == "" {
×
226
                err = db.RunReadwriteTransaction(ctx, func(ctx context.Context, tx dal.ReadwriteTransaction) (err error) {
×
227

×
228
                        recordsToInsert := make([]dal.Record, 0)
×
229

×
230
                        // chatData can be nil for inline requests
×
231
                        // TODO: Should we try to deduct chat ID from user ID for inline queries inside a bot chat for "chat_type": "sender"?
×
232

×
233
                        platformID := whc.BotPlatform().ID()
×
234
                        botID := whc.GetBotCode()
×
235
                        appContext := whc.AppContext()
×
236
                        var appUser record.DataWithID[string, botsfwmodels.AppUserData]
×
237
                        var botUser botsdal.BotUser
×
238
                        bot := botsdal.Bot{
×
239
                                Platform: botsfwconst.Platform(platformID),
×
240
                                ID:       botID,
×
241
                                User:     whc.Input().GetSender(),
×
242
                        }
×
243
                        if appUser, botUser, err = appContext.CreateAppUserFromBotUser(ctx, tx, bot); err != nil {
×
244
                                return
×
245
                        }
×
246
                        if appUser.Record != nil {
×
247
                                recordsToInsert = append(recordsToInsert, appUser.Record)
×
248
                        }
×
249
                        if botUser.Record != nil {
×
250
                                recordsToInsert = append(recordsToInsert, botUser.Record)
×
251
                        }
×
252

253
                        chatData.SetAppUserID(appUser.ID)
×
254

×
255
                        for _, recordToInsert := range recordsToInsert {
×
256
                                if err = tx.Insert(ctx, recordToInsert); err != nil {
×
257
                                        return
×
258
                                }
×
259
                        }
260
                        return
×
261
                })
262
                if err != nil {
×
263
                        handleError(err, fmt.Sprintf("Failed to run transaction for entriesWithInputs[%d]", i))
×
264
                        return
×
265
                }
×
266
        }
267

268
        responder := webhookHandler.GetResponder(w, whc) // TODO: Move inside webhookHandler.CreateWebhookContext()?
×
269
        router := botContext.BotSettings.Profile.Router()
×
270

×
271
        if err = router.Dispatch(webhookHandler, responder, whc); err != nil {
×
272
                handleError(err, "Failed to dispatch")
×
273
                return
×
274
        }
×
275

276
        return
×
277
}
278

279
func (BotDriver) invalidContextOrInputs(c context.Context, w http.ResponseWriter, r *http.Request, botContext *botsfw.BotContext, entriesWithInputs []botsfw.EntryInputs, err error) bool {
×
280
        if err != nil {
×
281
                var errAuthFailed botsfw.ErrAuthFailed
×
282
                if errors.As(err, &errAuthFailed) {
×
283
                        log.Warningf(c, "Auth failed: %v", err)
×
284
                        http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
×
285
                }
×
286
                return true
×
287
        }
288
        if botContext == nil {
×
289
                if entriesWithInputs == nil {
×
290
                        log.Warningf(c, "botContext == nil, entriesWithInputs == nil")
×
291
                } else if len(entriesWithInputs) == 0 {
×
292
                        log.Warningf(c, "botContext == nil, len(entriesWithInputs) == 0")
×
293
                } else {
×
294
                        log.Errorf(c, "botContext == nil, len(entriesWithInputs) == %v", len(entriesWithInputs))
×
295
                }
×
296
                return true
×
297
        } else if entriesWithInputs == nil {
×
298
                log.Errorf(c, "entriesWithInputs == nil")
×
299
                return true
×
300
        }
×
301

302
        switch botContext.BotSettings.Env {
×
303
        case botsfw.EnvLocal:
×
304
                if !isRunningLocally(r.Host) {
×
305
                        log.Warningf(c, "whc.GetBotSettings().Mode == Local, host: %v", r.Host)
×
306
                        w.WriteHeader(http.StatusBadRequest)
×
307
                        return true
×
308
                }
×
309
        case botsfw.EnvProduction:
×
310
                if isRunningLocally(r.Host) {
×
311
                        log.Warningf(c, "whc.GetBotSettings().Mode == Production, host: %v", r.Host)
×
312
                        w.WriteHeader(http.StatusBadRequest)
×
313
                        return true
×
314
                }
×
315
        }
316

317
        return false
×
318
}
319

320
func isRunningLocally(host string) bool { // TODO(help-wanted): allow customization
×
321
        result := host == "localhost" ||
×
322
                strings.HasSuffix(host, ".ngrok.io") ||
×
323
                strings.HasSuffix(host, ".ngrok.dev") ||
×
324
                strings.HasSuffix(host, ".ngrok.app") ||
×
325
                strings.HasSuffix(host, ".ngrok-free.app")
×
326
        return result
×
327
}
×
328

329
func (BotDriver) reportErrorToGA(c context.Context, whc botsfw.WebhookContext, measurementSender *gamp.BufferedClient, messageText string) {
×
330
        log.Warningf(c, "reportErrorToGA() is temporary disabled")
×
331

×
332
        ga := whc.GA()
×
333
        if ga == nil {
×
334
                return
×
335
        }
×
336
        gaMessage := gamp.NewException(messageText, true)
×
337
        gaMessage.Common = ga.GaCommon()
×
338

×
339
        if err := ga.Queue(gaMessage); err != nil {
×
340
                log.Errorf(c, "Failed to queue exception message for GA: %v", err)
×
341
        } else {
×
342
                log.Debugf(c, "Exception message queued for GA.")
×
343
        }
×
344

345
        if err := measurementSender.Flush(); err != nil {
×
346
                log.Errorf(c, "Failed to flush GA buffer after exception: %v", err)
×
347
        } else {
×
348
                log.Debugf(c, "GA buffer flushed after exception")
×
349
        }
×
350
}
351

352
func (BotDriver) logInput(c context.Context, i int, input botinput.WebhookInput) {
×
353
        sender := input.GetSender()
×
354
        prefix := fmt.Sprintf("BotUser#%v(%v %v)", sender.GetID(), sender.GetFirstName(), sender.GetLastName())
×
355
        switch input := input.(type) {
×
356
        case botinput.WebhookTextMessage:
×
357
                log.Debugf(c, "%s => text: %v", prefix, input.Text())
×
358
        case botinput.WebhookNewChatMembersMessage:
×
359
                newMembers := input.NewChatMembers()
×
360
                var b bytes.Buffer
×
361
                b.WriteString(fmt.Sprintf("NewChatMembers: %d", len(newMembers)))
×
362
                for i, member := range newMembers {
×
363
                        b.WriteString(fmt.Sprintf("\t%d: (%v) - %v %v", i+1, member.GetUserName(), member.GetFirstName(), member.GetLastName()))
×
364
                }
×
365
                log.Debugf(c, b.String())
×
366
        case botinput.WebhookContactMessage:
×
367
                log.Debugf(c, "%s => Contact(botUserID=%s, firstName=%s)", prefix, input.GetBotUserID(), input.GetFirstName())
×
368
        case botinput.WebhookCallbackQuery:
×
369
                callbackData := input.GetData()
×
370
                log.Debugf(c, "%s => callback: %v", prefix, callbackData)
×
371
        case botinput.WebhookInlineQuery:
×
372
                log.Debugf(c, "%s => inline query: %v", prefix, input.GetQuery())
×
373
        case botinput.WebhookChosenInlineResult:
×
374
                log.Debugf(c, "%s => chosen InlineMessageID: %v", prefix, input.GetInlineMessageID())
×
375
        case botinput.WebhookReferralMessage:
×
376
                log.Debugf(c, "%s => text: %v", prefix, input.(botinput.WebhookTextMessage).Text())
×
377
        case botinput.WebhookSharedUsersMessage:
×
378
                sharedUsers := input.GetSharedUsers()
×
379
                log.Debugf(c, "%s => shared %d users", prefix, len(sharedUsers))
×
380
        default:
×
381
                log.Warningf(c, "unknown input[%v] type %T", i, input)
×
382
        }
383
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc