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 {