Go Fiber v3: BodyRaw for Sume webhooks, and copy it before you queue

Sign-check a Sume webhook in Go Fiber with c.BodyRaw() and hmac.Equal, then copy the bytes before handing them to a goroutine, since Fiber reuses them.

5 min readSume
All posts

In Go Fiber, verify a Sume webhook with c.BodyRaw(), not c.Body(): BodyRaw returns the bytes as sent, while Body() decompresses according to Content-Encoding. Recompute HMAC SHA-256 over <timestamp>.<raw_body>, compare each sume-v1= header entry with hmac.Equal, and send 204 through c.SendStatus. If you queue the event for a goroutine, copy the bytes first.

That last step is the Fiber-specific trap. Handlers return before your worker runs, and the buffer is not yours after that.

What does Fiber say about the body buffer?

The Fiber Ctx documentation says the value Body() returns is valid only within the handler, that you should not store references, and that you should make copies or use the Immutable setting instead. It also notes that the fiber.Ctx instance is pooled and reused after the handler returns.

For a webhook that means go process(c.BodyRaw()) is a data race that surfaces as signature failures or corrupted JSON under load. Copy with append([]byte(nil), raw...) before the handoff, or write the event to durable storage inside the handler, which Sume's docs recommend anyway before you return 2xx.

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"strconv"
	"strings"
	"time"
)

func verify(secret, ts, header string, body []byte) bool {
	t, err := strconv.ParseInt(ts, 10, 64)
	d := time.Now().Unix() - t
	if secret == "" || header == "" || err != nil || d > 300 || d < -300 {
		return false
	}
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(strconv.FormatInt(t, 10) + "."))
	mac.Write(body)
	want := []byte("sume-v1=" + hex.EncodeToString(mac.Sum(nil)))
	ok := false
	for _, e := range strings.Split(header, ",") {
		if hmac.Equal([]byte(strings.TrimSpace(e)), want) {
			ok = true
		}
	}
	return ok
}

How do I register the route?

Fiber v3 handlers take fiber.Ctx by value as an interface. Read headers with c.Get, verify against BodyRaw, and make the copy only after the check passes.

package main

import (
	"os"

	"github.com/gofiber/fiber/v3"
)

func main() {
	app := fiber.New()
	app.Post("/hooks/sume", func(c fiber.Ctx) error {
		raw := c.BodyRaw()
		if !verify(os.Getenv("SUME_COM_WEBHOOK_SIGNING_SECRET"),
			c.Get("x-sume-webhook-timestamp"),
			c.Get("x-sume-webhook-signature"), raw) {
			return c.SendStatus(fiber.StatusUnauthorized)
		}
		event := append([]byte(nil), raw...) // own the bytes before queueing
		go store(event)
		return c.SendStatus(fiber.StatusNoContent)
	})
	app.Listen(":8080")
}

func store(event []byte) { /* insert keyed by job_id or request_id */ }

Body or BodyRaw, which one for which job?

Both read the request body, but they answer different questions.

Fiber body accessors, read 2026-10-11 from the Fiber Ctx documentation, with the Sume rule for each.
AccessorWhat Fiber documentsUse for Sume
c.Body()Decompresses by Content-Encoding, bounded by the app BodyLimit; valid only within the handlerParsing after verification, if a proxy compresses
c.BodyRaw()Raw bytes, no decompressionComputing the signature
c.Get(name)Returns a request header valuex-sume-webhook-timestamp and x-sume-webhook-signature

What mistakes show up most often?

Most Fiber failures are not cryptography bugs. They are lifetime and encoding bugs around the body, and they appear only under concurrent load, which is why a local curl test passes and production does not.

Run go test -race against a handler test that fires several signed requests in parallel while a goroutine reads the stored bytes. If the race detector stays quiet and every signature verifies, the copy is doing its job.

  • Verify against BodyRaw(), not Body().
  • Copy the bytes before any goroutine, channel or queue touches them.
  • Compare with hmac.Equal for every sume-v1= entry.
  • Refuse an empty secret in verify itself.

What else does Sume need from the receiver?

Return 2xx inside 10 seconds. Sume retries network errors and non-2xx responses up to 10 times, so a goroutine panic that happens after you answered 204 loses the event silently. Write to a durable queue inside the handler, then process it elsewhere.

Dedupe on job_id for job events and request_id for run events, and keep polling the status_url for anything the ten attempts did not deliver.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume