Skip to content

Commit 18eefef

Browse files
committed
feat(echo): add support for Echo v5
BREAKING CHANGE: echo integration now requires github.com/labstack/echo/v5 and Go 1.25. Echo v4 is no longer supported. - Bump github.com/labstack/echo from v4.10.1 to v5.0.0. - Set go directive to 1.25.0 in echo/go.mod. - Drop Echo v4-only indirects (gommon, go-colorable, go-isatty, bytebufferpool, fasttemplate, x/crypto, x/net) and update remaining (x/sys, x/text). go.sum updated accordingly. - Use *echo.Context instead of echo.Context in handler and public API (GetHubFromContext, SetHubOnContext, GetSpanFromContext) to match v5’s pointer-based Context. - Update all handler signatures in code and docs from `func(c echo.Context) error` to `func(c *echo.Context) error`. - In v5, Context.Response() returns http.ResponseWriter, not *Response, so ctx.Response().Status is no longer available. - Replace direct ctx.Response().Status with echo.UnwrapResponse(ctx.Response()) to obtain *echo.Response and use resp.Status when UnwrapResponse succeeds and resp.Status != 0. - When UnwrapResponse fails (e.g. middleware replaces the response with a writer that does not unwrap to *echo.Response), leave status at its zero value (0) instead of defaulting to 200. - For handler-returned errors, use echo.HTTPStatusCoder instead of *echo.HTTPError so that both *HTTPError and unexported *httpError (ErrNotFound, ErrMethodNotAllowed, etc.) are handled and the correct status is used for the transaction. - In TestIntegration, skip route registration when Handler is nil so the “404 / no route” case does not call router.GET("", nil). Echo v5’s router rejects Handler == nil and panics with “adding route without handler function”. - Add TestUnwrapResponseError: when the response is wrapped by a writer that does not implement Unwrap() (e.g. &struct{ http.ResponseWriter }{}), UnwrapResponse returns an error; the middleware must not panic and must record http.response.status_code as 0 in the transaction. - README.md and example_test.go: switch imports and examples from echo/v4 to echo/v5 and from echo.Context to *echo.Context.
1 parent d7b1a9a commit 18eefef

File tree

6 files changed

+100
-59
lines changed

6 files changed

+100
-59
lines changed

echo/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import (
2424

2525
"github.com/getsentry/sentry-go"
2626
sentryecho "github.com/getsentry/sentry-go/echo"
27-
"github.com/labstack/echo/v4"
28-
"github.com/labstack/echo/v4/middleware"
27+
"github.com/labstack/echo/v5"
28+
"github.com/labstack/echo/v5/middleware"
2929
)
3030

3131
// To initialize Sentry's handler, you need to initialize Sentry itself beforehand
@@ -45,7 +45,7 @@ app.Use(middleware.Recover())
4545
app.Use(sentryecho.New(sentryecho.Options{}))
4646

4747
// Set up routes
48-
app.GET("/", func(ctx echo.Context) error {
48+
app.GET("/", func(ctx *echo.Context) error {
4949
return ctx.String(http.StatusOK, "Hello, World!")
5050
})
5151

@@ -90,15 +90,15 @@ app.Use(sentryecho.New(sentryecho.Options{
9090
}))
9191

9292
app.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
93-
return func(ctx echo.Context) error {
93+
return func(ctx *echo.Context) error {
9494
if hub := sentryecho.GetHubFromContext(ctx); hub != nil {
9595
hub.Scope().SetTag("someRandomTag", "maybeYouNeedIt")
9696
}
9797
return next(ctx)
9898
}
9999
})
100100

101-
app.GET("/", func(ctx echo.Context) error {
101+
app.GET("/", func(ctx *echo.Context) error {
102102
if hub := sentryecho.GetHubFromContext(ctx); hub != nil {
103103
hub.WithScope(func(scope *sentry.Scope) {
104104
scope.SetExtra("unwantedQuery", "someQueryDataMaybe")
@@ -108,7 +108,7 @@ app.GET("/", func(ctx echo.Context) error {
108108
return ctx.String(http.StatusOK, "Hello, World!")
109109
})
110110

111-
app.GET("/foo", func(ctx echo.Context) error {
111+
app.GET("/foo", func(ctx *echo.Context) error {
112112
// sentryecho handler will catch it just fine. Also, because we attached "someRandomTag"
113113
// in the middleware before, it will be sent through as well
114114
panic("y tho")

echo/example_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import (
66

77
"github.com/getsentry/sentry-go"
88
sentryecho "github.com/getsentry/sentry-go/echo"
9-
"github.com/labstack/echo/v4"
9+
"github.com/labstack/echo/v5"
1010
)
1111

1212
func ExampleGetSpanFromContext() {
1313
router := echo.New()
1414
router.Use(sentryecho.New(sentryecho.Options{}))
15-
router.GET("/", func(c echo.Context) error {
15+
router.GET("/", func(c *echo.Context) error {
1616
expensiveThing := func(ctx context.Context) error {
1717
span := sentry.StartTransaction(ctx, "expensive_thing")
1818
defer span.Finish()

echo/go.mod

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,16 @@
11
module github.com/getsentry/sentry-go/echo
22

3-
go 1.24.0
3+
go 1.25.0
44

55
replace github.com/getsentry/sentry-go => ../
66

77
require (
88
github.com/getsentry/sentry-go v0.43.0
99
github.com/google/go-cmp v0.5.9
10-
github.com/labstack/echo/v4 v4.10.1
10+
github.com/labstack/echo/v5 v5.0.0
1111
)
1212

1313
require (
14-
github.com/labstack/gommon v0.4.2 // indirect
15-
github.com/mattn/go-colorable v0.1.13 // indirect
16-
github.com/mattn/go-isatty v0.0.20 // indirect
17-
github.com/valyala/bytebufferpool v1.0.0 // indirect
18-
github.com/valyala/fasttemplate v1.2.2 // indirect
19-
golang.org/x/crypto v0.48.0 // indirect
20-
golang.org/x/net v0.50.0 // indirect
21-
golang.org/x/sys v0.41.0 // indirect
22-
golang.org/x/text v0.34.0 // indirect
14+
golang.org/x/sys v0.40.0 // indirect
15+
golang.org/x/text v0.33.0 // indirect
2316
)

echo/go.sum

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,19 @@ github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxI
44
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
55
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
66
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
7-
github.com/labstack/echo/v4 v4.10.1 h1:rB+D8In9PWjsp1OpHaqK+t04nQv/SBD1IoIcXCg0lpY=
8-
github.com/labstack/echo/v4 v4.10.1/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
9-
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
10-
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
11-
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
12-
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
13-
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
14-
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
15-
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
7+
github.com/labstack/echo/v5 v5.0.0 h1:JHKGrI0cbNsNMyKvranuY0C94O4hSM7yc/HtwcV3Na4=
8+
github.com/labstack/echo/v5 v5.0.0/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo=
169
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
1710
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
1811
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
1912
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
2013
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
2114
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
22-
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
23-
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
24-
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
25-
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
26-
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
27-
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
15+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
16+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
2817
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
2918
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
19+
<<<<<<< HEAD
3020
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
3121
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
3222
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
@@ -37,5 +27,13 @@ golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
3727
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
3828
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
3929
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
30+
=======
31+
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
32+
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
33+
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
34+
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
35+
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
36+
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
37+
>>>>>>> c1f71ea (feat(echo): add support for Echo v5)
4038
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
4139
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

echo/sentryecho.go

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@ import (
77
"time"
88

99
"github.com/getsentry/sentry-go"
10-
"github.com/labstack/echo/v4"
10+
"github.com/labstack/echo/v5"
1111
)
1212

1313
const (
1414
// sdkIdentifier is the identifier of the Echo SDK.
1515
sdkIdentifier = "sentry.go.echo"
1616

17-
// valuesKey is used as a key to store the Sentry Hub instance on the echo.Context.
17+
// valuesKey is used as a key to store the Sentry Hub instance on the *echo.Context.
1818
valuesKey = "sentry"
1919

20-
// transactionKey is used as a key to store the Sentry transaction on the echo.Context.
20+
// transactionKey is used as a key to store the Sentry transaction on the *echo.Context.
2121
transactionKey = "sentry_transaction"
2222

23-
// errorKey is used as a key to store the error on the echo.Context.
23+
// errorKey is used as a key to store the error on the *echo.Context.
2424
errorKey = "error"
2525
)
2626

@@ -57,7 +57,7 @@ func New(options Options) echo.MiddlewareFunc {
5757
}
5858

5959
func (h *handler) handle(next echo.HandlerFunc) echo.HandlerFunc {
60-
return func(ctx echo.Context) error {
60+
return func(ctx *echo.Context) error {
6161
hub := GetHubFromContext(ctx)
6262
if hub == nil {
6363
hub = sentry.CurrentHub().Clone()
@@ -93,10 +93,13 @@ func (h *handler) handle(next echo.HandlerFunc) echo.HandlerFunc {
9393
transaction.SetData("http.request.method", r.Method)
9494

9595
defer func() {
96-
status := ctx.Response().Status
96+
var status int
97+
if resp, err := echo.UnwrapResponse(ctx.Response()); err == nil && resp.Status != 0 {
98+
status = resp.Status
99+
}
97100
if err := ctx.Get(errorKey); err != nil {
98-
if httpError, ok := err.(*echo.HTTPError); ok {
99-
status = httpError.Code
101+
if coder, ok := err.(echo.HTTPStatusCoder); ok {
102+
status = coder.StatusCode()
100103
}
101104
}
102105

@@ -135,22 +138,22 @@ func (h *handler) recoverWithSentry(hub *sentry.Hub, r *http.Request) {
135138
}
136139
}
137140

138-
// GetHubFromContext retrieves attached *sentry.Hub instance from echo.Context.
139-
func GetHubFromContext(ctx echo.Context) *sentry.Hub {
141+
// GetHubFromContext retrieves attached *sentry.Hub instance from *echo.Context.
142+
func GetHubFromContext(ctx *echo.Context) *sentry.Hub {
140143
if hub, ok := ctx.Get(valuesKey).(*sentry.Hub); ok {
141144
return hub
142145
}
143146
return nil
144147
}
145148

146-
// SetHubOnContext attaches *sentry.Hub instance to echo.Context.
147-
func SetHubOnContext(ctx echo.Context, hub *sentry.Hub) {
149+
// SetHubOnContext attaches *sentry.Hub instance to *echo.Context.
150+
func SetHubOnContext(ctx *echo.Context, hub *sentry.Hub) {
148151
ctx.Set(valuesKey, hub)
149152
}
150153

151-
// GetSpanFromContext retrieves attached *sentry.Span instance from echo.Context.
152-
// If there is no transaction on echo.Context, it will return nil.
153-
func GetSpanFromContext(ctx echo.Context) *sentry.Span {
154+
// GetSpanFromContext retrieves attached *sentry.Span instance from *echo.Context.
155+
// If there is no transaction on *echo.Context, it will return nil.
156+
func GetSpanFromContext(ctx *echo.Context) *sentry.Span {
154157
if span, ok := ctx.Get(transactionKey).(*sentry.Span); ok {
155158
return span
156159
}

echo/sentryecho_test.go

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
"github.com/getsentry/sentry-go/internal/testutils"
1616
"github.com/google/go-cmp/cmp"
1717
"github.com/google/go-cmp/cmp/cmpopts"
18-
"github.com/labstack/echo/v4"
18+
"github.com/labstack/echo/v5"
1919
)
2020

2121
func TestIntegration(t *testing.T) {
@@ -37,7 +37,7 @@ func TestIntegration(t *testing.T) {
3737
RoutePath: "/panic/:id",
3838
Method: "GET",
3939
WantStatus: 200,
40-
Handler: func(c echo.Context) error {
40+
Handler: func(c *echo.Context) error {
4141
panic("test")
4242
},
4343
WantEvent: &sentry.Event{
@@ -114,7 +114,7 @@ func TestIntegration(t *testing.T) {
114114
Method: "POST",
115115
WantStatus: 200,
116116
Body: "payload",
117-
Handler: func(c echo.Context) error {
117+
Handler: func(c *echo.Context) error {
118118
hub := sentryecho.GetHubFromContext(c)
119119
body, err := io.ReadAll(c.Request().Body)
120120
if err != nil {
@@ -168,7 +168,7 @@ func TestIntegration(t *testing.T) {
168168
RoutePath: "/get",
169169
Method: "GET",
170170
WantStatus: 200,
171-
Handler: func(c echo.Context) error {
171+
Handler: func(c *echo.Context) error {
172172
hub := sentryecho.GetHubFromContext(c)
173173
hub.CaptureMessage("get")
174174
return c.JSON(http.StatusOK, map[string]string{"status": "get"})
@@ -215,7 +215,7 @@ func TestIntegration(t *testing.T) {
215215
Method: "POST",
216216
WantStatus: 200,
217217
Body: largePayload,
218-
Handler: func(c echo.Context) error {
218+
Handler: func(c *echo.Context) error {
219219
hub := sentryecho.GetHubFromContext(c)
220220
body, err := io.ReadAll(c.Request().Body)
221221
if err != nil {
@@ -270,7 +270,7 @@ func TestIntegration(t *testing.T) {
270270
Method: "POST",
271271
WantStatus: 200,
272272
Body: "client sends, server ignores, SDK doesn't read",
273-
Handler: func(c echo.Context) error {
273+
Handler: func(c *echo.Context) error {
274274
hub := sentryecho.GetHubFromContext(c)
275275
hub.CaptureMessage("body ignored")
276276
return nil
@@ -322,7 +322,7 @@ func TestIntegration(t *testing.T) {
322322
RoutePath: "/badreq",
323323
Method: "GET",
324324
WantStatus: 400,
325-
Handler: func(c echo.Context) error {
325+
Handler: func(c *echo.Context) error {
326326
return c.JSON(http.StatusBadRequest, map[string]string{"status": "bad_request"})
327327
},
328328
WantTransaction: &sentry.Event{
@@ -376,6 +376,9 @@ func TestIntegration(t *testing.T) {
376376
router.Use(sentryecho.New(sentryecho.Options{}))
377377

378378
for _, tt := range tests {
379+
if tt.Handler == nil {
380+
continue // no route to register (e.g. 404 case: path /404/1 must not exist)
381+
}
379382
switch tt.Method {
380383
case http.MethodGet:
381384
router.GET(tt.RoutePath, tt.Handler)
@@ -499,7 +502,7 @@ func TestSetHubOnContext(t *testing.T) {
499502

500503
hub := sentry.CurrentHub().Clone()
501504
router := echo.New()
502-
router.GET("/set-hub", func(c echo.Context) error {
505+
router.GET("/set-hub", func(c *echo.Context) error {
503506
sentryecho.SetHubOnContext(c, hub)
504507
retrievedHub := sentryecho.GetHubFromContext(c)
505508
if retrievedHub == nil {
@@ -544,14 +547,14 @@ func TestGetSpanFromContext(t *testing.T) {
544547
}
545548

546549
router := echo.New()
547-
router.GET("/no-span", func(c echo.Context) error {
550+
router.GET("/no-span", func(c *echo.Context) error {
548551
span := sentryecho.GetSpanFromContext(c)
549552
if span != nil {
550553
t.Error("expecting span to be nil")
551554
}
552555
return c.NoContent(http.StatusOK)
553556
})
554-
router.GET("/with-span", func(c echo.Context) error {
557+
router.GET("/with-span", func(c *echo.Context) error {
555558
span := sentryecho.GetSpanFromContext(c)
556559
if span == nil {
557560
t.Error("expecting span to not be nil")
@@ -598,3 +601,47 @@ func TestGetSpanFromContext(t *testing.T) {
598601
}
599602
}
600603
}
604+
605+
func TestUnwrapResponseError(t *testing.T) {
606+
ch := make(chan *sentry.Event, 1)
607+
if err := sentry.Init(sentry.ClientOptions{
608+
EnableTracing: true,
609+
TracesSampleRate: 1.0,
610+
BeforeSendTransaction: func(e *sentry.Event, _ *sentry.EventHint) *sentry.Event {
611+
ch <- e
612+
return e
613+
},
614+
}); err != nil {
615+
t.Fatal(err)
616+
}
617+
618+
router := echo.New()
619+
router.Use(sentryecho.New(sentryecho.Options{}))
620+
// ResponseWriter that does not implement Unwrap(), so echo.UnwrapResponse() returns an error.
621+
router.GET("/unwrap-err", func(c *echo.Context) error {
622+
c.SetResponse(&struct{ http.ResponseWriter }{c.Response()})
623+
return c.JSON(http.StatusOK, "ok")
624+
})
625+
626+
srv := httptest.NewServer(router)
627+
defer srv.Close()
628+
629+
res, err := srv.Client().Get(srv.URL + "/unwrap-err")
630+
if err != nil {
631+
t.Fatal(err)
632+
}
633+
res.Body.Close()
634+
if res.StatusCode != http.StatusOK {
635+
t.Errorf("expected 200, got %d", res.StatusCode)
636+
}
637+
638+
if !sentry.Flush(testutils.FlushTimeout()) {
639+
t.Fatal("Flush timed out")
640+
}
641+
tx := <-ch
642+
643+
code := tx.Contexts["trace"]["data"].(map[string]interface{})["http.response.status_code"].(int)
644+
if code != 0 {
645+
t.Errorf("when UnwrapResponse fails, expected status_code 0, got %d", code)
646+
}
647+
}

0 commit comments

Comments
 (0)