Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
package main

import (
"github.com/labstack/echo/v5"
"github.com/ad3n/echo/v5"
"net/http"
"net/http/httptest"
"testing"
Expand Down
2 changes: 1 addition & 1 deletion API_CHANGES_V5.md
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ Top-level binding functions that work with `*Context`.
### 12. **New echotest Package**

```go
package echotest // import "github.com/labstack/echo/v5/echotest"
package echotest // import "github.com/ad3n/echo/v5/echotest"

func LoadBytes(t *testing.T, name string, opts ...loadBytesOpts) []byte
func TrimNewlineEnd(bytes []byte) []byte
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## About This Project

Echo is a high performance, minimalist Go web framework. This is the main repository for Echo v5, which is available as a Go module at `github.com/labstack/echo/v5`.
Echo is a high performance, minimalist Go web framework. This is the main repository for Echo v5, which is available as a Go module at `github.com/ad3n/echo/v5`.

## Development Commands

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
PKG := "github.com/labstack/echo"
PKG := "github.com/ad3n/echo/v5"
PKG_LIST := $(shell go list ${PKG}/...)

.DEFAULT_GOAL := check
Expand Down
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[![Latest release](https://img.shields.io/github/v/release/labstack/echo?style=flat-square&label=release&color=00afd1)](https://github.com/labstack/echo/releases)
[![Last commit](https://img.shields.io/github/last-commit/labstack/echo/master?style=flat-square)](https://github.com/labstack/echo/commits/master)
[![Sourcegraph](https://sourcegraph.com/github.com/labstack/echo/-/badge.svg?style=flat-square)](https://sourcegraph.com/github.com/labstack/echo?badge)
[![GoDoc](https://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://pkg.go.dev/github.com/labstack/echo/v5)
[![GoDoc](https://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://pkg.go.dev/github.com/ad3n/echo/v5)
[![Go Report Card](https://goreportcard.com/badge/github.com/labstack/echo?style=flat-square)](https://goreportcard.com/report/github.com/labstack/echo)
[![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/labstack/echo/echo.yml?style=flat-square)](https://github.com/labstack/echo/actions)
[![Codecov](https://img.shields.io/codecov/c/github/labstack/echo.svg?style=flat-square)](https://codecov.io/gh/labstack/echo)
Expand Down Expand Up @@ -62,8 +62,7 @@ See [ROADMAP.md](./ROADMAP.md) for where Echo is heading and the version support
### Installation

```sh
// go get github.com/labstack/echo/{version}
go get github.com/labstack/echo/v5
go get github.com/ad3n/echo/v5
```

Latest version of Echo supports last four Go major [releases](https://go.dev/doc/devel/release) and might work with
Expand All @@ -75,8 +74,8 @@ older versions.
package main

import (
"github.com/labstack/echo/v5"
"github.com/labstack/echo/v5/middleware"
"github.com/ad3n/echo/v5"
"github.com/ad3n/echo/v5/middleware"
"log/slog"
"net/http"
)
Expand Down
157 changes: 157 additions & 0 deletions allocation_ownership_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package echo

import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestContextReleaseDropsRequestReferences(t *testing.T) {
e := New()
req := httptest.NewRequest(http.MethodGet, "/?tag=one&tag=two", nil)
req.Header.Set("X-Test", "original")
c := e.NewContext(req, httptest.NewRecorder())
c.Set("payload", req)
c.SetLogger(e.Logger.With("request", req))
c.SetPathValues(PathValues{{Name: "first", Value: "secret"}, {Name: "second", Value: "tail"}})
c.SetPathValues(PathValues{{Name: "first", Value: "short"}})
c.orgResponse.Before(func() { _ = req.URL })
c.orgResponse.After(func() { _ = req.Header })
c.dsw.ResponseWriter = c.Response()
c.route = &RouteInfo{Path: "/:first"}
c.handler = func(*Context) error { return nil }
c.path = "/:first"
query := c.QueryParams()
var bound map[string][]string
require.NoError(t, BindQueryParams(c, &bound))

e.ReleaseContext(c)

assert.Nil(t, c.request)
assert.Nil(t, c.query)
assert.Empty(t, c.store)
assert.Nil(t, c.route)
assert.Nil(t, c.handler)
assert.Empty(t, c.path)
assert.Same(t, e.Logger, c.logger)
assert.Nil(t, c.orgResponse.ResponseWriter)
assert.Same(t, c.orgResponse, c.response)
assert.Nil(t, c.dsw.ResponseWriter)
assert.Empty(t, c.PathValues())
for _, value := range (*c.pathValues)[:cap(*c.pathValues)] {
assert.Equal(t, PathValue{}, value)
}

for _, hooks := range [][]func(){c.orgResponse.beforeFuncs, c.orgResponse.afterFuncs} {
assert.Empty(t, hooks)
for _, hook := range hooks[:cap(hooks)] {
assert.Nil(t, hook)
}
}

assert.Equal(t, url.Values{"tag": {"one", "two"}}, query)
assert.Equal(t, []string{"one", "two"}, bound["tag"])
assert.Equal(t, "original", req.Header.Get("X-Test"))
}

func TestContextResetDropsOversizedStore(t *testing.T) {
c := New().NewContext(nil, nil)
for i := 0; i <= maxPooledContextStoreEntries; i++ {
c.Set(fmt.Sprint(i), i)
}

c.Reset(nil, nil)
require.Nil(t, c.store)
c.Set("next", "value")
assert.Equal(t, "value", c.Get("next"))
}

func TestContextResetWithoutEcho(t *testing.T) {
c := NewContext(nil, nil)
c.Set("previous", "value")
assert.NotPanics(t, func() { c.Reset(nil, nil) })
assert.Empty(t, c.store)
assert.NotNil(t, c.Logger())
}

func TestResponseHooksReuseAndRelease(t *testing.T) {
r := NewResponse(httptest.NewRecorder(), New().Logger)
var before, after int
r.Before(func() { before++ })
r.After(func() { after++ })
beforeSlot, afterSlot := &r.beforeFuncs[0], &r.afterFuncs[0]
_, err := r.Write([]byte("one"))
require.NoError(t, err)
r.reset(httptest.NewRecorder())
require.Nil(t, *beforeSlot)
require.Nil(t, *afterSlot)
r.Before(func() { before += 10 })
r.After(func() { after += 10 })
assert.Equal(t, beforeSlot, &r.beforeFuncs[0])
assert.Equal(t, afterSlot, &r.afterFuncs[0])
_, err = r.Write([]byte("two"))
require.NoError(t, err)
assert.Equal(t, 11, before)
assert.Equal(t, 11, after)

for range maxPooledResponseHooks + 1 {
r.Before(func() {})
r.After(func() {})
}

r.reset(nil)
assert.Nil(t, r.beforeFuncs)
assert.Nil(t, r.afterFuncs)
}

func TestServeHTTPReleasesContextOnEveryExit(t *testing.T) {
for _, outcome := range []string{"success", "error", "panic"} {
t.Run(outcome, func(t *testing.T) {
e := New()
var captured *Context
e.GET("/", func(c *Context) error {
captured = c
c.Set("request", c.Request())
switch outcome {
case "error":
return ErrBadRequest
case "panic":
panic("test")
}

return c.NoContent(http.StatusNoContent)
})
serve := func() {
e.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil))
}
if outcome == "panic" {
assert.Panics(t, serve)
}

if outcome != "panic" {
assert.NotPanics(t, serve)
}

require.NotNil(t, captured)
assert.Nil(t, captured.request)
assert.Empty(t, captured.store)
assert.Nil(t, captured.orgResponse.ResponseWriter)
})
}
}

func BenchmarkServeHTTP_ResponseHooks(b *testing.B) {
e := New()
hook := func() {}
e.GET("/", func(c *Context) error {
c.orgResponse.Before(hook)
c.orgResponse.After(hook)
return c.NoContent(http.StatusNoContent)
})
benchServe(b, e, httptest.NewRequest(http.MethodGet, "/", nil))
}
2 changes: 1 addition & 1 deletion binder_external_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"net/http"
"net/http/httptest"

"github.com/labstack/echo/v5"
"github.com/ad3n/echo/v5"
)

func ExampleValueBinder_BindErrors() {
Expand Down
56 changes: 23 additions & 33 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,35 +58,27 @@ const (
indexPage = "index.html"
)

// Context represents the context of the current HTTP request. It holds request and
// response objects, path, path parameters, data and registered handler.
type Context struct {
response http.ResponseWriter
request *http.Request
orgResponse *Response
response http.ResponseWriter
query url.Values

// formParseMaxMemory is used for http.Request.ParseMultipartForm
formParseMaxMemory int64

route *RouteInfo
pathValues *PathValues

// handler is the route handler resolved during routing. It is invoked by the terminal of the global
// middleware chain (see Echo.buildRouterChains) so that the chain can be compiled once and reused.
handler HandlerFunc

// dsw is reused by json() so that each JSON response does not heap-allocate a delayedStatusWriter.
// It lives on the pooled Context; &c.dsw is a stable, allocation-free pointer. Only json() may point
// the response at &c.dsw, and only via the nested-call guard there — aliasing it to itself (wrapping
// &c.dsw around &c.dsw) would make the response writer reference itself.
dsw delayedStatusWriter

store map[string]any
echo *Echo
logger *slog.Logger

path string

dsw delayedStatusWriter

formParseMaxMemory int64

lock sync.RWMutex
}

Expand Down Expand Up @@ -134,27 +126,32 @@ func newContext(r *http.Request, w http.ResponseWriter, e *Echo) *Context {
return c
}

// Reset resets the context after request completes. It must be called along
// with `Echo#AcquireContext()` and `Echo#ReleaseContext()`.
// See `Echo#ServeHTTP()`
func (c *Context) Reset(r *http.Request, w http.ResponseWriter) {
c.request = r
c.orgResponse.reset(w)
c.response = c.orgResponse
c.query = nil
// clear (rather than nil) keeps the map allocated on the pooled Context so that requests using Set
// do not allocate a fresh map each time. clear(nil) is a no-op.
clear(c.store)
c.logger = c.echo.Logger
if len(c.store) > maxPooledContextStoreEntries {
c.store = nil
}

clear(c.store)
c.route = nil
c.handler = nil
c.dsw = delayedStatusWriter{}
c.path = ""
// NOTE: empty by setting length to 0. PathValues has to have capacity of c.echo.contextPathParamAllocSize at all times
clear((*c.pathValues)[:cap(*c.pathValues)])
*c.pathValues = (*c.pathValues)[:0]
if c.echo == nil {
c.logger = slog.Default()
return
}

c.logger = c.echo.Logger
}

const maxPooledContextStoreEntries = 256

func (c *Context) writeContentType(value string) {
header := c.response.Header()
if header.Get(HeaderContentType) == "" {
Expand Down Expand Up @@ -487,25 +484,18 @@ func (c *Context) Validate(i any) error {
return c.echo.Validator.Validate(i)
}

// Render renders a template with data and sends a text/html response with status
// code. Renderer must be registered using `Echo.Renderer`.
func (c *Context) Render(code int, name string, data any) (err error) {
if c.echo.Renderer == nil {
return ErrRendererNotRegistered
}
// as Renderer.Render can fail, and in that case we need to delay sending status code to the client until
// (global) error handler decides the correct status code for the error to be sent to the client, so we need to write
// the rendered template to the buffer first.
//
// html.Template.ExecuteTemplate() documentations writes:
// > If an error occurs executing the template or writing its output,
// > execution stops, but partial results may already have been written to
// > the output writer.

buf := new(bytes.Buffer)
buf := renderBufPool.Get().(*bytes.Buffer)
defer releaseRenderBuffer(buf)

if err = c.echo.Renderer.Render(c, buf, name, data); err != nil {
return
}

return c.HTMLBlob(code, buf.Bytes())
}

Expand Down
15 changes: 8 additions & 7 deletions echo.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ Example:
"log/slog"
"net/http"

"github.com/labstack/echo/v5"
"github.com/labstack/echo/v5/middleware"
"github.com/ad3n/echo/v5"
"github.com/ad3n/echo/v5/middleware"
)

// Handler
Expand Down Expand Up @@ -185,7 +185,6 @@ const (
// RouteAny is a special method type that matches any HTTP method in request. Any has lower
// priority that other methods that have been registered with Router to that path.
RouteAny = "echo_route_any"

)

// Headers
Expand Down Expand Up @@ -793,9 +792,8 @@ func (e *Echo) AcquireContext() *Context {
return e.contextPool.Get().(*Context)
}

// ReleaseContext returns the `Context` instance back to the pool.
// You must call it after `AcquireContext()`.
func (e *Echo) ReleaseContext(c *Context) {
c.Reset(nil, nil)
e.contextPool.Put(c)
}

Expand All @@ -807,9 +805,12 @@ func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// serveHTTP implements `http.Handler` interface, which serves HTTP requests.
func (e *Echo) serveHTTP(w http.ResponseWriter, r *http.Request) {
c := e.contextPool.Get().(*Context)
defer e.contextPool.Put(c)
defer e.ReleaseContext(c)

c.Reset(r, w)
c.request = r
c.orgResponse.ResponseWriter = w
c.orgResponse.Status = http.StatusOK
c.logger = e.Logger

// The global (e.chain) and pre-middleware (e.preChain) chains are compiled once in buildRouterChains and
// reused here, so no middleware closures are allocated per request.
Expand Down
2 changes: 1 addition & 1 deletion echo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,7 @@ func TestEchoFile(t *testing.T) {
givenFile: "./go.mod",
whenPath: "/",
expectCode: http.StatusOK,
expectStartsWith: "module github.com/labstack/echo/v",
expectStartsWith: "module github.com/ad3n/echo/v5",
},
{
name: "nok file does not exist",
Expand Down
Loading