go 57 lines · 9 steps

Building a resilient webhook handler in Gin

A Gin handler that verifies, persists, and dispatches incoming webhooks while degrading gracefully into retry on failure.

Explained by highlit
1func HandleWebhook(store WebhookStore, dispatcher *EventDispatcher) gin.HandlerFunc {
2 return func(c *gin.Context) {
3 body, err := c.GetRawData()
4 if err != nil {
5 c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "unable to read request body"})
6 return
7 }
8 
9 signature := c.GetHeader("X-Signature-256")
10 if !verifySignature(body, signature) {
11 c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid signature"})
12 return
13 }
14 
15 delivery := &WebhookDelivery{
16 ID: uuid.NewString(),
17 Source: c.Param("provider"),
18 EventType: c.GetHeader("X-Event-Type"),
19 RawBody: body,
20 Headers: flattenHeaders(c.Request.Header),
21 Status: DeliveryPending,
22 ReceivedAt: time.Now().UTC(),
23 }
24 
25 if err := store.Save(c.Request.Context(), delivery); err != nil {
26 c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "could not persist delivery"})
27 return
28 }
29 
30 if err := dispatcher.Dispatch(c.Request.Context(), delivery); err != nil {
31 delivery.Status = DeliveryFailed
32 delivery.LastError = err.Error()
33 delivery.Attempts++
34 _ = store.Update(c.Request.Context(), delivery)
35 
36 c.JSON(http.StatusAccepted, gin.H{
37 "id": delivery.ID,
38 "status": "queued_for_retry",
39 })
40 return
41 }
42 
43 delivery.Status = DeliveryProcessed
44 delivery.Attempts++
45 _ = store.Update(c.Request.Context(), delivery)
46 
47 c.JSON(http.StatusOK, gin.H{"id": delivery.ID, "status": "processed"})
48 }
49}
50 
51func flattenHeaders(h http.Header) map[string]string {
52 out := make(map[string]string, len(h))
53 for k := range h {
54 out[k] = h.Get(k)
55 }
56 return out
57}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Injecting dependencies through a factory that returns a handler keeps request logic testable and free of globals.
  2. 2Persisting an incoming request before processing it lets you recover or retry even if downstream work fails.
  3. 3Distinguishing failure modes with specific status codes tells clients exactly what happened and what to expect next.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Building a resilient webhook handler in Gin — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code