Go MaxBytesReader: cap a Sume webhook body at 1 MiB, 413 before HMAC
A webhook endpoint that reads an unbounded body invites memory abuse. A Go handler with http.MaxBytesReader, an HMAC check and a refusal of empty secrets.

A public webhook URL accepts POSTs from anyone, and io.ReadAll(r.Body) will read as much as is sent before you can check a signature. Wrap the body with http.MaxBytesReader first so an oversized request fails at the limit and answers 413. Sume's webhook bodies are small job events, so 1 MiB is far above anything legitimate.
The handler below runs on Go 1.27.1. In a local test it returned 200 for a valid signed request, 413 for a 2 MB body, and 401 for a wrong signature; calling it with an empty secret panics at construction, by design.
The handler
Sume signs <timestamp>.<raw_body> with HMAC-SHA256 and sends x-sume-webhook-signature: sume-v1=<hex> (comma-separated entries during a secret rotation). This version checks the signature of the bytes it read and nothing else, so see the stale-timestamp post for the 300-second window.
package main
import ("crypto/hmac"; "crypto/sha256"; "encoding/hex"; "fmt"; "io"; "net/http"; "os"; "strconv"; "strings")
func handler(secret string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20)) // 1 MiB cap
if err != nil { http.Error(w, "too large", http.StatusRequestEntityTooLarge); return }
ts, _ := strconv.Atoi(r.Header.Get("x-sume-webhook-timestamp"))
m := hmac.New(sha256.New, []byte(secret))
m.Write([]byte(fmt.Sprintf("%d.", ts))); m.Write(body)
want := hex.EncodeToString(m.Sum(nil))
for _, e := range strings.Split(r.Header.Get("x-sume-webhook-signature"), ",") {
if hmac.Equal([]byte(strings.TrimPrefix(strings.TrimSpace(e), "sume-v1=")), []byte(want)) {
w.WriteHeader(http.StatusOK); return
}
}
http.Error(w, "bad signature", http.StatusUnauthorized)
}
}
func main() {
secret := os.Getenv("SUME_COM_WEBHOOK_SIGNING_SECRET")
if secret == "" { panic("refusing to start with an empty signing secret") }
http.Handle("/hook", handler(secret))
http.ListenAndServe(":8894", nil)
}Order matters
Cap the size, read the raw bytes, compare the HMAC with hmac.Equal, and only then parse the JSON. Re-encoding the body before the check changes the bytes and breaks the signature. Return 2xx quickly once verified and do the real work afterwards, because Sume retries a non-2xx delivery.
Fetch the signing secret from GET /v1/webhooks/signing-secret and keep it in your secret store, not in the source file.
Sources
Related posts
More in Developers
- Go: poll a Sume job and stop on any terminal state, even a new one
A Go status loop that names completed, failed and canceled, stops on terminal=true for anything else, and never sleeps less than next_poll_after_seconds.
- Go WaitGroup fan-out for Wan 3.0 submits: no mutex, clean -race
Submit 12 Wan 3.0 clips from Go with sync.WaitGroup and a 4-slot channel. Each goroutine owns one slice index, so no mutex. Runs clean under go run -race.
- gpt-image-1.5 is removed Dec 1: a 55-day cutover calendar
OpenAI removes gpt-image-1.5, gpt-image-1-mini and chatgpt-image-latest on December 1, 2026. A week-by-week plan from October 7 to move to GPT Image 2.5.
- GPT Image 2.5 banner: 3840x1280 passes the 3:1 rule
3840x1280 is exactly 3:1, both edges are multiples of 16 and it is 4,915,200 pixels, so Sume accepts it on GPT Image 2.5. The four checks shown.
Written by Sume