From 54a47d695456d83f840dc58fc94d6d4d7ff461b6 Mon Sep 17 00:00:00 2001 From: tsan88 Date: Fri, 4 Sep 2026 15:38:21 +0700 Subject: [PATCH] feat(sentry): decode zstd request bodies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sentry-php 4.x sends envelopes with Content-Encoding: zstd by default, but decompress() only knew gzip and deflate. The body stayed compressed, failed to parse as an envelope and the event was dropped — without a "failed to parse" warning, so the loss was invisible in the logs while the SDK saw HTTP 200. Handles both the declared encoding and the zstd magic bytes, matching how gzip and zlib are already auto-detected. github.com/klauspost/compress is promoted from an indirect to a direct dependency; it is already present in go.sum (pulled in by prometheus/common), so no new module enters the build. --- go.mod | 1 + modules/sentry/handler.go | 23 ++++++++++++++++++++++ modules/sentry/handler_test.go | 35 ++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/go.mod b/go.mod index 8fc7d90..f4f4290 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.26.1 require ( github.com/emersion/go-smtp v0.24.0 github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/klauspost/compress v1.18.0 github.com/modelcontextprotocol/go-sdk v1.4.0 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 diff --git a/modules/sentry/handler.go b/modules/sentry/handler.go index 5fff42d..dcb00a0 100644 --- a/modules/sentry/handler.go +++ b/modules/sentry/handler.go @@ -11,6 +11,8 @@ import ( "net/http" "strings" + "github.com/klauspost/compress/zstd" + "github.com/buggregator/go-buggregator/internal/event" ) @@ -324,9 +326,21 @@ func decompress(data []byte, encoding string) []byte { if d, err := decompressZlib(data); err == nil { return d } + case "zstd": + if d, err := decompressZstd(data); err == nil { + return d + } } // Auto-detect by magic bytes (Sentry SDKs sometimes omit the header). + if len(data) >= 4 { + // Zstd magic: 0x28 0xb5 0x2f 0xfd + if data[0] == 0x28 && data[1] == 0xb5 && data[2] == 0x2f && data[3] == 0xfd { + if d, err := decompressZstd(data); err == nil { + return d + } + } + } if len(data) >= 2 { // Gzip magic: 0x1f 0x8b if data[0] == 0x1f && data[1] == 0x8b { @@ -354,6 +368,15 @@ func decompressGzip(data []byte) ([]byte, error) { return io.ReadAll(r) } +func decompressZstd(data []byte) ([]byte, error) { + r, err := zstd.NewReader(bytes.NewReader(data)) + if err != nil { + return nil, err + } + defer r.Close() + return io.ReadAll(r) +} + func decompressZlib(data []byte) ([]byte, error) { r, err := zlib.NewReader(bytes.NewReader(data)) if err != nil { diff --git a/modules/sentry/handler_test.go b/modules/sentry/handler_test.go index 963b988..a242a9a 100644 --- a/modules/sentry/handler_test.go +++ b/modules/sentry/handler_test.go @@ -8,6 +8,8 @@ import ( "strconv" "strings" "testing" + + "github.com/klauspost/compress/zstd" ) func TestHandler_Priority(t *testing.T) { @@ -152,6 +154,39 @@ func TestDecompress(t *testing.T) { } }) + // sentry-php sends Content-Encoding: zstd by default since 4.x. Without zstd + // support the body stays compressed, fails to parse as an envelope and the + // event is dropped silently — the request still answers 200. + t.Run("zstd with header", func(t *testing.T) { + var buf bytes.Buffer + w, err := zstd.NewWriter(&buf) + if err != nil { + t.Fatalf("zstd writer: %v", err) + } + w.Write([]byte("zstd payload")) + w.Close() + + got := decompress(buf.Bytes(), "zstd") + if string(got) != "zstd payload" { + t.Errorf("got %q, want %q", got, "zstd payload") + } + }) + + t.Run("zstd auto-detect by magic bytes", func(t *testing.T) { + var buf bytes.Buffer + w, err := zstd.NewWriter(&buf) + if err != nil { + t.Fatalf("zstd writer: %v", err) + } + w.Write([]byte("zstd auto-detected")) + w.Close() + + got := decompress(buf.Bytes(), "") + if string(got) != "zstd auto-detected" { + t.Errorf("got %q, want %q", got, "zstd auto-detected") + } + }) + t.Run("empty data", func(t *testing.T) { got := decompress([]byte{}, "gzip") if len(got) != 0 {