mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-10-13 15:53:17 +01:00
chore(deps)!: migrate from echo v4 to echo v5
This commit is contained in:
@@ -11,7 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v5"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v5"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
||||
@@ -95,7 +95,7 @@ func webhookMiddleware(w *Webhook) api.Middleware {
|
||||
})
|
||||
}
|
||||
|
||||
return func(c echo.Context) error {
|
||||
return func(c *echo.Context) error {
|
||||
webhookUrl := c.Request().Header.Get("Gotenberg-Webhook-Url")
|
||||
if webhookUrl == "" {
|
||||
// No webhook URL, call the next middleware in the chain.
|
||||
@@ -208,7 +208,7 @@ func webhookMiddleware(w *Webhook) api.Middleware {
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve values from echo.Context before it gets recycled.
|
||||
// Retrieve values from [echo.Context] before it gets recycled.
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1000.
|
||||
startTime := c.Get("startTime").(time.Time)
|
||||
correlationIdHeader := c.Get("correlationIdHeader").(string)
|
||||
@@ -323,16 +323,16 @@ func webhookMiddleware(w *Webhook) api.Middleware {
|
||||
// As a webhook URL has been given, we handle the request in a
|
||||
// goroutine and return immediately.
|
||||
//
|
||||
// Echo returns the echo.Context back to its sync.Pool as
|
||||
// Echo returns the [echo.Context] back to its sync.Pool as
|
||||
// soon as this synchronous handler returns ErrAsyncProcess.
|
||||
// A concurrent request can then claim the recycled context
|
||||
// and c.Reset() wipes the shared store, which would cause
|
||||
// any c.Get("...").(T) assertion downstream of the webhook
|
||||
// goroutine to panic on a nil value and crash the process.
|
||||
// Snapshot the keys downstream reads onto a detached
|
||||
// wrapper before spawning the goroutine so pool reuse
|
||||
// context before spawning the goroutine so pool reuse
|
||||
// cannot reach into our async work.
|
||||
detached := newPoolSafeContext(c, "logger", "context", "correlationId", "correlationIdHeader", "startTime")
|
||||
detached := newDetachedContext(c, "logger", "context", "correlationId", "correlationIdHeader", "startTime")
|
||||
|
||||
w.asyncCount.Add(1)
|
||||
go func() {
|
||||
|
||||
@@ -1,58 +1,34 @@
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v5"
|
||||
)
|
||||
|
||||
// poolSafeContext wraps an [echo.Context] and keeps a private snapshot of
|
||||
// the values that downstream middleware and route handlers read from the
|
||||
// store. Echo returns an [echo.Context] to its sync.Pool as soon as the
|
||||
// synchronous handler returns, including when the webhook middleware
|
||||
// returns [api.ErrAsyncProcess]. A concurrent request can then claim the
|
||||
// recycled context and c.Reset() wipes the shared store out from under
|
||||
// the webhook goroutine, which causes any
|
||||
// `c.Get("logger").(*slog.Logger)`-style assertion further down the
|
||||
// chain to panic on a nil value.
|
||||
// newDetachedContext returns an [echo.Context] carrying a snapshot of the given
|
||||
// keys, detached from Echo's context pool.
|
||||
//
|
||||
// Wrapping c before handing it to the goroutine insulates the async work
|
||||
// from pool reuse: Get/Set read and write the private store while every
|
||||
// other [echo.Context] method delegates to the embedded context for
|
||||
// anything the downstream might still need.
|
||||
type poolSafeContext struct {
|
||||
echo.Context
|
||||
mu sync.RWMutex
|
||||
store map[string]any
|
||||
}
|
||||
|
||||
// newPoolSafeContext snapshots the given keys from c into a detached
|
||||
// store and returns a wrapper whose Get/Set operate on that store
|
||||
// exclusively. Keys absent from c are omitted; the wrapper still
|
||||
// Echo returns an [echo.Context] to its sync.Pool as soon as the synchronous
|
||||
// handler returns, including when the webhook middleware returns
|
||||
// [api.ErrAsyncProcess]. A concurrent request can then claim the recycled
|
||||
// context and c.Reset() wipes the shared store out from under the webhook
|
||||
// goroutine, which causes any `c.Get("logger").(*slog.Logger)`-style assertion
|
||||
// further down the chain to panic on a nil value.
|
||||
//
|
||||
// [echo.NewContext] allocates outside the pool, so recycling cannot reach the
|
||||
// returned context. Keys absent from c are omitted; the returned context still
|
||||
// returns nil for them, matching [echo.Context.Get] behavior.
|
||||
func newPoolSafeContext(c echo.Context, keys ...string) *poolSafeContext {
|
||||
store := make(map[string]any, len(keys))
|
||||
//
|
||||
// Only the asynchronous path uses this. Nothing downstream of the webhook
|
||||
// middleware writes to the response: contextMiddleware sits upstream and has
|
||||
// already answered 204 by the time the goroutine runs.
|
||||
func newDetachedContext(c *echo.Context, keys ...string) *echo.Context {
|
||||
detached := echo.NewContext(c.Request(), c.Response(), c.Echo())
|
||||
|
||||
for _, key := range keys {
|
||||
if v := c.Get(key); v != nil {
|
||||
store[key] = v
|
||||
detached.Set(key, v)
|
||||
}
|
||||
}
|
||||
return &poolSafeContext{Context: c, store: store}
|
||||
}
|
||||
|
||||
// Get returns the value stored in the detached store, not the embedded
|
||||
// context's pooled store.
|
||||
func (p *poolSafeContext) Get(key string) any {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return p.store[key]
|
||||
}
|
||||
|
||||
// Set writes to the detached store, not the embedded context's pooled
|
||||
// store. This prevents downstream middleware writes from leaking into a
|
||||
// later request that claims the same pooled context.
|
||||
func (p *poolSafeContext) Set(key string, val any) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.store[key] = val
|
||||
return detached
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v5"
|
||||
)
|
||||
|
||||
func TestPoolSafeContext_SurvivesUnderlyingReset(t *testing.T) {
|
||||
func TestNewDetachedContext_SurvivesUnderlyingReset(t *testing.T) {
|
||||
e := echo.New()
|
||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
@@ -19,10 +19,10 @@ func TestPoolSafeContext_SurvivesUnderlyingReset(t *testing.T) {
|
||||
c.Set("logger", logger)
|
||||
c.Set("correlationId", "abc-123")
|
||||
|
||||
detached := newPoolSafeContext(c, "logger", "correlationId", "missing")
|
||||
detached := newDetachedContext(c, "logger", "correlationId", "missing")
|
||||
|
||||
// Simulate Echo recycling c for a concurrent request. Reset wipes the
|
||||
// shared store, which is exactly the crash scenario the wrapper
|
||||
// Simulate Echo recycling c for a concurrent request. Reset clears the
|
||||
// pooled store, which is exactly the crash scenario the detached context
|
||||
// guards against.
|
||||
c.Reset(httptest.NewRequest(http.MethodGet, "/", nil), httptest.NewRecorder())
|
||||
|
||||
@@ -38,17 +38,17 @@ func TestPoolSafeContext_SurvivesUnderlyingReset(t *testing.T) {
|
||||
|
||||
// Underlying c must remain clean.
|
||||
if c.Get("logger") != nil {
|
||||
t.Fatalf("underlying c.Get(\"logger\") leaked wrapper state after reset")
|
||||
t.Fatalf("underlying c.Get(\"logger\") leaked detached state after reset")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolSafeContext_SetDoesNotTouchUnderlying(t *testing.T) {
|
||||
func TestNewDetachedContext_SetDoesNotTouchUnderlying(t *testing.T) {
|
||||
e := echo.New()
|
||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
c := e.NewContext(req, rec)
|
||||
|
||||
detached := newPoolSafeContext(c)
|
||||
detached := newDetachedContext(c)
|
||||
detached.Set("foo", "bar")
|
||||
|
||||
if got, _ := detached.Get("foo").(string); got != "bar" {
|
||||
|
||||
Reference in New Issue
Block a user