diff --git a/binder_generic.go b/binder_generic.go index 0c0eb9089..62e1da512 100644 --- a/binder_generic.go +++ b/binder_generic.go @@ -49,10 +49,11 @@ const ( // It returns the typed value and an error if binding fails. Returns ErrNonExistentKey if parameter not found. // // Empty String Handling: -// If the parameter exists but has an empty value, the zero value of type T is returned -// with no error. For example, a path parameter with value "" returns (0, nil) for int types. -// This differs from standard library behavior where parsing empty strings returns errors. -// To treat empty values as errors, validate the result separately or check the raw value. +// +// If the parameter exists but has an empty value, the zero value of type T is returned +// with no error. For example, a path parameter with value "" returns (0, nil) for int types. +// This differs from standard library behavior where parsing empty strings returns errors. +// To treat empty values as errors, validate the result separately or check the raw value. // // See ParseValue for supported types and options func PathParam[T any](c *Context, paramName string, opts ...any) (T, error) { @@ -74,10 +75,11 @@ func PathParam[T any](c *Context, paramName string, opts ...any) (T, error) { // Returns an error only if parsing fails (e.g., "abc" for int type). // // Example: -// id, err := echo.PathParamOr[int](c, "id", 0) -// // If "id" is missing: returns (0, nil) -// // If "id" is "123": returns (123, nil) -// // If "id" is "abc": returns (0, BindingError) +// +// id, err := echo.PathParamOr[int](c, "id", 0) +// // If "id" is missing: returns (0, nil) +// // If "id" is "123": returns (123, nil) +// // If "id" is "abc": returns (0, BindingError) // // See ParseValue for supported types and options func PathParamOr[T any](c *Context, paramName string, defaultValue T, opts ...any) (T, error) { @@ -97,10 +99,11 @@ func PathParamOr[T any](c *Context, paramName string, defaultValue T, opts ...an // It returns the typed value and an error if binding fails. Returns ErrNonExistentKey if parameter not found. // // Empty String Handling: -// If the parameter exists but has an empty value (?key=), the zero value of type T is returned -// with no error. For example, "?count=" returns (0, nil) for int types. -// This differs from standard library behavior where parsing empty strings returns errors. -// To treat empty values as errors, validate the result separately or check the raw value. +// +// If the parameter exists but has an empty value (?key=), the zero value of type T is returned +// with no error. For example, "?count=" returns (0, nil) for int types. +// This differs from standard library behavior where parsing empty strings returns errors. +// To treat empty values as errors, validate the result separately or check the raw value. // // Behavior Summary: // - Missing key (?other=value): returns (zero, ErrNonExistentKey) @@ -131,10 +134,11 @@ func QueryParam[T any](c *Context, key string, opts ...any) (T, error) { // Returns an error only if parsing fails (e.g., "abc" for int type). // // Example: -// page, err := echo.QueryParamOr[int](c, "page", 1) -// // If "page" is missing: returns (1, nil) -// // If "page" is "5": returns (5, nil) -// // If "page" is "abc": returns (1, BindingError) +// +// page, err := echo.QueryParamOr[int](c, "page", 1) +// // If "page" is missing: returns (1, nil) +// // If "page" is "5": returns (5, nil) +// // If "page" is "abc": returns (1, BindingError) // // See ParseValue for supported types and options func QueryParamOr[T any](c *Context, key string, defaultValue T, opts ...any) (T, error) { @@ -175,10 +179,11 @@ func QueryParams[T any](c *Context, key string, opts ...any) ([]T, error) { // Returns an error only if parsing any value fails. // // Example: -// ids, err := echo.QueryParamsOr[int](c, "ids", []int{}) -// // If "ids" is missing: returns ([], nil) -// // If "ids" is "1&ids=2": returns ([1, 2], nil) -// // If "ids" contains "abc": returns ([], BindingError) +// +// ids, err := echo.QueryParamsOr[int](c, "ids", []int{}) +// // If "ids" is missing: returns ([], nil) +// // If "ids" is "1&ids=2": returns ([1, 2], nil) +// // If "ids" contains "abc": returns ([], BindingError) // // See ParseValues for supported types and options func QueryParamsOr[T any](c *Context, key string, defaultValue []T, opts ...any) ([]T, error) { @@ -198,10 +203,11 @@ func QueryParamsOr[T any](c *Context, key string, defaultValue []T, opts ...any) // It returns the typed value and an error if binding fails. Returns ErrNonExistentKey if parameter not found. // // Empty String Handling: -// If the form field exists but has an empty value, the zero value of type T is returned -// with no error. For example, an empty form field returns (0, nil) for int types. -// This differs from standard library behavior where parsing empty strings returns errors. -// To treat empty values as errors, validate the result separately or check the raw value. +// +// If the form field exists but has an empty value, the zero value of type T is returned +// with no error. For example, an empty form field returns (0, nil) for int types. +// This differs from standard library behavior where parsing empty strings returns errors. +// To treat empty values as errors, validate the result separately or check the raw value. // // See ParseValue for supported types and options func FormValue[T any](c *Context, key string, opts ...any) (T, error) { @@ -232,10 +238,11 @@ func FormValue[T any](c *Context, key string, opts ...any) (T, error) { // Returns an error only if parsing fails or form parsing errors occur. // // Example: -// limit, err := echo.FormValueOr[int](c, "limit", 100) -// // If "limit" is missing: returns (100, nil) -// // If "limit" is "50": returns (50, nil) -// // If "limit" is "abc": returns (100, BindingError) +// +// limit, err := echo.FormValueOr[int](c, "limit", 100) +// // If "limit" is missing: returns (100, nil) +// // If "limit" is "50": returns (50, nil) +// // If "limit" is "abc": returns (100, BindingError) // // See ParseValue for supported types and options func FormValueOr[T any](c *Context, key string, defaultValue T, opts ...any) (T, error) { @@ -284,9 +291,10 @@ func FormValues[T any](c *Context, key string, opts ...any) ([]T, error) { // Returns an error only if parsing any value fails or form parsing errors occur. // // Example: -// tags, err := echo.FormValuesOr[string](c, "tags", []string{}) -// // If "tags" is missing: returns ([], nil) -// // If form parsing fails: returns (nil, error) +// +// tags, err := echo.FormValuesOr[string](c, "tags", []string{}) +// // If "tags" is missing: returns ([], nil) +// // If form parsing fails: returns (nil, error) // // See ParseValues for supported types and options func FormValuesOr[T any](c *Context, key string, defaultValue []T, opts ...any) ([]T, error) { diff --git a/context.go b/context.go index f91ea7a60..0000fbc28 100644 --- a/context.go +++ b/context.go @@ -146,6 +146,16 @@ func (c *Context) SetResponse(r http.ResponseWriter) { c.response = r } +// IsNotFound returns true if the route is not found otherwise false +func (c *Context) IsNotFound() bool { + return c.route == notFoundRouteInfo +} + +// IsMethodAllowed returns true if the method is not allowed otherwise false +func (c *Context) IsMethodNotAllowed() bool { + return c.route == methodNotAllowedRouteInfo +} + // IsTLS returns true if HTTP connection is TLS otherwise false. func (c *Context) IsTLS() bool { return c.request.TLS != nil diff --git a/echo.go b/echo.go index 4855e8429..39497ff60 100644 --- a/echo.go +++ b/echo.go @@ -92,6 +92,9 @@ type Echo struct { // formParseMaxMemory is passed to Context for multipart form parsing (See http.Request.ParseMultipartForm) formParseMaxMemory int64 + + //SkipMiddlewareOnNotFound is a flag, when route is not found, echo prevents wasting compute resources + SkipMiddlewareOnNotFound bool } // JSONSerializer is the interface that encodes and decodes JSON to and from interfaces. @@ -684,13 +687,38 @@ func (e *Echo) serveHTTP(w http.ResponseWriter, r *http.Request) { defer e.contextPool.Put(c) c.Reset(r, w) + var h HandlerFunc if e.premiddleware == nil { - h = applyMiddleware(e.router.Route(c), e.middleware...) + // --- THE FIX START --- + // Perform routing immediately + h = e.router.Route(c) + + // Check if the global middleware chain for 404/405 should be skipped + // We use c.InternalRouteInfo() to check if it's a "virtual" route (404/405) + if e.SkipMiddlewareOnNotFound && (c.IsNotFound() || c.IsMethodNotAllowed()) { + // Execute the 404/405 handler directly, skipping e.middleware... + if err := h(c); err != nil { + e.HTTPErrorHandler(c, err) + } + return + } + // Otherwise apply the middleware chain normally + h = applyMiddleware(h, e.middleware...) + // --- THE FIX END --- } else { + // If premiddleware exists, we must wrap the routing logic inside a function + // because premiddleware might change the URL (Rewrite/TrailingSlash) h = func(cc *Context) error { - h1 := applyMiddleware(e.router.Route(cc), e.middleware...) + rh := e.router.Route(cc) + + // Apply logic inside the premiddleware-triggered chain + if e.SkipMiddlewareOnNotFound && (cc.IsNotFound() || cc.IsMethodNotAllowed()) { + return rh(cc) + } + + h1 := applyMiddleware(rh, e.middleware...) return h1(cc) } h = applyMiddleware(h, e.premiddleware...) diff --git a/middleware_skip_test.go b/middleware_skip_test.go new file mode 100644 index 000000000..87865511e --- /dev/null +++ b/middleware_skip_test.go @@ -0,0 +1,48 @@ +package echo + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +// BenchmarkMiddleware404 compares performance when middleware is skipped on 404 +func BenchmarkMiddleware404(b *testing.B) { + e := New() + + // Simulate a "Heavy" middleware (e.g., Auth or Logging) + e.Use(func(next HandlerFunc) HandlerFunc { + return func(c *Context) error { + c.Set("user_id", "12345") // Simulate some work + return next(c) + } + }) + + e.GET("/exists", func(c *Context) error { + return c.NoContent(http.StatusOK) + }) + + // Case 1: Standard behavior (Middleware runs on 404) + b.Run("Normal_404", func(b *testing.B) { + e.SkipMiddlewareOnNotFound = false + req := httptest.NewRequest(http.MethodGet, "/not-found", nil) + w := httptest.NewRecorder() + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + e.ServeHTTP(w, req) + } + }) + + // Case 2: Optimized behavior (Middleware skipped on 404) + b.Run("Optimized_404", func(b *testing.B) { + e.SkipMiddlewareOnNotFound = true + req := httptest.NewRequest(http.MethodGet, "/not-found", nil) + w := httptest.NewRecorder() + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + e.ServeHTTP(w, req) + } + }) +}