[
  {
    "path": ".editorconfig",
    "content": "# EditorConfig coding styles definitions. For more information about the\n# properties used in this file, please see the EditorConfig documentation:\n# http://editorconfig.org/\n\n# indicate this is the root of the project\nroot = true\n\n[*]\ncharset = utf-8\n\nend_of_line = LF\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\nindent_style = space\nindent_size = 2\n\n[Makefile]\nindent_style = tab\n\n[*.md]\ntrim_trailing_whitespace = false\n\n[*.go]\nindent_style = tab\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Automatically normalize line endings for all text-based files\n# http://git-scm.com/docs/gitattributes#_end_of_line_conversion\n* text=auto\n\n# For the following file types, normalize line endings to LF on checking and\n# prevent conversion to CRLF when they are checked out (this is required in\n# order to prevent newline related issues)\n.*      text eol=lf\n*.go    text eol=lf\n*.yml   text eol=lf\n*.html  text eol=lf\n*.css   text eol=lf\n*.js    text eol=lf\n*.json  text eol=lf\nLICENSE text eol=lf\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: [labstack]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "### Issue Description\n\n### Working code to debug\n\n```go\npackage main\n\nimport (\n  \"github.com/labstack/echo/v5\"\n  \"net/http\"\n  \"net/http/httptest\"\n  \"testing\"\n)\n\nfunc TestExample(t *testing.T) {\n  e := echo.New()\n\n  e.GET(\"/\", func(c *echo.Context) error {\n    return c.String(http.StatusOK, \"Hello, World!\")\n  })\n\n  req := httptest.NewRequest(http.MethodGet, \"/\", nil)\n  rec := httptest.NewRecorder()\n\n  e.ServeHTTP(rec, req)\n\n  if rec.Code != http.StatusOK {\n    t.Errorf(\"got %d, want %d\", rec.Code, http.StatusOK)\n  }\n}\n```\n\n### Version/commit\n"
  },
  {
    "path": ".github/stale.yml",
    "content": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 60\n# Number of days of inactivity before a stale issue is closed\ndaysUntilClose: 30\n# Issues with these labels will never be considered stale\nexemptLabels:\n  - pinned\n  - security\n  - bug\n  - enhancement\n# Label to use when marking an issue as stale\nstaleLabel: stale\n# Comment to post when marking an issue as stale. Set to `false` to disable\nmarkComment: >\n  This issue has been automatically marked as stale because it has not had\n  recent activity. It will be closed within a month if no further activity occurs.\n  Thank you for your contributions.\n# Comment to post when closing a stale issue. Set to `false` to disable\ncloseComment: false\n"
  },
  {
    "path": ".github/workflows/checks.yml",
    "content": "name: Run checks\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    branches:\n      - master\n  workflow_dispatch:\n\npermissions:\n  contents: read #  to fetch code (actions/checkout)\n\nenv:\n  # run static analysis only with the latest Go version\n  LATEST_GO_VERSION: \"1.26\"\n\njobs:\n  check:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout Code\n        uses: actions/checkout@v5\n\n      - name: Set up Go ${{ matrix.go }}\n        uses: actions/setup-go@v5\n        with:\n          go-version: ${{ env.LATEST_GO_VERSION }}\n          check-latest: true\n\n      - name: Run golint\n        run: |\n          go install golang.org/x/lint/golint@latest\n          golint -set_exit_status ./...\n\n      - name: Run staticcheck\n        run: |\n          go install honnef.co/go/tools/cmd/staticcheck@latest\n          staticcheck ./...\n\n      - name: Run govulncheck\n        run: |\n          go version\n          go install golang.org/x/vuln/cmd/govulncheck@latest\n          govulncheck ./...\n\n"
  },
  {
    "path": ".github/workflows/echo.yml",
    "content": "name: Run Tests\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    branches:\n      - master\n  workflow_dispatch:\n\npermissions:\n  contents: read #  to fetch code (actions/checkout)\n\nenv:\n  # run coverage and benchmarks only with the latest Go version\n  LATEST_GO_VERSION: \"1.26\"\n\njobs:\n  test:\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macos-latest, windows-latest]\n        # Each major Go release is supported until there are two newer major releases. https://golang.org/doc/devel/release.html#policy\n        # Echo tests with last four major releases (unless there are pressing vulnerabilities)\n        # As we depend on `golang.org/x/` libraries which only support the last 2 Go releases, we could have situations when\n        # we derive from the last four major releases promise.\n        go: [\"1.25\", \"1.26\"]\n    name: ${{ matrix.os }} @ Go ${{ matrix.go }}\n    runs-on: ${{ matrix.os }}\n    steps:\n      - name: Checkout Code\n        uses: actions/checkout@v5\n\n      - name: Set up Go ${{ matrix.go }}\n        uses: actions/setup-go@v5\n        with:\n          go-version: ${{ matrix.go }}\n\n      - name: Run Tests\n        run: go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./...\n\n      - name: Upload coverage to Codecov\n        if: success() && matrix.go == env.LATEST_GO_VERSION && matrix.os == 'ubuntu-latest'\n        uses: codecov/codecov-action@v5\n        with:\n          token:\n          fail_ci_if_error: false\n\n  benchmark:\n    needs: test\n    name: Benchmark comparison\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout Code (Previous)\n        uses: actions/checkout@v5\n        with:\n          ref: ${{ github.base_ref }}\n          path: previous\n\n      - name: Checkout Code (New)\n        uses: actions/checkout@v5\n        with:\n          path: new\n\n      - name: Set up Go ${{ matrix.go }}\n        uses: actions/setup-go@v5\n        with:\n          go-version: ${{ env.LATEST_GO_VERSION }}\n\n      - name: Install Dependencies\n        run: go install golang.org/x/perf/cmd/benchstat@latest\n\n      - name: Run Benchmark (Previous)\n        run: |\n          cd previous\n          go test -run=\"-\" -bench=\".*\" -count=8 ./... > benchmark.txt\n\n      - name: Run Benchmark (New)\n        run: |\n          cd new\n          go test -run=\"-\" -bench=\".*\" -count=8 ./... > benchmark.txt\n\n      - name: Run Benchstat\n        run: |\n          benchstat previous/benchmark.txt new/benchmark.txt\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\ncoverage.txt\n_test\nvendor\n.idea\n*.iml\n*.out\n.vscode\n"
  },
  {
    "path": "API_CHANGES_V5.md",
    "content": "# Echo v5 Public API Changes\n\n**Comparison between `master` (v4.15.0) and `v5` (v5.0.0-alpha) branches**\n\nGenerated: 2026-01-01\n\n---\n\n## Executive Summary (by authors)\n\nEcho `v5` is maintenance release with **major breaking changes** \n- `Context` is now struct instead of interface and we can add method to it in the future in minor versions.\n- Adds new `Router` interface for possible new routing implementations.\n- Drops old logging interface and uses moderm `log/slog` instead.\n- Rearranges alot of methods/function signatures to make them more consistent.\n\n## Executive Summary (by LLMs)\n\nEcho v5 represents a **major breaking release** with significant architectural changes focused on:\n- **Updated generic helpers** to take `*Context` and rename form helpers to `FormValue*`\n- **Simplified API surface** by moving Context from interface to concrete struct\n- **Modern Go patterns** including slog.Logger integration\n- **Enhanced routing** with explicit RouteInfo and Routes types\n- **Better error handling** with simplified HTTPError\n- **New test helpers** via the `echotest` package\n\n### Change Statistics\n\n- **Major Breaking Changes**: 15+\n- **New Functions Added**: 30+\n- **Type Signature Changes**: 20+\n- **Removed APIs**: 10+\n- **New Packages Added**: 1 (`echotest`)\n- **Version Change**: `4.15.0` → `5.0.0-alpha`\n\n---\n\n## Critical Breaking Changes\n\n### 1. **Context: Interface → Concrete Struct**\n\n**v4 (master):**\n```go\ntype Context interface {\n    Request() *http.Request\n    // ... many methods\n}\n\n// Handler signature\nfunc handler(c echo.Context) error\n```\n\n**v5:**\n```go\ntype Context struct {\n    // Has unexported fields\n}\n\n// Handler signature - NOW USES POINTER!\nfunc handler(c *echo.Context) error\n```\n\n**Impact:** 🔴 **CRITICAL BREAKING CHANGE**\n- ALL handlers must change from `echo.Context` to `*echo.Context`\n- Context is now a concrete struct, not an interface\n- This affects every single handler function in user code\n\n**Migration:**\n```go\n// Before (v4)\nfunc MyHandler(c echo.Context) error {\n    return c.JSON(200, map[string]string{\"hello\": \"world\"})\n}\n\n// After (v5)\nfunc MyHandler(c *echo.Context) error {\n    return c.JSON(200, map[string]string{\"hello\": \"world\"})\n}\n```\n\n---\n\n### 2. **Logger: Custom Interface → slog.Logger**\n\n**v4:**\n```go\ntype Echo struct {\n    Logger Logger  // Custom interface with Print, Debug, Info, etc.\n}\n\ntype Logger interface {\n    Output() io.Writer\n    SetOutput(w io.Writer)\n    Prefix() string\n    // ... many custom methods\n}\n\n// Context returns Logger interface\nfunc (c Context) Logger() Logger\n```\n\n**v5:**\n```go\ntype Echo struct {\n    Logger *slog.Logger  // Standard library structured logger\n}\n\n// Context returns slog.Logger\nfunc (c *Context) Logger() *slog.Logger\nfunc (c *Context) SetLogger(logger *slog.Logger)\n```\n\n**Impact:** 🔴 **BREAKING CHANGE**\n- Must use Go's standard `log/slog` package\n- Logger interface completely removed\n- All logging code needs updating\n\n---\n\n### 3. **Router: From Router to DefaultRouter**\n\n**v4:**\n```go\ntype Router struct { ... }\n\nfunc NewRouter(e *Echo) *Router\nfunc (e *Echo) Router() *Router\n```\n\n**v5:**\n```go\ntype DefaultRouter struct { ... }\n\nfunc NewRouter(config RouterConfig) *DefaultRouter\nfunc (e *Echo) Router() Router  // Returns interface\n```\n\n**Changes:**\n- New `Router` interface introduced\n- `DefaultRouter` is the concrete implementation\n- `NewRouter()` now takes `RouterConfig` instead of `*Echo`\n- Added `NewConcurrentRouter(r Router) Router` for thread-safe routing\n\n---\n\n### 4. **Route Return Types Changed**\n\n**v4:**\n```go\nfunc (e *Echo) GET(path string, h HandlerFunc, m ...MiddlewareFunc) *Route\nfunc (e *Echo) Any(path string, h HandlerFunc, m ...MiddlewareFunc) []*Route\nfunc (e *Echo) Routes() []*Route\n```\n\n**v5:**\n```go\nfunc (e *Echo) GET(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo\nfunc (e *Echo) Any(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo\nfunc (e *Echo) Match(...) Routes  // Returns Routes type\nfunc (e *Echo) Router() Router  // Returns interface\n```\n\n**New Types:**\n```go\ntype RouteInfo struct {\n    Name       string\n    Method     string\n    Path       string\n    Parameters []string\n}\n\ntype Routes []RouteInfo  // Collection with helper methods\n```\n\n**Impact:** 🔴 **BREAKING CHANGE**\n- Route registration methods return `RouteInfo` instead of `*Route`\n- New `Routes` collection type with filtering methods\n- `Route` struct still exists but used differently\n\n---\n\n### 5. **Response Type Changed**\n\n**v4:**\n```go\nfunc (c Context) Response() *Response\ntype Response struct {\n    Writer http.ResponseWriter\n    Status int\n    Size   int64\n    Committed bool\n}\nfunc NewResponse(w http.ResponseWriter, e *Echo) *Response\n```\n\n**v5:**\n```go\nfunc (c *Context) Response() http.ResponseWriter\ntype Response struct {\n    http.ResponseWriter  // Embedded\n    Status    int\n    Size      int64\n    Committed bool\n}\nfunc NewResponse(w http.ResponseWriter, logger *slog.Logger) *Response\nfunc UnwrapResponse(rw http.ResponseWriter) (*Response, error)\n```\n\n**Changes:**\n- Context.Response() returns `http.ResponseWriter` instead of `*Response`\n- Response now embeds `http.ResponseWriter`\n- NewResponse takes `*slog.Logger` instead of `*Echo`\n- New `UnwrapResponse()` helper function\n\n---\n\n### 6. **HTTPError Simplified**\n\n**v4:**\n```go\ntype HTTPError struct {\n    Internal error\n    Message  interface{}  // Can be any type\n    Code     int\n}\n\nfunc NewHTTPError(code int, message ...interface{}) *HTTPError\n```\n\n**v5:**\n```go\ntype HTTPError struct {\n    Code    int\n    Message string  // Now string only\n    // Has unexported fields (Internal moved)\n}\n\nfunc NewHTTPError(code int, message string) *HTTPError\nfunc (he HTTPError) Wrap(err error) error  // New method\nfunc (he *HTTPError) StatusCode() int      // Implements HTTPStatusCoder\n```\n\n**Changes:**\n- `Message` field changed from `interface{}` to `string`\n- `NewHTTPError()` now takes `string` instead of `...interface{}`\n- Added `HTTPStatusCoder` interface and `StatusCode()` method\n- Added `Wrap(err error)` method for error wrapping\n\n---\n\n### 7. **HTTPErrorHandler Signature Changed**\n\n**v4:**\n```go\ntype HTTPErrorHandler func(err error, c Context)\n\nfunc (e *Echo) DefaultHTTPErrorHandler(err error, c Context)\n```\n\n**v5:**\n```go\ntype HTTPErrorHandler func(c *Context, err error)  // Parameters swapped!\n\nfunc DefaultHTTPErrorHandler(exposeError bool) HTTPErrorHandler  // Now a factory\n```\n\n**Impact:** 🔴 **BREAKING CHANGE**\n- Parameter order reversed: `(c *Context, err error)` instead of `(err error, c Context)`\n- DefaultHTTPErrorHandler is now a factory function that returns HTTPErrorHandler\n- Takes `exposeError` bool to control error message exposure\n\n---\n\n## Notable API Changes in v5\n\n### 1. **Generic Parameter Extraction Functions (Updated Signatures)**\n\nThese helpers keep the same generic API but now accept `*Context`, and the\nform helpers are renamed from `FormParam*` to `FormValue*`:\n\n```go\n// Query Parameters\nfunc QueryParam[T any](c *Context, key string, opts ...any) (T, error)\nfunc QueryParamOr[T any](c *Context, key string, defaultValue T, opts ...any) (T, error)\nfunc QueryParams[T any](c *Context, key string, opts ...any) ([]T, error)\nfunc QueryParamsOr[T any](c *Context, key string, defaultValue []T, opts ...any) ([]T, error)\n\n// Path Parameters\nfunc PathParam[T any](c *Context, paramName string, opts ...any) (T, error)\nfunc PathParamOr[T any](c *Context, paramName string, defaultValue T, opts ...any) (T, error)\n\n// Form Values\nfunc FormValue[T any](c *Context, key string, opts ...any) (T, error)\nfunc FormValueOr[T any](c *Context, key string, defaultValue T, opts ...any) (T, error)\nfunc FormValues[T any](c *Context, key string, opts ...any) ([]T, error)\nfunc FormValuesOr[T any](c *Context, key string, defaultValue []T, opts ...any) ([]T, error)\n\n// Generic Parsing\nfunc ParseValue[T any](value string, opts ...any) (T, error)\nfunc ParseValueOr[T any](value string, defaultValue T, opts ...any) (T, error)\nfunc ParseValues[T any](values []string, opts ...any) ([]T, error)\nfunc ParseValuesOr[T any](values []string, defaultValue []T, opts ...any) ([]T, error)\n```\n\n`FormParam*` was renamed to `FormValue*`; the rest keep names but now take `*Context`.\n\n**Supported Types:**\n- bool, string\n- int, int8, int16, int32, int64\n- uint, uint8, uint16, uint32, uint64\n- float32, float64\n- time.Time, time.Duration\n- BindUnmarshaler, encoding.TextUnmarshaler, json.Unmarshaler\n\n**Example Usage:**\n```go\n// v5 - Type-safe parameter binding\nid, err := echo.PathParam[int](c, \"id\")\npage, err := echo.QueryParamOr[int](c, \"page\", 1)\ntags, err := echo.QueryParams[string](c, \"tags\")\n```\n\n---\n\n### 2. **Context Store Helpers Now Use `*Context`**\n\n```go\n// Type-safe context value retrieval\nfunc ContextGet[T any](c *Context, key string) (T, error)\nfunc ContextGetOr[T any](c *Context, key string, defaultValue T) (T, error)\n\n// Error types\nvar ErrNonExistentKey = errors.New(\"non existent key\")\nvar ErrInvalidKeyType = errors.New(\"invalid key type\")\n```\n\nThese helpers existed in v4 with `Context` and now accept `*Context`.\n\n**Example:**\n```go\n// v5\nuser, err := echo.ContextGet[*User](c, \"user\")\ncount, err := echo.ContextGetOr[int](c, \"count\", 0)\n```\n\n---\n\n### 3. **PathValues Type**\n\nNew structured path parameter handling:\n\n```go\ntype PathValue struct {\n    Name  string\n    Value string\n}\n\ntype PathValues []PathValue\n\nfunc (p PathValues) Get(name string) (string, bool)\nfunc (p PathValues) GetOr(name string, defaultValue string) string\n\n// Context methods\nfunc (c *Context) PathValues() PathValues\nfunc (c *Context) SetPathValues(pathValues PathValues)\n```\n\n---\n\n### 4. **Time Parsing Options**\n\n```go\ntype TimeLayout string\n\nconst (\n    TimeLayoutUnixTime      = TimeLayout(\"UnixTime\")\n    TimeLayoutUnixTimeMilli = TimeLayout(\"UnixTimeMilli\")\n    TimeLayoutUnixTimeNano  = TimeLayout(\"UnixTimeNano\")\n)\n\ntype TimeOpts struct {\n    Layout          TimeLayout\n    ParseInLocation *time.Location\n    ToInLocation    *time.Location\n}\n```\n\n---\n\n### 5. **StartConfig for Server Configuration**\n\n```go\ntype StartConfig struct {\n    Address         string\n    HideBanner      bool\n    HidePort        bool\n    CertFilesystem  fs.FS\n    TLSConfig       *tls.Config\n    ListenerNetwork string\n    ListenerAddrFunc func(addr net.Addr)\n    GracefulTimeout  time.Duration\n    OnShutdownError  func(err error)\n    BeforeServeFunc  func(s *http.Server) error\n}\n\nfunc (sc StartConfig) Start(ctx context.Context, h http.Handler) error\nfunc (sc StartConfig) StartTLS(ctx context.Context, h http.Handler, certFile, keyFile any) error\n```\n\n**Example:**\n```go\n// v5 - More control over server startup\nctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\ndefer cancel()\n\nsc := echo.StartConfig{\n    Address:         \":8080\",\n    GracefulTimeout: 10 * time.Second,\n}\nif err := sc.Start(ctx, e); err != nil {\n    log.Fatal(err)\n}\n```\n\n---\n\n### 6. **Echo Config and Constructors**\n\n```go\ntype Config struct {\n    // Configuration for Echo (logger, binder, renderer, etc.)\n}\n\nfunc NewWithConfig(config Config) *Echo\n```\n\nThis adds a configuration struct for creating an `Echo` instance without\nmutating fields after `New()`.\n\n---\n\n### 7. **Enhanced Routing Features**\n\n```go\n// New route methods\nfunc (e *Echo) AddRoute(route Route) (RouteInfo, error)\nfunc (e *Echo) Middlewares() []MiddlewareFunc\nfunc (e *Echo) PreMiddlewares() []MiddlewareFunc\ntype AddRouteError struct{ ... }\n\n// Routes collection with filters\ntype Routes []RouteInfo\n\nfunc (r Routes) Clone() Routes\nfunc (r Routes) FilterByMethod(method string) (Routes, error)\nfunc (r Routes) FilterByName(name string) (Routes, error)\nfunc (r Routes) FilterByPath(path string) (Routes, error)\nfunc (r Routes) FindByMethodPath(method string, path string) (RouteInfo, error)\nfunc (r Routes) Reverse(routeName string, pathValues ...any) (string, error)\n\n// RouteInfo operations\nfunc (r RouteInfo) Clone() RouteInfo\nfunc (r RouteInfo) Reverse(pathValues ...any) string\n```\n\n---\n\n### 8. **Middleware Configuration Interface**\n\n```go\ntype MiddlewareConfigurator interface {\n    ToMiddleware() (MiddlewareFunc, error)\n}\n```\n\nAllows middleware configs to be converted to middleware without panicking.\n\n---\n\n### 9. **New Context Methods**\n\n```go\n// v5 additions\nfunc (c *Context) FileFS(file string, filesystem fs.FS) error\nfunc (c *Context) FormValueOr(name, defaultValue string) string\nfunc (c *Context) InitializeRoute(ri *RouteInfo, pathValues *PathValues)\nfunc (c *Context) ParamOr(name, defaultValue string) string\nfunc (c *Context) QueryParamOr(name, defaultValue string) string\nfunc (c *Context) RouteInfo() RouteInfo\n```\n\n---\n\n### 10. **Virtual Host Support**\n\n```go\nfunc NewVirtualHostHandler(vhosts map[string]*Echo) *Echo\n```\n\nCreates an Echo instance that routes requests to different Echo instances based on host.\n\n---\n\n### 11. **New Binder Functions**\n\n```go\nfunc BindBody(c *Context, target any) error\nfunc BindHeaders(c *Context, target any) error\nfunc BindPathValues(c *Context, target any) error  // Renamed from BindPathParams\nfunc BindQueryParams(c *Context, target any) error\n```\n\nTop-level binding functions that work with `*Context`.\n\n---\n\n### 12. **New echotest Package**\n\n```go\npackage echotest // import \"github.com/labstack/echo/v5/echotest\"\n\nfunc LoadBytes(t *testing.T, name string, opts ...loadBytesOpts) []byte\nfunc TrimNewlineEnd(bytes []byte) []byte\ntype ContextConfig struct{ ... }\ntype MultipartForm struct{ ... }\ntype MultipartFormFile struct{ ... }\n```\n\nHelpers for loading fixtures and constructing test contexts.\n\n---\n\n## Removed APIs in v5\n\n### Constants\n\n```go\n// v4 - Removed in v5\nconst CONNECT = http.MethodConnect  // Use http.MethodConnect directly\n```\n\n**Reason:** Deprecated in v4, use stdlib `http.Method*` constants instead.\n\n---\n\n### Constants Added in v5\n\n```go\n// v5 additions\nconst (\n    NotFoundRouteName = \"echo_route_not_found_name\"\n)\n```\n\n---\n\n### Error Variable Changes\n\n**v4 exports:**\n```go\nErrBadRequest\nErrInvalidKeyType\nErrNonExistentKey\n```\n\n**v5 exports:**\n```go\nErrBadRequest  // Now backed by unexported httpError type\nErrValidatorNotRegistered  // New\nErrInvalidKeyType\nErrNonExistentKey\n```\n\n**Reason:** v5 centralizes on `NewHTTPError(code, message)` rather than a broad set\nof predefined HTTP error variables.\n\n---\n\n### Functions Removed\n\n```go\n// v4 - Removed in v5\nfunc GetPath(r *http.Request) string  // Use r.URL.Path or r.URL.RawPath\n```\n\n### Variables Removed\n\n```go\n// v4 - Removed in v5\nvar MethodNotAllowedHandler = func(c Context) error { ... }\nvar NotFoundHandler = func(c Context) error { ... }\n```\n\n### Functions Renamed\n\n```go\n// v4\nfunc FormParam[T any](c Context, key string, opts ...any) (T, error)\nfunc FormParamOr[T any](c Context, key string, defaultValue T, opts ...any) (T, error)\nfunc FormParams[T any](c Context, key string, opts ...any) ([]T, error)\nfunc FormParamsOr[T any](c Context, key string, defaultValue []T, opts ...any) ([]T, error)\n\n// v5\nfunc FormValue[T any](c *Context, key string, opts ...any) (T, error)\nfunc FormValueOr[T any](c *Context, key string, defaultValue T, opts ...any) (T, error)\nfunc FormValues[T any](c *Context, key string, opts ...any) ([]T, error)\nfunc FormValuesOr[T any](c *Context, key string, defaultValue []T, opts ...any) ([]T, error)\n```\n\n---\n\n### Type Methods Removed/Changed\n\n**Echo struct changes:**\n```go\n// v4 fields removed in v5\ntype Echo struct {\n    StdLogger        *stdLog.Logger  // Removed\n    Server           *http.Server    // Removed (use StartConfig)\n    TLSServer        *http.Server    // Removed (use StartConfig)\n    Listener         net.Listener    // Removed (use StartConfig)\n    TLSListener      net.Listener    // Removed (use StartConfig)\n    AutoTLSManager   autocert.Manager // Removed\n    ListenerNetwork  string          // Removed\n    OnAddRouteHandler func(...)      // Changed to OnAddRoute\n    DisableHTTP2      bool           // Removed (use StartConfig)\n    Debug             bool           // Removed\n    HideBanner        bool           // Removed (use StartConfig)\n    HidePort          bool           // Removed (use StartConfig)\n}\n\n// v5 Echo struct (simplified)\ntype Echo struct {\n    Binder           Binder\n    Filesystem       fs.FS  // NEW\n    Renderer         Renderer\n    Validator        Validator\n    JSONSerializer   JSONSerializer\n    IPExtractor      IPExtractor\n    OnAddRoute       func(route Route) error  // Simplified\n    HTTPErrorHandler HTTPErrorHandler\n    Logger           *slog.Logger  // Changed from Logger interface\n}\n```\n\n---\n\n**Context interface → struct:**\n```go\n// v4\ntype Context interface {\n    // Had: SetResponse(*Response)\n    Response() *Response\n\n    // Had: ParamNames(), SetParamNames(), ParamValues(), SetParamValues()\n    // These are removed in v5 (use PathValues() instead)\n}\n\n// v5\ntype Context struct {\n    // Concrete struct with unexported fields\n}\n\nfunc (c *Context) Response() http.ResponseWriter  // Changed return type\nfunc (c *Context) PathValues() PathValues         // Replaces ParamNames/Values\n```\n\n---\n\n**Types removed:**\n```go\n// v4\ntype Map map[string]interface{}\n```\n\n**Group changes:**\n```go\n// v4\nfunc (g *Group) File(path, file string)  // No return value\nfunc (g *Group) Static(pathPrefix, fsRoot string)  // No return value\nfunc (g *Group) StaticFS(pathPrefix string, filesystem fs.FS)  // No return value\n\n// v5\nfunc (g *Group) File(path, file string, middleware ...MiddlewareFunc) RouteInfo\nfunc (g *Group) Static(pathPrefix, fsRoot string, middleware ...MiddlewareFunc) RouteInfo\nfunc (g *Group) StaticFS(pathPrefix string, filesystem fs.FS, middleware ...MiddlewareFunc) RouteInfo\n```\n\nNow return `RouteInfo` and accept middleware.\n\n---\n\n### Value Binder Factory Name Changes\n\n```go\n// v4\nfunc PathParamsBinder(c Context) *ValueBinder\nfunc QueryParamsBinder(c Context) *ValueBinder\nfunc FormFieldBinder(c Context) *ValueBinder\n\n// v5\nfunc PathValuesBinder(c *Context) *ValueBinder  // Renamed\nfunc QueryParamsBinder(c *Context) *ValueBinder\nfunc FormFieldBinder(c *Context) *ValueBinder\n```\n\n---\n\n## Type Signature Changes\n\n### Binder Interface\n\n```go\n// v4\ntype Binder interface {\n    Bind(i interface{}, c Context) error\n}\n\n// v5\ntype Binder interface {\n    Bind(c *Context, target any) error  // Parameters swapped!\n}\n```\n\n---\n\n### DefaultBinder Methods\n\n```go\n// v4\nfunc (b *DefaultBinder) Bind(i interface{}, c Context) error\nfunc (b *DefaultBinder) BindBody(c Context, i interface{}) error\nfunc (b *DefaultBinder) BindPathParams(c Context, i interface{}) error\n\n// v5\nfunc (b *DefaultBinder) Bind(c *Context, target any) error  // Swapped params\n// BindBody, BindPathParams, etc. are now top-level functions\n```\n\n---\n\n### JSONSerializer Interface\n\n```go\n// v4\ntype JSONSerializer interface {\n    Serialize(c Context, i interface{}, indent string) error\n    Deserialize(c Context, i interface{}) error\n}\n\n// v5\ntype JSONSerializer interface {\n    Serialize(c *Context, target any, indent string) error\n    Deserialize(c *Context, target any) error\n}\n```\n\n---\n\n### Renderer Interface\n\n```go\n// v4\ntype Renderer interface {\n    Render(io.Writer, string, interface{}, Context) error\n}\n\n// v5\ntype Renderer interface {\n    Render(c *Context, w io.Writer, templateName string, data any) error\n}\n```\n\nParameters reordered with Context first.\n\n---\n\n### NewBindingError\n\n```go\n// v4\nfunc NewBindingError(sourceParam string, values []string, message interface{}, internalError error) error\n\n// v5\nfunc NewBindingError(sourceParam string, values []string, message string, err error) error\n```\n\nMessage parameter changed from `interface{}` to `string`.\n\n---\n\n### HandlerName\n\n```go\n// v5 only\nfunc HandlerName(h HandlerFunc) string\n```\n\nNew utility function to get handler function name.\n\n---\n\n## Middleware Package Changes\n\n### Signature and Type Updates\n\n```go\n// CORS now accepts optional allow-origins\nfunc CORS(allowOrigins ...string) echo.MiddlewareFunc\n\n// BodyLimit now accepts bytes\nfunc BodyLimit(limitBytes int64) echo.MiddlewareFunc\n\n// DefaultSkipper now uses *echo.Context\nfunc DefaultSkipper(c *echo.Context) bool\n\n// Trailing slash configs renamed/split\nfunc AddTrailingSlashWithConfig(config AddTrailingSlashConfig) echo.MiddlewareFunc\nfunc RemoveTrailingSlashWithConfig(config RemoveTrailingSlashConfig) echo.MiddlewareFunc\ntype AddTrailingSlashConfig struct{ ... }\ntype RemoveTrailingSlashConfig struct{ ... }\n\n// Auth + extractor signatures now use *echo.Context and add ExtractorSource\ntype BasicAuthValidator func(c *echo.Context, user string, password string) (bool, error)\ntype Extractor func(c *echo.Context) (string, error)\ntype ExtractorSource string\ntype KeyAuthValidator func(c *echo.Context, key string, source ExtractorSource) (bool, error)\ntype KeyAuthErrorHandler func(c *echo.Context, err error) error\n\n// BodyDump handler now includes err\ntype BodyDumpHandler func(c *echo.Context, reqBody []byte, resBody []byte, err error)\n\n// ValuesExtractor now returns extractor source and CreateExtractors takes a limit\ntype ValuesExtractor func(c *echo.Context) ([]string, ExtractorSource, error)\nfunc CreateExtractors(lookups string, limit uint) ([]ValuesExtractor, error)\ntype ValueExtractorError struct{ ... }\n\n// New constants\nconst KB = 1024\n\n// Rate limiter store now takes a float64 limit\nfunc NewRateLimiterMemoryStore(rateLimit float64) (store *RateLimiterMemoryStore)\n```\n\n### Added Middleware Exports\n\n```go\nvar ErrInvalidKey = echo.NewHTTPError(http.StatusUnauthorized, \"invalid key\")\nvar ErrKeyMissing = echo.NewHTTPError(http.StatusUnauthorized, \"missing key\")\nvar RedirectHTTPSConfig = RedirectConfig{ ... }\nvar RedirectHTTPSWWWConfig = RedirectConfig{ ... }\nvar RedirectNonHTTPSWWWConfig = RedirectConfig{ ... }\nvar RedirectNonWWWConfig = RedirectConfig{ ... }\nvar RedirectWWWConfig = RedirectConfig{ ... }\n```\n\n### Removed/Consolidated Middleware Exports\n\n```go\n// Removed in v5\nfunc Logger() echo.MiddlewareFunc\nfunc LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc\nfunc Timeout() echo.MiddlewareFunc\nfunc TimeoutWithConfig(config TimeoutConfig) echo.MiddlewareFunc\ntype ErrKeyAuthMissing struct{ ... }\ntype CSRFErrorHandler func(err error, c echo.Context) error\ntype LoggerConfig struct{ ... }\ntype LogErrorFunc func(c echo.Context, err error, stack []byte) error\ntype TargetProvider interface{ ... }\ntype TrailingSlashConfig struct{ ... }\ntype TimeoutConfig struct{ ... }\n```\n\nAlso removed defaults: `DefaultBasicAuthConfig`, `DefaultBodyDumpConfig`, `DefaultBodyLimitConfig`,\n`DefaultCORSConfig`, `DefaultDecompressConfig`, `DefaultGzipConfig`, `DefaultLoggerConfig`,\n`DefaultRedirectConfig`, `DefaultRequestIDConfig`, `DefaultRewriteConfig`, `DefaultTimeoutConfig`,\n`DefaultTrailingSlashConfig`.\n\n---\n\n## Router Interface Changes\n\n### v4 Router (Concrete Struct)\n\n```go\ntype Router struct { ... }\n\nfunc NewRouter(e *Echo) *Router\nfunc (r *Router) Add(method, path string, h HandlerFunc)\nfunc (r *Router) Find(method, path string, c Context)\nfunc (r *Router) Reverse(name string, params ...interface{}) string\nfunc (r *Router) Routes() []*Route\n```\n\n### v5 Router (Interface + DefaultRouter)\n\n```go\ntype Router interface {\n    Add(routable Route) (RouteInfo, error)\n    Remove(method string, path string) error\n    Routes() Routes\n    Route(c *Context) HandlerFunc\n}\n\ntype DefaultRouter struct { ... }\n\nfunc NewRouter(config RouterConfig) *DefaultRouter\nfunc NewConcurrentRouter(r Router) Router  // NEW\n\ntype RouterConfig struct {\n    NotFoundHandler           HandlerFunc\n    MethodNotAllowedHandler   HandlerFunc\n    OptionsMethodHandler      HandlerFunc\n    AllowOverwritingRoute     bool\n    UnescapePathParamValues   bool\n    UseEscapedPathForMatching bool\n}\n```\n\n**Key Changes:**\n- Router is now an interface\n- DefaultRouter is the concrete implementation\n- Add() returns `(RouteInfo, error)` instead of being void\n- New `Remove()` method\n- New `Route()` method replaces `Find()`\n- Configuration through `RouterConfig`\n\n---\n\n## Echo Instance Method Changes\n\n### Route Registration\n\n```go\n// v4\nfunc (e *Echo) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route\n\n// v5\nfunc (e *Echo) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) RouteInfo\nfunc (e *Echo) AddRoute(route Route) (RouteInfo, error)  // NEW\n```\n\n### Static File Serving\n\n```go\n// v4\nfunc (e *Echo) Static(pathPrefix, fsRoot string) *Route\nfunc (e *Echo) StaticFS(pathPrefix string, filesystem fs.FS) *Route\nfunc (e *Echo) File(path, file string, m ...MiddlewareFunc) *Route\nfunc (e *Echo) FileFS(path, file string, filesystem fs.FS, m ...MiddlewareFunc) *Route\n\n// v5\nfunc (e *Echo) Static(pathPrefix, fsRoot string, middleware ...MiddlewareFunc) RouteInfo\nfunc (e *Echo) StaticFS(pathPrefix string, filesystem fs.FS, middleware ...MiddlewareFunc) RouteInfo\nfunc (e *Echo) File(path, file string, middleware ...MiddlewareFunc) RouteInfo\nfunc (e *Echo) FileFS(path, file string, filesystem fs.FS, m ...MiddlewareFunc) RouteInfo\n```\n\nReturn type changed from `*Route` to `RouteInfo`.\n\n### Server Management\n\n```go\n// v4\nfunc (e *Echo) Start(address string) error\nfunc (e *Echo) StartTLS(address string, certFile, keyFile interface{}) error\nfunc (e *Echo) StartAutoTLS(address string) error\nfunc (e *Echo) StartH2CServer(address string, h2s *http2.Server) error\nfunc (e *Echo) StartServer(s *http.Server) error\nfunc (e *Echo) Shutdown(ctx context.Context) error\nfunc (e *Echo) Close() error\nfunc (e *Echo) ListenerAddr() net.Addr\nfunc (e *Echo) TLSListenerAddr() net.Addr\nfunc (e *Echo) DefaultHTTPErrorHandler(err error, c Context)\n\n// v5\nfunc (e *Echo) Start(address string) error  // Simplified\nfunc (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request)\n\n// Removed: StartTLS, StartAutoTLS, StartH2CServer, StartServer\n// Use StartConfig instead for advanced server configuration\n// Removed: Shutdown, Close, ListenerAddr, TLSListenerAddr\n// Removed: DefaultHTTPErrorHandler (now a top-level factory function)\n```\n\n**v5 provides** `StartConfig` type for all advanced server configuration.\n\n### Router Access\n\n```go\n// v4\nfunc (e *Echo) Router() *Router\nfunc (e *Echo) Routers() map[string]*Router  // For multi-host\nfunc (e *Echo) Routes() []*Route\nfunc (e *Echo) Reverse(name string, params ...interface{}) string\nfunc (e *Echo) URI(handler HandlerFunc, params ...interface{}) string\nfunc (e *Echo) URL(h HandlerFunc, params ...interface{}) string\nfunc (e *Echo) Host(name string, m ...MiddlewareFunc) *Group\n\n// v5\nfunc (e *Echo) Router() Router  // Returns interface\n// Removed: Routers(), Reverse(), URI(), URL(), Host()\n// Use router.Routes() and Routes.Reverse() instead\n```\n\n---\n\n## NewContext Changes\n\n```go\n// v4\nfunc (e *Echo) NewContext(r *http.Request, w http.ResponseWriter) Context\nfunc NewResponse(w http.ResponseWriter, e *Echo) *Response\n\n// v5\nfunc (e *Echo) NewContext(r *http.Request, w http.ResponseWriter) *Context\nfunc NewContext(r *http.Request, w http.ResponseWriter, opts ...any) *Context  // Standalone\nfunc NewResponse(w http.ResponseWriter, logger *slog.Logger) *Response\n```\n\n---\n\n## Migration Guide Summary\n\nIf you are using Linux you can migrate easier parts like that:\n```bash\nfind . -type f -name \"*.go\" -exec sed -i 's/ echo.Context/ *echo.Context/g' {} +\nfind . -type f -name \"*.go\" -exec sed -i 's/echo\\/v4/echo\\/v5/g' {} +\n```\nor in your favorite IDE\n\nReplace all:\n1. ` echo.Context` -> ` *echo.Context`\n2. `echo/v4` -> `echo/v5`\n\n\n### 1. Update All Handler Signatures\n\n```go\n// Before\nfunc MyHandler(c echo.Context) error { ... }\n\n// After\nfunc MyHandler(c *echo.Context) error { ... }\n```\n\n### 2. Update Logger Usage\n\n```go\n// Before\ne.Logger.Info(\"Server started\")\nc.Logger().Error(\"Something went wrong\")\n\n// After\ne.Logger.Info(\"Server started\")\nc.Logger().Error(\"Something went wrong\")  // Same API, different logger\n```\n\n### 3. Use Type-Safe Parameter Extraction\n\n```go\n// Before\nidStr := c.Param(\"id\")\nid, err := strconv.Atoi(idStr)\n\n// After\nid, err := echo.PathParam[int](c, \"id\")\n```\n\n### 4. Update Error Handler\n\n```go\n// Before\ne.HTTPErrorHandler = func(err error, c echo.Context) {\n    // handle error\n}\n\n// After\ne.HTTPErrorHandler = func(c *echo.Context, err error) {  // Swapped!\n    // handle error\n}\n\n// Or use factory\ne.HTTPErrorHandler = echo.DefaultHTTPErrorHandler(true)  // exposeError=true\n```\n\n### 5. Update Server Startup\n\n```go\n// Before\ne.Start(\":8080\")\ne.StartTLS(\":443\", \"cert.pem\", \"key.pem\")\n\n// After\n// Simple\ne.Start(\":8080\")\n\n// Advanced with graceful shutdown\nctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)\ndefer cancel()\nsc := echo.StartConfig{Address: \":8080\"}\nsc.Start(ctx, e)\n```\n\n### 6. Update Route Info Access\n\n```go\n// Before\nroutes := e.Routes()\nfor _, r := range routes {\n    fmt.Println(r.Method, r.Path)\n}\n\n// After\nroutes := e.Router().Routes()\nfor _, r := range routes {\n    fmt.Println(r.Method, r.Path)\n}\n```\n\n### 7. Update HTTPError Creation\n\n```go\n// Before\nreturn echo.NewHTTPError(400, \"invalid request\", someDetail)\n\n// After\nreturn echo.NewHTTPError(400, \"invalid request\")\n```\n\n### 8. Update Custom Binder\n\n```go\n// Before\ntype MyBinder struct{}\nfunc (b *MyBinder) Bind(i interface{}, c echo.Context) error { ... }\n\n// After\ntype MyBinder struct{}\nfunc (b *MyBinder) Bind(c *echo.Context, target any) error { ... }  // Swapped!\n```\n\n### 9. Path Parameters\n\n```go\n// Before\nnames := c.ParamNames()\nvalues := c.ParamValues()\n\n// After\npathValues := c.PathValues()\nfor _, pv := range pathValues {\n    fmt.Println(pv.Name, pv.Value)\n}\n```\n\n### 10. Response Access\n\n```go\n// Before\nresp := c.Response()\nresp.Header().Set(\"X-Custom\", \"value\")\n\n// After\nc.Response().Header().Set(\"X-Custom\", \"value\")  // Returns http.ResponseWriter\n\n// To get *echo.Response\nresp, err := echo.UnwrapResponse(c.Response())\n```\n\n### Go Version Requirements\n\n- **v4**: Go 1.24.0 (per `go.mod`)\n- **v5**: Go 1.25.0 (per `go.mod`)\n\n---\n\n**Generated by comparing `go doc` output from master (v4.15.0) and v5 (v5.0.0-alpha) branches**\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## v5.0.4 - 2026-02-15\n\n**Enhancements**\n\n* Remove unused import 'errors' from README example by @kumapower17 in https://github.com/labstack/echo/pull/2889\n* Fix Graceful shutdown: after `http.Server.Serve` returns we need to wait for graceful shutdown goroutine to finish by @aldas in https://github.com/labstack/echo/pull/2898\n* Update location of oapi-codegen in README by @mromaszewicz in https://github.com/labstack/echo/pull/2896\n* Add Go 1.26 to CI flow by @aldas in https://github.com/labstack/echo/pull/2899\n* Add new function `echo.StatusCode` by @suwakei in https://github.com/labstack/echo/pull/2892\n* CSRF: support older token-based CSRF protection handler that want to render token into template by @aldas in https://github.com/labstack/echo/pull/2894\n* Add `echo.ResolveResponseStatus` function to help middleware/handlers determine HTTP status code and echo.Response by @aldas in https://github.com/labstack/echo/pull/2900\n\n\n## v5.0.3 - 2026-02-06\n\n**Security**\n\n* Fix directory traversal vulnerability under Windows in Static middleware when default Echo filesystem is used. Reported by @shblue21.\n\nThis applies to cases when:\n- Windows is used as OS\n- `middleware.StaticConfig.Filesystem` is `nil` (default)\n- `echo.Filesystem` is has not been set explicitly (default)\n\nExposure is restricted to the active process working directory and its subfolders.\n\n\n## v5.0.2 - 2026-02-02\n\n**Security**\n\n* Fix Static middleware with `config.Browse=true` lists all files/subfolders from `config.Filesystem` root and not starting from `config.Root` in https://github.com/labstack/echo/pull/2887\n\n\n## v5.0.1 - 2026-01-28\n\n* Panic MW: will now return a custom PanicStackError with stack trace by @aldas in https://github.com/labstack/echo/pull/2871\n* Docs: add missing err parameter to DenyHandler example by @cgalibern in https://github.com/labstack/echo/pull/2878\n* improve: improve websocket checks in IsWebSocket() [per RFC 6455] by @raju-mechatronics in https://github.com/labstack/echo/pull/2875\n* fix: Context.Json() should not send status code before serialization is complete by @aldas in https://github.com/labstack/echo/pull/2877\n\n\n## v5.0.0 - 2026-01-18\n\nEcho `v5` is maintenance release with **major breaking changes**\n- `Context` is now struct instead of interface and we can add method to it in the future in minor versions.\n- Adds new `Router` interface for possible new routing implementations.\n- Drops old logging interface and uses moderm `log/slog` instead.\n- Rearranges alot of methods/function signatures to make them more consistent.\n\nUpgrade notes and `v4` support:\n- Echo `v4` is supported with **security*** updates and **bug** fixes until **2026-12-31**\n- If you are using Echo in a production environment, it is recommended to wait until after 2026-03-31 before upgrading.\n- Until 2026-03-31, any critical issues requiring breaking `v5` API changes will be addressed, even if this violates semantic versioning.\n\nSee [API_CHANGES_V5.md](./API_CHANGES_V5.md) for public API changes between `v4` and `v5`, notes on **upgrading**.\n\nUpgrading TLDR:\n\nIf you are using Linux you can migrate easier parts like that:\n```bash\nfind . -type f -name \"*.go\" -exec sed -i 's/ echo.Context/ *echo.Context/g' {} +\nfind . -type f -name \"*.go\" -exec sed -i 's/echo\\/v4/echo\\/v5/g' {} +\n```\nmacOS\n```bash\nfind . -type f -name \"*.go\" -exec sed -i '' 's/ echo.Context/ *echo.Context/g' {} +\nfind . -type f -name \"*.go\" -exec sed -i '' 's/echo\\/v4/echo\\/v5/g' {} +\n```\n\nor in your favorite IDE\n\nReplace all:\n1. ` echo.Context` -> ` *echo.Context`\n2. `echo/v4` -> `echo/v5`\n\nThis should solve most of the issues. Probably the hardest part is updating all the tests.\n\n\n## v4.15.0 - 2026-01-01\n\n\n**Security**\n\nNB: **If your application relies on cross-origin or same-site (same subdomain) requests do not blindly push this version to production**\n\n\nThe CSRF middleware now supports the [**Sec-Fetch-Site**](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-Fetch-Site) header as a modern, defense-in-depth approach to [CSRF\nprotection](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#fetch-metadata-headers), implementing the OWASP-recommended Fetch Metadata API alongside the traditional token-based mechanism.\n\n**How it works:**\n\nModern browsers automatically send the `Sec-Fetch-Site` header with all requests, indicating the relationship\nbetween the request origin and the target. The middleware uses this to make security decisions:\n\n- **`same-origin`** or **`none`**: Requests are allowed (exact origin match or direct user navigation)\n- **`same-site`**: Falls back to token validation (e.g., subdomain to main domain)\n- **`cross-site`**: Blocked by default with 403 error for unsafe methods (POST, PUT, DELETE, PATCH)\n\nFor browsers that don't send this header (older browsers), the middleware seamlessly falls back to\ntraditional token-based CSRF protection.\n\n**New Configuration Options:**\n- `TrustedOrigins []string`: Allowlist specific origins for cross-site requests (useful for OAuth callbacks, webhooks)\n- `AllowSecFetchSiteFunc func(echo.Context) (bool, error)`: Custom logic for same-site/cross-site request validation\n\n**Example:**\n  ```go\n  e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{\n      // Allow OAuth callbacks from trusted provider\n      TrustedOrigins: []string{\"https://oauth-provider.com\"},\n\n      // Custom validation for same-site requests\n      AllowSecFetchSiteFunc: func(c echo.Context) (bool, error) {\n          // Your custom authorization logic here\n          return validateCustomAuth(c), nil\n          // return true, err  // blocks request with error\n          // return true, nil  // allows CSRF request through\n          // return false, nil // falls back to legacy token logic\n      },\n  }))\n  ```\nPR: https://github.com/labstack/echo/pull/2858\n\n**Type-Safe Generic Parameter Binding**\n\n* Added generic functions for type-safe parameter extraction and context access by @aldas in https://github.com/labstack/echo/pull/2856\n\n  Echo now provides generic functions for extracting path, query, and form parameters with automatic type conversion,\n  eliminating manual string parsing and type assertions.\n\n  **New Functions:**\n  - Path parameters: `PathParam[T]`, `PathParamOr[T]`\n  - Query parameters: `QueryParam[T]`, `QueryParamOr[T]`, `QueryParams[T]`, `QueryParamsOr[T]`\n  - Form values: `FormParam[T]`, `FormParamOr[T]`, `FormParams[T]`, `FormParamsOr[T]`\n  - Context store: `ContextGet[T]`, `ContextGetOr[T]`\n\n  **Supported Types:**\n  Primitives (`bool`, `string`, `int`/`uint` variants, `float32`/`float64`), `time.Duration`, `time.Time`\n  (with custom layouts and Unix timestamp support), and custom types implementing `BindUnmarshaler`,\n  `TextUnmarshaler`, or `JSONUnmarshaler`.\n\n  **Example:**\n  ```go\n  // Before: Manual parsing\n  idStr := c.Param(\"id\")\n  id, err := strconv.Atoi(idStr)\n\n  // After: Type-safe with automatic parsing\n  id, err := echo.PathParam[int](c, \"id\")\n\n  // With default values\n  page, err := echo.QueryParamOr[int](c, \"page\", 1)\n  limit, err := echo.QueryParamOr[int](c, \"limit\", 20)\n\n  // Type-safe context access (no more panics from type assertions)\n  user, err := echo.ContextGet[*User](c, \"user\")\n  ```\n  \nPR: https://github.com/labstack/echo/pull/2856\n\n\n\n**DEPRECATION NOTICE** Timeout Middleware Deprecated - Use ContextTimeout Instead\n\nThe `middleware.Timeout` middleware has been **deprecated** due to fundamental architectural issues that cause\ndata races. Use `middleware.ContextTimeout` or `middleware.ContextTimeoutWithConfig` instead.\n\n**Why is this being deprecated?**\n\nThe Timeout middleware manipulates response writers across goroutine boundaries, which causes data races that\ncannot be reliably fixed without a complete architectural redesign. The middleware:\n\n- Swaps the response writer using `http.TimeoutHandler`\n- Must be the first middleware in the chain (fragile constraint)\n- Can cause races with other middleware (Logger, metrics, custom middleware)\n- Has been the source of multiple race condition fixes over the years\n\n**What should you use instead?**\n\nThe `ContextTimeout` middleware (available since v4.12.0) provides timeout functionality using Go's standard\ncontext mechanism. It is:\n\n- Race-free by design\n- Can be placed anywhere in the middleware chain\n- Simpler and more maintainable\n- Compatible with all other middleware\n\n**Migration Guide:**\n\n```go\n// Before (deprecated):\ne.Use(middleware.Timeout())\n\n// After (recommended):\ne.Use(middleware.ContextTimeout(30 * time.Second))\n```\n\n**Important Behavioral Differences:**\n\n1. **Handler cooperation required**: With ContextTimeout, your handlers must check `context.Done()` for cooperative\n   cancellation. The old Timeout middleware would send a 503 response regardless of handler cooperation, but had\n   data race issues.\n\n2. **Error handling**: ContextTimeout returns errors through the standard error handling flow. Handlers that receive\n   `context.DeadlineExceeded` should handle it appropriately:\n\n```go\ne.GET(\"/long-task\", func(c echo.Context) error {\n    ctx := c.Request().Context()\n\n    // Example: database query with context\n    result, err := db.QueryContext(ctx, \"SELECT * FROM large_table\")\n    if err != nil {\n        if errors.Is(err, context.DeadlineExceeded) {\n            // Handle timeout\n            return echo.NewHTTPError(http.StatusServiceUnavailable, \"Request timeout\")\n        }\n        return err\n    }\n\n    return c.JSON(http.StatusOK, result)\n})\n```\n\n3. **Background tasks**: For long-running background tasks, use goroutines with context:\n\n```go\ne.GET(\"/async-task\", func(c echo.Context) error {\n    ctx := c.Request().Context()\n\n    resultCh := make(chan Result, 1)\n    errCh := make(chan error, 1)\n\n    go func() {\n        result, err := performLongTask(ctx)\n        if err != nil {\n            errCh <- err\n            return\n        }\n        resultCh <- result\n    }()\n\n    select {\n    case result := <-resultCh:\n        return c.JSON(http.StatusOK, result)\n    case err := <-errCh:\n        return err\n    case <-ctx.Done():\n        return echo.NewHTTPError(http.StatusServiceUnavailable, \"Request timeout\")\n    }\n})\n```\n\n**Enhancements**\n\n* Fixes by @aldas in https://github.com/labstack/echo/pull/2852\n* Generic functions by @aldas in https://github.com/labstack/echo/pull/2856\n* CRSF with Sec-Fetch-Site checks by @aldas in https://github.com/labstack/echo/pull/2858\n\n\n## v4.14.0 - 2025-12-11\n\n`middleware.Logger` has been deprecated. For request logging, use `middleware.RequestLogger` or\n`middleware.RequestLoggerWithConfig`.\n\n`middleware.RequestLogger` replaces `middleware.Logger`, offering comparable configuration while relying on the\nGo standard library’s new `slog` logger.\n\nThe previous default output format was JSON. The new default follows the standard `slog` logger settings.\nTo continue emitting request logs in JSON, configure `slog` accordingly:\n```go\nslog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))\ne.Use(middleware.RequestLogger())\n```\n\n\n**Security**\n\n* Logger middleware json string escaping and deprecation by @aldas in https://github.com/labstack/echo/pull/2849\n\n\n\n**Enhancements**\n\n* Update deps  by @aldas in https://github.com/labstack/echo/pull/2807\n* refactor to use reflect.TypeFor by @cuiweixie in https://github.com/labstack/echo/pull/2812\n* Use Go 1.25 in CI by @aldas in https://github.com/labstack/echo/pull/2810\n* Modernize context.go by replacing interface{} with any by @vishr in https://github.com/labstack/echo/pull/2822\n* Fix typo in SetParamValues comment by @vishr in https://github.com/labstack/echo/pull/2828\n* Fix typo in ContextTimeout middleware comment by @vishr in https://github.com/labstack/echo/pull/2827\n* Improve BasicAuth middleware: use strings.Cut and RFC compliance by @vishr in https://github.com/labstack/echo/pull/2825\n* Fix duplicate plus operator in router backtracking logic by @yuya-morimoto in https://github.com/labstack/echo/pull/2832\n* Replace custom private IP range check with built-in net.IP.IsPrivate by @kumapower17 in https://github.com/labstack/echo/pull/2835\n* Ensure proxy connection is closed in proxyRaw function(#2837) by @kumapower17 in https://github.com/labstack/echo/pull/2838\n* Update deps by @aldas in https://github.com/labstack/echo/pull/2843\n* Update golang.org/x/* deps by @aldas in https://github.com/labstack/echo/pull/2850\n\n\n\n## v4.13.4 - 2025-05-22\n\n**Enhancements**\n\n* chore: fix some typos in comment by @zhuhaicity in https://github.com/labstack/echo/pull/2735\n* CI: test with Go 1.24 by @aldas in https://github.com/labstack/echo/pull/2748\n* Add support for TLS WebSocket proxy by @t-ibayashi-safie in https://github.com/labstack/echo/pull/2762\n\n**Security**\n\n* Update dependencies for [GO-2025-3487](https://pkg.go.dev/vuln/GO-2025-3487), [GO-2025-3503](https://pkg.go.dev/vuln/GO-2025-3503) and [GO-2025-3595](https://pkg.go.dev/vuln/GO-2025-3595) in https://github.com/labstack/echo/pull/2780\n\n\n## v4.13.3 - 2024-12-19\n\n**Security**\n\n* Update golang.org/x/net dependency [GO-2024-3333](https://pkg.go.dev/vuln/GO-2024-3333) in https://github.com/labstack/echo/pull/2722\n\n\n## v4.13.2 - 2024-12-12\n\n**Security**\n\n* Update dependencies (dependabot reports [GO-2024-3321](https://pkg.go.dev/vuln/GO-2024-3321)) in https://github.com/labstack/echo/pull/2721\n\n\n## v4.13.1 - 2024-12-11\n\n**Fixes**\n\n* Fix BindBody ignoring `Transfer-Encoding: chunked` requests by @178inaba in https://github.com/labstack/echo/pull/2717\n\n\n\n## v4.13.0 - 2024-12-04\n\n**BREAKING CHANGE** JWT Middleware Removed from Core use [labstack/echo-jwt](https://github.com/labstack/echo-jwt) instead\n\nThe JWT middleware has been **removed from Echo core** due to another security vulnerability, [CVE-2024-51744](https://nvd.nist.gov/vuln/detail/CVE-2024-51744). For more details, refer to issue [#2699](https://github.com/labstack/echo/issues/2699). A drop-in replacement is available in the [labstack/echo-jwt](https://github.com/labstack/echo-jwt) repository.\n\n**Important**: Direct assignments like `token := c.Get(\"user\").(*jwt.Token)` will now cause a panic due to an invalid cast. Update your code accordingly. Replace the current imports from `\"github.com/golang-jwt/jwt\"` in your handlers to the new middleware version using `\"github.com/golang-jwt/jwt/v5\"`.\n\n\nBackground: \n\nThe version of `golang-jwt/jwt` (v3.2.2) previously used in Echo core has been in an unmaintained state for some time. This is not the first vulnerability affecting this library; earlier issues were addressed in [PR #1946](https://github.com/labstack/echo/pull/1946).\nJWT middleware was marked as deprecated in Echo core as of [v4.10.0](https://github.com/labstack/echo/releases/tag/v4.10.0) on 2022-12-27. If you did not notice that, consider leveraging tools like [Staticcheck](https://staticcheck.dev/) to catch such deprecations earlier in you dev/CI flow.  For bonus points - check out [gosec](https://github.com/securego/gosec).\n\nWe sincerely apologize for any inconvenience caused by this change. While we strive to maintain backward compatibility within Echo core, recurring security issues with third-party dependencies have forced this decision.\n\n**Enhancements**\n\n* remove jwt middleware by @stevenwhitehead in https://github.com/labstack/echo/pull/2701\n* optimization: struct alignment by @behnambm in https://github.com/labstack/echo/pull/2636\n* bind: Maintain backwards compatibility for map[string]interface{} binding by @thesaltree in https://github.com/labstack/echo/pull/2656\n* Add Go 1.23 to CI by @aldas in https://github.com/labstack/echo/pull/2675\n* improve `MultipartForm` test by @martinyonatann in https://github.com/labstack/echo/pull/2682\n* `bind` : add support of multipart multi files by @martinyonatann in https://github.com/labstack/echo/pull/2684\n* Add TemplateRenderer struct to ease creating renderers for `html/template` and `text/template` packages. by @aldas in https://github.com/labstack/echo/pull/2690\n* Refactor TestBasicAuth to utilize table-driven test format by @ErikOlson in https://github.com/labstack/echo/pull/2688\n* Remove broken header by @aldas in https://github.com/labstack/echo/pull/2705\n* fix(bind body): content-length can be -1 by @phamvinhdat in https://github.com/labstack/echo/pull/2710\n* CORS middleware should compile allowOrigin regexp at creation by @aldas in https://github.com/labstack/echo/pull/2709\n* Shorten Github issue template and add test example by @aldas in https://github.com/labstack/echo/pull/2711\n\n\n## v4.12.0 - 2024-04-15\n\n**Security**\n\n* Update golang.org/x/net dep because of [GO-2024-2687](https://pkg.go.dev/vuln/GO-2024-2687) by @aldas in https://github.com/labstack/echo/pull/2625\n\n\n**Enhancements**\n\n* binder: make binding to Map work better with string destinations by @aldas in https://github.com/labstack/echo/pull/2554\n* README.md: add Encore as sponsor by @marcuskohlberg in https://github.com/labstack/echo/pull/2579\n* Reorder paragraphs in README.md by @aldas in https://github.com/labstack/echo/pull/2581\n* CI: upgrade actions/checkout to v4 by @aldas in https://github.com/labstack/echo/pull/2584\n* Remove default charset from 'application/json' Content-Type header by @doortts in https://github.com/labstack/echo/pull/2568\n* CI: Use Go 1.22 by @aldas in https://github.com/labstack/echo/pull/2588\n* binder: allow binding to a nil map by @georgmu in https://github.com/labstack/echo/pull/2574\n* Add Skipper Unit Test In BasicBasicAuthConfig and Add More Detail Explanation regarding BasicAuthValidator by @RyoKusnadi in https://github.com/labstack/echo/pull/2461\n* fix some typos by @teslaedison in https://github.com/labstack/echo/pull/2603\n* fix: some typos by @pomadev in https://github.com/labstack/echo/pull/2596\n* Allow ResponseWriters to unwrap writers when flushing/hijacking by @aldas in https://github.com/labstack/echo/pull/2595\n* Add SPDX licence comments to files.  by @aldas in https://github.com/labstack/echo/pull/2604\n* Upgrade deps by @aldas in https://github.com/labstack/echo/pull/2605\n* Change type definition blocks to single declarations. This helps copy… by @aldas in https://github.com/labstack/echo/pull/2606\n* Fix Real IP logic by @cl-bvl in https://github.com/labstack/echo/pull/2550\n* Default binder can use `UnmarshalParams(params []string) error` inter… by @aldas in https://github.com/labstack/echo/pull/2607\n* Default binder can bind pointer to slice as struct field. For example  `*[]string` by @aldas in https://github.com/labstack/echo/pull/2608\n* Remove maxparam dependence from Context by @aldas in https://github.com/labstack/echo/pull/2611\n* When route is registered with empty path it is normalized to `/`.  by @aldas in https://github.com/labstack/echo/pull/2616\n* proxy middleware should use httputil.ReverseProxy for SSE requests by @aldas in https://github.com/labstack/echo/pull/2624\n\n\n## v4.11.4 - 2023-12-20\n\n**Security**\n\n* Upgrade golang.org/x/crypto to v0.17.0 to fix vulnerability [issue](https://pkg.go.dev/vuln/GO-2023-2402) [#2562](https://github.com/labstack/echo/pull/2562)\n\n**Enhancements**\n\n* Update deps and mark Go version to 1.18 as this is what golang.org/x/* use [#2563](https://github.com/labstack/echo/pull/2563)\n* Request logger: add example for Slog https://pkg.go.dev/log/slog [#2543](https://github.com/labstack/echo/pull/2543)\n\n\n## v4.11.3 - 2023-11-07\n\n**Security**\n\n* 'c.Attachment' and 'c.Inline' should escape filename in 'Content-Disposition' header to avoid 'Reflect File Download' vulnerability. [#2541](https://github.com/labstack/echo/pull/2541)\n\n**Enhancements**\n\n* Tests: refactor context tests to be separate functions [#2540](https://github.com/labstack/echo/pull/2540)\n* Proxy middleware: reuse echo request context [#2537](https://github.com/labstack/echo/pull/2537)\n* Mark unmarshallable yaml struct tags as ignored [#2536](https://github.com/labstack/echo/pull/2536)\n\n\n## v4.11.2 - 2023-10-11\n\n**Security**\n\n* Bump golang.org/x/net to prevent CVE-2023-39325 / CVE-2023-44487 HTTP/2 Rapid Reset Attack [#2527](https://github.com/labstack/echo/pull/2527)\n* fix(sec): randomString bias introduced by #2490 [#2492](https://github.com/labstack/echo/pull/2492)\n* CSRF/RequestID mw: switch math/random usage to crypto/random [#2490](https://github.com/labstack/echo/pull/2490)\n\n**Enhancements**\n\n* Delete unused context in body_limit.go [#2483](https://github.com/labstack/echo/pull/2483)\n* Use Go 1.21 in CI [#2505](https://github.com/labstack/echo/pull/2505)\n* Fix some typos [#2511](https://github.com/labstack/echo/pull/2511)\n* Allow CORS middleware to send Access-Control-Max-Age: 0 [#2518](https://github.com/labstack/echo/pull/2518)\n* Bump dependancies [#2522](https://github.com/labstack/echo/pull/2522)\n\n## v4.11.1 - 2023-07-16\n\n**Fixes**\n\n* Fix `Gzip` middleware not sending response code for no content responses (404, 301/302 redirects etc) [#2481](https://github.com/labstack/echo/pull/2481)\n\n\n## v4.11.0 - 2023-07-14\n\n\n**Fixes**\n\n* Fixes the proxy middleware concurrency issue of calling the Next() proxy target on Round Robin Balancer [#2409](https://github.com/labstack/echo/pull/2409)\n* Fix `group.RouteNotFound` not working when group has attached middlewares [#2411](https://github.com/labstack/echo/pull/2411)\n* Fix global error handler return error message when message is an error [#2456](https://github.com/labstack/echo/pull/2456)\n* Do not use global timeNow variables [#2477](https://github.com/labstack/echo/pull/2477)\n\n\n**Enhancements**\n\n* Added a optional config variable to disable centralized error handler in recovery middleware [#2410](https://github.com/labstack/echo/pull/2410)\n* refactor: use `strings.ReplaceAll` directly [#2424](https://github.com/labstack/echo/pull/2424)\n* Add support for Go1.20 `http.rwUnwrapper` to Response struct [#2425](https://github.com/labstack/echo/pull/2425)\n* Check whether is nil before invoking centralized error handling [#2429](https://github.com/labstack/echo/pull/2429)\n* Proper colon support in `echo.Reverse` method [#2416](https://github.com/labstack/echo/pull/2416)\n* Fix misuses of a vs an in documentation comments [#2436](https://github.com/labstack/echo/pull/2436)\n* Add link to slog.Handler library for Echo logging into README.md [#2444](https://github.com/labstack/echo/pull/2444)\n* In proxy middleware Support retries of failed proxy requests [#2414](https://github.com/labstack/echo/pull/2414)\n* gofmt fixes to comments [#2452](https://github.com/labstack/echo/pull/2452)\n* gzip response only if it exceeds a minimal length [#2267](https://github.com/labstack/echo/pull/2267)\n* Upgrade packages [#2475](https://github.com/labstack/echo/pull/2475)\n\n\n## v4.10.2 - 2023-02-22\n\n**Security**\n\n* `filepath.Clean` behaviour has changed in Go 1.20 - adapt to it [#2406](https://github.com/labstack/echo/pull/2406)\n* Add `middleware.CORSConfig.UnsafeWildcardOriginWithAllowCredentials` to make UNSAFE usages of wildcard origin + allow cretentials less likely [#2405](https://github.com/labstack/echo/pull/2405)\n\n**Enhancements**\n\n* Add more HTTP error values [#2277](https://github.com/labstack/echo/pull/2277)\n\n\n## v4.10.1 - 2023-02-19\n\n**Security**\n\n* Upgrade deps due to the latest golang.org/x/net vulnerability [#2402](https://github.com/labstack/echo/pull/2402)\n\n\n**Enhancements**\n\n* Add new JWT repository to the README [#2377](https://github.com/labstack/echo/pull/2377)\n* Return an empty string for ctx.path if there is no registered path [#2385](https://github.com/labstack/echo/pull/2385)\n* Add context timeout middleware [#2380](https://github.com/labstack/echo/pull/2380)\n* Update link to jaegertracing [#2394](https://github.com/labstack/echo/pull/2394)\n\n\n## v4.10.0 - 2022-12-27\n\n**Security**\n\n* We are deprecating JWT middleware in this repository. Please use https://github.com/labstack/echo-jwt instead. \n\n  JWT middleware is moved to separate repository to allow us to bump/upgrade version of JWT implementation (`github.com/golang-jwt/jwt`) we are using\nwhich we can not do in Echo core because this would break backwards compatibility guarantees we try to maintain.\n\n* This minor version bumps minimum Go version to 1.17 (from 1.16) due `golang.org/x/` packages we depend on. There are\n  several vulnerabilities fixed in these libraries.\n\n  Echo still tries to support last 4 Go versions but there are occasions we can not guarantee this promise.\n\n\n**Enhancements**\n\n* Bump x/text to 0.3.8 [#2305](https://github.com/labstack/echo/pull/2305)\n* Bump dependencies and add notes about Go releases we support [#2336](https://github.com/labstack/echo/pull/2336)\n* Add helper interface for ProxyBalancer interface [#2316](https://github.com/labstack/echo/pull/2316)\n* Expose `middleware.CreateExtractors` function so we can use it from echo-contrib repository [#2338](https://github.com/labstack/echo/pull/2338)\n* Refactor func(Context) error to HandlerFunc [#2315](https://github.com/labstack/echo/pull/2315)\n* Improve function comments [#2329](https://github.com/labstack/echo/pull/2329)\n* Add new method HTTPError.WithInternal [#2340](https://github.com/labstack/echo/pull/2340)\n* Replace io/ioutil package usages [#2342](https://github.com/labstack/echo/pull/2342)\n* Add staticcheck to CI flow [#2343](https://github.com/labstack/echo/pull/2343)\n* Replace relative path determination from proprietary to std [#2345](https://github.com/labstack/echo/pull/2345)\n* Remove square brackets from ipv6 addresses in XFF (X-Forwarded-For header) [#2182](https://github.com/labstack/echo/pull/2182)\n* Add testcases for some BodyLimit middleware configuration options [#2350](https://github.com/labstack/echo/pull/2350)\n* Additional configuration options for RequestLogger and Logger middleware [#2341](https://github.com/labstack/echo/pull/2341)\n* Add route to request log [#2162](https://github.com/labstack/echo/pull/2162)\n* GitHub Workflows security hardening [#2358](https://github.com/labstack/echo/pull/2358)\n* Add govulncheck to CI and bump dependencies [#2362](https://github.com/labstack/echo/pull/2362)\n* Fix rate limiter docs [#2366](https://github.com/labstack/echo/pull/2366)\n* Refactor how `e.Routes()` work and introduce `e.OnAddRouteHandler` callback [#2337](https://github.com/labstack/echo/pull/2337)\n\n\n## v4.9.1 - 2022-10-12\n\n**Fixes**\n\n* Fix logger panicing (when template is set to empty) by bumping dependency version [#2295](https://github.com/labstack/echo/issues/2295)\n\n**Enhancements**\n\n* Improve CORS documentation [#2272](https://github.com/labstack/echo/pull/2272)\n* Update readme about supported Go versions [#2291](https://github.com/labstack/echo/pull/2291)\n* Tests: improve error handling on closing body [#2254](https://github.com/labstack/echo/pull/2254)\n* Tests: refactor some of the assertions in tests [#2275](https://github.com/labstack/echo/pull/2275)\n* Tests: refactor assertions [#2301](https://github.com/labstack/echo/pull/2301)\n\n## v4.9.0 - 2022-09-04\n\n**Security**\n\n* Fix open redirect vulnerability in handlers serving static directories (e.Static, e.StaticFs, echo.StaticDirectoryHandler) [#2260](https://github.com/labstack/echo/pull/2260)\n\n**Enhancements**\n\n* Allow configuring ErrorHandler in CSRF middleware [#2257](https://github.com/labstack/echo/pull/2257)\n* Replace HTTP method constants in tests with stdlib constants [#2247](https://github.com/labstack/echo/pull/2247)\n\n\n## v4.8.0 - 2022-08-10\n\n**Most notable things**\n\nYou can now add any arbitrary HTTP method type as a route [#2237](https://github.com/labstack/echo/pull/2237)\n```go\ne.Add(\"COPY\", \"/*\", func(c echo.Context) error \n  return c.String(http.StatusOK, \"OK COPY\")\n})\n```\n\nYou can add custom 404 handler for specific paths [#2217](https://github.com/labstack/echo/pull/2217)\n```go\ne.RouteNotFound(\"/*\", func(c echo.Context) error { return c.NoContent(http.StatusNotFound) })\n\ng := e.Group(\"/images\")\ng.RouteNotFound(\"/*\", func(c echo.Context) error { return c.NoContent(http.StatusNotFound) })\n```\n\n**Enhancements**\n\n* Add new value binding methods (UnixTimeMilli,TextUnmarshaler,JSONUnmarshaler) to Valuebinder [#2127](https://github.com/labstack/echo/pull/2127)\n* Refactor: body_limit middleware unit test [#2145](https://github.com/labstack/echo/pull/2145)\n* Refactor: Timeout mw: rework how test waits for timeout. [#2187](https://github.com/labstack/echo/pull/2187)\n* BasicAuth middleware returns 500 InternalServerError on invalid base64 strings but should return 400 [#2191](https://github.com/labstack/echo/pull/2191)\n* Refactor: duplicated findStaticChild process at findChildWithLabel [#2176](https://github.com/labstack/echo/pull/2176)\n* Allow different param names in different methods with same path scheme [#2209](https://github.com/labstack/echo/pull/2209)\n* Add support for registering handlers for different 404 routes [#2217](https://github.com/labstack/echo/pull/2217)\n* Middlewares should use errors.As() instead of type assertion on HTTPError [#2227](https://github.com/labstack/echo/pull/2227)\n* Allow arbitrary HTTP method types to be added as routes [#2237](https://github.com/labstack/echo/pull/2237)\n\n## v4.7.2 - 2022-03-16\n\n**Fixes**\n\n* Fix nil pointer exception when calling Start again after address binding error [#2131](https://github.com/labstack/echo/pull/2131)\n* Fix CSRF middleware not being able to extract token from multipart/form-data form [#2136](https://github.com/labstack/echo/pull/2136)\n* Fix Timeout middleware write race [#2126](https://github.com/labstack/echo/pull/2126)\n\n**Enhancements**\n\n* Recover middleware should not log panic for aborted handler [#2134](https://github.com/labstack/echo/pull/2134)\n\n\n## v4.7.1 - 2022-03-13\n\n**Fixes**\n\n* Fix `e.Static`, `.File()`, `c.Attachment()` being picky with paths starting with `./`, `../` and `/` after 4.7.0 introduced echo.Filesystem support (Go1.16+) [#2123](https://github.com/labstack/echo/pull/2123)\n\n**Enhancements**\n\n* Remove some unused code [#2116](https://github.com/labstack/echo/pull/2116)\n\n\n## v4.7.0 - 2022-03-01\n\n**Enhancements**\n\n* Add JWT, KeyAuth, CSRF multivalue extractors [#2060](https://github.com/labstack/echo/pull/2060)\n* Add LogErrorFunc to recover middleware [#2072](https://github.com/labstack/echo/pull/2072)\n* Add support for HEAD method query params binding [#2027](https://github.com/labstack/echo/pull/2027)\n* Improve filesystem support with echo.FileFS, echo.StaticFS, group.FileFS, group.StaticFS [#2064](https://github.com/labstack/echo/pull/2064)\n\n**Fixes**\n\n* Fix X-Real-IP bug, improve tests [#2007](https://github.com/labstack/echo/pull/2007)\n* Minor syntax fixes [#1994](https://github.com/labstack/echo/pull/1994), [#2102](https://github.com/labstack/echo/pull/2102), [#2102](https://github.com/labstack/echo/pull/2102)\n\n**General**\n\n* Add cache-control and connection headers [#2103](https://github.com/labstack/echo/pull/2103)\n* Add Retry-After header constant [#2078](https://github.com/labstack/echo/pull/2078)\n* Upgrade `go` directive in `go.mod` to 1.17 [#2049](https://github.com/labstack/echo/pull/2049)\n* Add Pagoda [#2077](https://github.com/labstack/echo/pull/2077) and Souin [#2069](https://github.com/labstack/echo/pull/2069) to 3rd-party middlewares in README\n\n## v4.6.3 - 2022-01-10\n\n**Fixes**\n\n* Fixed Echo version number in greeting message which was not incremented to `4.6.2` [#2066](https://github.com/labstack/echo/issues/2066)\n\n\n## v4.6.2 - 2022-01-08\n\n**Fixes**\n\n* Fixed route containing escaped colon should be matchable but is not matched to request path [#2047](https://github.com/labstack/echo/pull/2047)\n* Fixed a problem that returned wrong content-encoding when the gzip compressed content was empty. [#1921](https://github.com/labstack/echo/pull/1921)\n* Update (test) dependencies [#2021](https://github.com/labstack/echo/pull/2021)\n\n\n**Enhancements**\n\n* Add support for configurable target header for the request_id middleware [#2040](https://github.com/labstack/echo/pull/2040)\n* Change decompress middleware to use stream decompression instead of buffering [#2018](https://github.com/labstack/echo/pull/2018)\n* Documentation updates\n\n\n## v4.6.1 - 2021-09-26\n\n**Enhancements**\n\n* Add start time to request logger middleware values [#1991](https://github.com/labstack/echo/pull/1991)\n\n## v4.6.0 - 2021-09-20\n\nIntroduced a new [request logger](https://github.com/labstack/echo/blob/master/middleware/request_logger.go) middleware \nto help with cases when you want to use some other logging library in your application.\n\n**Fixes**\n\n* fix timeout middleware warning: superfluous response.WriteHeader [#1905](https://github.com/labstack/echo/issues/1905)\n\n**Enhancements**\n\n* Add Cookie to KeyAuth middleware's KeyLookup [#1929](https://github.com/labstack/echo/pull/1929)\n* JWT middleware should ignore case of auth scheme in request header [#1951](https://github.com/labstack/echo/pull/1951)\n* Refactor default error handler to return first if response is already committed [#1956](https://github.com/labstack/echo/pull/1956)\n* Added request logger middleware which helps to use custom logger library for logging requests. [#1980](https://github.com/labstack/echo/pull/1980)\n* Allow escaping of colon in route path so Google Cloud API \"custom methods\" could be implemented [#1988](https://github.com/labstack/echo/pull/1988)\n\n## v4.5.0 - 2021-08-01\n\n**Important notes**\n\nA **BREAKING CHANGE** is introduced for JWT middleware users.\nThe JWT library used for the JWT middleware had to be changed from [github.com/dgrijalva/jwt-go](https://github.com/dgrijalva/jwt-go) to\n[github.com/golang-jwt/jwt](https://github.com/golang-jwt/jwt) due former library being unmaintained and affected by security\nissues.\nThe [github.com/golang-jwt/jwt](https://github.com/golang-jwt/jwt) project is a drop-in replacement, but supports only the latest 2 Go versions.\nSo for JWT middleware users Go 1.15+ is required. For detailed information please read [#1940](https://github.com/labstack/echo/discussions/)\n\nTo change the library imports in all .go files in your project replace all occurrences of `dgrijalva/jwt-go` with `golang-jwt/jwt`.\n\nFor Linux CLI you can use:\n```bash\nfind -type f -name \"*.go\" -exec sed -i \"s/dgrijalva\\/jwt-go/golang-jwt\\/jwt/g\" {} \\;\ngo mod tidy\n```\n\n**Fixes**\n\n* Change JWT library to `github.com/golang-jwt/jwt` [#1946](https://github.com/labstack/echo/pull/1946)\n\n## v4.4.0 - 2021-07-12\n\n**Fixes**\n\n* Split HeaderXForwardedFor header only by comma [#1878](https://github.com/labstack/echo/pull/1878)\n* Fix Timeout middleware Context propagation [#1910](https://github.com/labstack/echo/pull/1910)\n \n**Enhancements**\n\n* Bind data using headers as source [#1866](https://github.com/labstack/echo/pull/1866)\n* Adds JWTConfig.ParseTokenFunc to JWT middleware to allow different libraries implementing JWT parsing. [#1887](https://github.com/labstack/echo/pull/1887)\n* Adding tests for Echo#Host [#1895](https://github.com/labstack/echo/pull/1895)\n* Adds RequestIDHandler function to RequestID middleware [#1898](https://github.com/labstack/echo/pull/1898)\n* Allow for custom JSON encoding implementations [#1880](https://github.com/labstack/echo/pull/1880)\n\n## v4.3.0 - 2021-05-08\n\n**Important notes**\n\n* Route matching has improvements for following cases:\n  1. Correctly match routes with parameter part as last part of route (with trailing backslash)\n  2. Considering handlers when resolving routes and search for matching http method handler\n* Echo minimal Go version is now 1.13. \n\n**Fixes**\n\n* When url ends with slash first param route is the match [#1804](https://github.com/labstack/echo/pull/1812)\n* Router should check if node is suitable as matching route by path+method and if not then continue search in tree [#1808](https://github.com/labstack/echo/issues/1808)\n* Fix timeout middleware not writing response correctly when handler panics [#1864](https://github.com/labstack/echo/pull/1864)\n* Fix binder not working with embedded pointer structs [#1861](https://github.com/labstack/echo/pull/1861)\n* Add Go 1.16 to CI and drop 1.12 specific code [#1850](https://github.com/labstack/echo/pull/1850)\n\n**Enhancements**\n\n* Make KeyFunc public in JWT middleware [#1756](https://github.com/labstack/echo/pull/1756)\n* Add support for optional filesystem to the static middleware [#1797](https://github.com/labstack/echo/pull/1797)\n* Add a custom error handler to key-auth middleware [#1847](https://github.com/labstack/echo/pull/1847)\n* Allow JWT token to be looked up from multiple sources [#1845](https://github.com/labstack/echo/pull/1845)\n\n## v4.2.2 - 2021-04-07\n\n**Fixes**\n\n* Allow proxy middleware to use query part in rewrite (#1802)\n* Fix timeout middleware not sending status code when handler returns an error (#1805)\n* Fix Bind() when target is array/slice and path/query params complains bind target not being struct (#1835)\n* Fix panic in redirect middleware on short host name (#1813)\n* Fix timeout middleware docs (#1836)\n\n## v4.2.1 - 2021-03-08\n\n**Important notes**\n\nDue to a datarace the config parameters for the newly added timeout middleware required a change.\nSee the [docs](https://echo.labstack.com/middleware/timeout).\nA performance regression has been fixed, even bringing better performance than before for some routing scenarios.\n\n**Fixes**\n\n* Fix performance regression caused by path escaping (#1777, #1798, #1799, aldas)\n* Avoid context canceled errors (#1789, clwluvw)\n* Improve router to use on stack backtracking (#1791, aldas, stffabi)\n* Fix panic in timeout middleware not being not recovered and cause application crash (#1794, aldas)\n* Fix Echo.Serve() not serving on HTTP port correctly when TLSListener is used (#1785, #1793, aldas)\n* Apply go fmt (#1788, Le0tk0k)\n* Uses strings.Equalfold (#1790, rkilingr)\n* Improve code quality (#1792, withshubh)\n\nThis release was made possible by our **contributors**:\naldas, clwluvw, lammel, Le0tk0k, maciej-jezierski, rkilingr, stffabi, withshubh\n\n## v4.2.0 - 2021-02-11\n\n**Important notes**\n\nThe behaviour for binding data has been reworked for compatibility with echo before v4.1.11 by\nenforcing `explicit tagging` for processing parameters. This **may break** your code if you \nexpect combined handling of query/path/form params.\nPlease see the updated documentation for [request](https://echo.labstack.com/guide/request) and [binding](https://echo.labstack.com/guide/request)\n\nThe handling for rewrite rules has been slightly adjusted to expand `*` to a non-greedy `(.*?)` capture group. This is only relevant if multiple asterisks are used in your rules.\nPlease see [rewrite](https://echo.labstack.com/middleware/rewrite) and [proxy](https://echo.labstack.com/middleware/proxy) for details.\n\n**Security**\n\n* Fix directory traversal vulnerability for Windows (#1718, little-cui)\n* Fix open redirect vulnerability with trailing slash (#1771,#1775 aldas,GeoffreyFrogeye)\n\n**Enhancements**\n\n* Add Echo#ListenerNetwork as configuration (#1667, pafuent)\n* Add ability to change the status code using response beforeFuncs (#1706, RashadAnsari)\n* Echo server startup to allow data race free access to listener address\n* Binder: Restore pre v4.1.11 behaviour for c.Bind() to use query params only for GET or DELETE methods (#1727, aldas)\n* Binder: Add separate methods to bind only query params, path params or request body (#1681, aldas)\n* Binder: New fluent binder for query/path/form parameter binding (#1717, #1736, aldas)\n* Router: Performance improvements for missed routes (#1689, pafuent)\n* Router: Improve performance for Real-IP detection using IndexByte instead of Split (#1640, imxyb)\n* Middleware: Support real regex rules for rewrite and proxy middleware (#1767)\n* Middleware: New rate limiting middleware (#1724, iambenkay)\n* Middleware: New timeout middleware implementation for go1.13+ (#1743, )\n* Middleware: Allow regex pattern for CORS middleware (#1623, KlotzAndrew)\n* Middleware: Add IgnoreBase parameter to static middleware (#1701, lnenad, iambenkay)\n* Middleware: Add an optional custom function to CORS middleware to validate origin (#1651, curvegrid)\n* Middleware: Support form fields in JWT middleware (#1704, rkfg)\n* Middleware: Use sync.Pool for (de)compress middleware to improve performance (#1699, #1672, pafuent)\n* Middleware: Add decompress middleware to support gzip compressed requests (#1687, arun0009)\n* Middleware: Add ErrJWTInvalid for JWT middleware (#1627, juanbelieni)\n* Middleware: Add SameSite mode for CSRF cookies to support iframes (#1524, pr0head)\n\n**Fixes**\n\n* Fix handling of special trailing slash case for partial prefix (#1741, stffabi)\n* Fix handling of static routes with trailing slash (#1747)\n* Fix Static files route not working (#1671, pwli0755, lammel)\n* Fix use of caret(^) in regex for rewrite middleware (#1588, chotow)\n* Fix Echo#Reverse for Any type routes (#1695, pafuent)\n* Fix Router#Find panic with infinite loop (#1661, pafuent)\n* Fix Router#Find panic fails on Param paths (#1659, pafuent)\n* Fix DefaultHTTPErrorHandler with Debug=true (#1477, lammel)\n* Fix incorrect CORS headers (#1669, ulasakdeniz)\n* Fix proxy middleware rewritePath to use url with updated tests (#1630, arun0009)\n* Fix rewritePath for proxy middleware to use escaped path in (#1628, arun0009)\n* Remove unless defer (#1656, imxyb)\n\n**General**\n\n* New maintainers for Echo: Roland Lammel (@lammel) and Pablo Andres Fuente (@pafuent)\n* Add GitHub action to compare benchmarks (#1702, pafuent)\n* Binding query/path params and form fields to struct only works for explicit tags (#1729,#1734, aldas)\n* Add support for Go 1.15 in CI (#1683, asahasrabuddhe)\n* Add test for request id to remain unchanged if provided (#1719, iambenkay)\n* Refactor echo instance listener access and startup to speed up testing (#1735, aldas)\n* Refactor and improve various tests for binding and routing\n* Run test workflow only for relevant changes (#1637, #1636, pofl)\n* Update .travis.yml (#1662, santosh653)\n* Update README.md with an recents framework benchmark (#1679, pafuent)\n\nThis release was made possible by **over 100 commits** from more than **20 contributors**:\nasahasrabuddhe, aldas, AndrewKlotz, arun0009, chotow, curvegrid, iambenkay, imxyb, \njuanbelieni,  lammel, little-cui, lnenad, pafuent, pofl, pr0head, pwli, RashadAnsari, \nrkfg, santosh653, segfiner, stffabi, ulasakdeniz\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## About This Project\n\nEcho is a high performance, minimalist Go web framework. This is the main repository for Echo v4, which is available as a Go module at `github.com/labstack/echo/v4`.\n\n## Development Commands\n\nThe project uses a Makefile for common development tasks:\n\n- `make check` - Run linting, vetting, and race condition tests (default target)\n- `make init` - Install required linting tools (golint, staticcheck)\n- `make lint` - Run staticcheck and golint\n- `make vet` - Run go vet\n- `make test` - Run short tests\n- `make race` - Run tests with race detector\n- `make benchmark` - Run benchmarks\n\nExample commands for development:\n```bash\n# Setup development environment\nmake init\n\n# Run all checks (lint, vet, race)\nmake check\n\n# Run specific tests\ngo test ./middleware/...\ngo test -race ./...\n\n# Run benchmarks\nmake benchmark\n```\n\n## Code Architecture\n\n### Core Components\n\n**Echo Instance (`echo.go`)**\n- The `Echo` struct is the top-level framework instance\n- Contains router, middleware stacks, and server configuration\n- Not goroutine-safe for mutations after server start\n\n**Context (`context.go`)**\n- The `Context` interface represents HTTP request/response context\n- Provides methods for request/response handling, path parameters, data binding\n- Core abstraction for request processing\n\n**Router (`router.go`)**\n- Radix tree-based HTTP router with smart route prioritization\n- Supports static routes, parameterized routes (`/users/:id`), and wildcard routes (`/static/*`)\n- Each HTTP method has its own routing tree\n\n**Middleware (`middleware/`)**\n- Extensive middleware system with 50+ built-in middlewares\n- Middleware can be applied at Echo, Group, or individual route level\n- Common middleware: Logger, Recover, CORS, JWT, Rate Limiting, etc.\n\n### Key Patterns\n\n**Middleware Chain**\n- Pre-middleware runs before routing\n- Regular middleware runs after routing but before handlers\n- Middleware functions have signature `func(next echo.HandlerFunc) echo.HandlerFunc`\n\n**Route Groups**\n- Routes can be grouped with common prefixes and middleware\n- Groups support nested sub-groups\n- Defined in `group.go`\n\n**Data Binding**\n- Automatic binding of request data (JSON, XML, form) to Go structs\n- Implemented in `binder.go` with support for custom binders\n\n**Error Handling**\n- Centralized error handling via `HTTPErrorHandler`\n- Automatic panic recovery with stack traces\n\n## File Organization\n\n- Root directory: Core Echo functionality (echo.go, context.go, router.go, etc.)\n- `middleware/`: All built-in middleware implementations\n- `_test/`: Test fixtures and utilities\n- `_fixture/`: Test data files\n\n## Code Style\n\n- Go code uses tabs for indentation (per .editorconfig)\n- Follows standard Go conventions and formatting\n- Uses gofmt, golint, and staticcheck for code quality\n\n## Testing\n\n- Standard Go testing with `testing` package\n- Tests include unit tests, integration tests, and benchmarks\n- Race condition testing is required (`make race`)\n- Test files follow `*_test.go` naming convention"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2022 LabStack\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "PKG := \"github.com/labstack/echo\"\nPKG_LIST := $(shell go list ${PKG}/...)\n\n.DEFAULT_GOAL := check\ncheck: lint vet race ## Check project\n\ninit:\n\t@go install golang.org/x/lint/golint@latest\n\t@go install honnef.co/go/tools/cmd/staticcheck@latest\n\nlint: ## Lint the files\n\t@staticcheck ${PKG_LIST}\n\t@golint -set_exit_status ${PKG_LIST}\n\nvet: ## Vet the files\n\t@go vet ${PKG_LIST}\n\ntest: ## Run tests\n\t@go test -short ${PKG_LIST}\n\nrace: ## Run tests with data race detector\n\t@go test -race ${PKG_LIST}\n\nbenchmark: ## Run benchmarks\n\t@go test -run=\"-\" -benchmem -bench=\".*\" ${PKG_LIST}\n\nhelp: ## Display this help screen\n\t@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = \":.*?## \"}; {printf \"\\033[36m%-30s\\033[0m %s\\n\", $$1, $$2}'\n\ngoversion ?= \"1.25\"\ntest_version: ## Run tests inside Docker with given version (defaults to 1.25 oldest supported). Example: make test_version goversion=1.25\n\t@docker run --rm -it -v $(shell pwd):/project golang:$(goversion) /bin/sh -c \"cd /project && make init check\"\n"
  },
  {
    "path": "README.md",
    "content": "[![Sourcegraph](https://sourcegraph.com/github.com/labstack/echo/-/badge.svg?style=flat-square)](https://sourcegraph.com/github.com/labstack/echo?badge)\n[![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://pkg.go.dev/github.com/labstack/echo/v4)\n[![Go Report Card](https://goreportcard.com/badge/github.com/labstack/echo?style=flat-square)](https://goreportcard.com/report/github.com/labstack/echo)\n[![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)\n[![Codecov](https://img.shields.io/codecov/c/github/labstack/echo.svg?style=flat-square)](https://codecov.io/gh/labstack/echo)\n[![Forum](https://img.shields.io/badge/community-forum-00afd1.svg?style=flat-square)](https://github.com/labstack/echo/discussions)\n[![Twitter](https://img.shields.io/badge/twitter-@labstack-55acee.svg?style=flat-square)](https://twitter.com/labstack)\n[![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/labstack/echo/master/LICENSE)\n\n## Echo\n\nHigh performance, extensible, minimalist Go web framework.\n\n* [Official website](https://echo.labstack.com)\n* [Quick start](https://echo.labstack.com/docs/quick-start)\n* [Middlewares](https://echo.labstack.com/docs/category/middleware)\n\nHelp and questions: [Github Discussions](https://github.com/labstack/echo/discussions)\n\n### Feature Overview\n\n- Optimized HTTP router which smartly prioritize routes\n- Build robust and scalable RESTful APIs\n- Group APIs\n- Extensible middleware framework\n- Define middleware at root, group or route level\n- Data binding for JSON, XML and form payload\n- Handy functions to send variety of HTTP responses\n- Centralized HTTP error handling\n- Template rendering with any template engine\n- Define your format for the logger\n- Highly customizable\n- Automatic TLS via Let’s Encrypt\n- HTTP/2 support\n\n## Sponsors\n\n<div>\n  <a href=\"https://encore.dev\" style=\"display: inline-flex; align-items: center; gap: 10px\">\n    <img src=\"https://user-images.githubusercontent.com/78424526/214602214-52e0483a-b5fc-4d4c-b03e-0b7b23e012df.svg\" height=\"28px\" alt=\"encore icon\"></img>\n  <b>Encore – the platform for building Go-based cloud backends</b>\n    </a>\n</div>\n<br/>\n\nClick [here](https://github.com/sponsors/labstack) for more information on sponsorship.\n\n## [Guide](https://echo.labstack.com/guide)\n\n### Supported Echo versions\n\n- Latest major version of Echo is `v5` as of 2026-01-18.\n  - Until 2026-03-31, any critical issues requiring breaking API changes will be addressed, even if this violates\n    semantic versioning.\n  - See [API_CHANGES_V5.md](./API_CHANGES_V5.md) for public API changes between `v4` and `v5`, notes on upgrading.\n  - If you are using Echo in a production environment, it is recommended to wait until after 2026-03-31 before\n    upgrading.\n- Echo `v4` is supported with **security*** updates and **bug** fixes until **2026-12-31**\n\n### Installation\n\n```sh\n// go get github.com/labstack/echo/{version}\ngo get github.com/labstack/echo/v5\n```\n\nLatest version of Echo supports last four Go major [releases](https://go.dev/doc/devel/release) and might work with\nolder versions.\n\n### Example\n\n```go\npackage main\n\nimport (\n  \"github.com/labstack/echo/v5\"\n  \"github.com/labstack/echo/v5/middleware\"\n  \"log/slog\"\n  \"net/http\"\n)\n\nfunc main() {\n  // Echo instance\n  e := echo.New()\n\n  // Middleware\n  e.Use(middleware.RequestLogger()) // use the RequestLogger middleware with slog logger\n  e.Use(middleware.Recover())       // recover panics as errors for proper error handling\n\n  // Routes\n  e.GET(\"/\", hello)\n\n  // Start server\n  if err := e.Start(\":8080\"); err != nil {\n    slog.Error(\"failed to start server\", \"error\", err)\n  }\n}\n\n// Handler\nfunc hello(c *echo.Context) error {\n  return c.String(http.StatusOK, \"Hello, World!\")\n}\n```\n\n# Official middleware repositories\n\nFollowing list of middleware is maintained by Echo team.\n\n| Repository                                                                               | Description                                                                                                                                                  |\n|------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| [github.com/labstack/echo-jwt](https://github.com/labstack/echo-jwt)                     | [JWT](https://github.com/golang-jwt/jwt) middleware                                                                                                          | \n| [github.com/labstack/echo-contrib](https://github.com/labstack/echo-contrib)             | [casbin](https://github.com/casbin/casbin), [gorilla/sessions](https://github.com/gorilla/sessions), [pprof](https://pkg.go.dev/net/http/pprof)) middlewares | \n| [github.com/labstack/echo-opentelemetry](https://github.com/labstack/echo-opentelemetry) | [OpenTelemetry](https://opentelemetry.io/) middleware for tracing and metrics                                                                                |\n| [github.com/labstack/echo-prometheus](https://github.com/labstack/echo-prometheus)       | [Prometheus](https://github.com/prometheus/client_golang/) middleware for Echo                                                                               |\n\n# Third-party middleware repositories\n\nBe careful when adding 3rd party middleware. Echo teams does not have time or manpower to guarantee safety and quality\nof middlewares in this list.\n\n| Repository                                                                                           | Description                                                                                                                                                                                              |\n|------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| [oapi-codegen/oapi-codegen](https://github.com/oapi-codegen/oapi-codegen)                            | Automatically generate RESTful API documentation with [OpenAPI](https://swagger.io/specification/) Client and Server Code Generator                                                                      |\n| [github.com/swaggo/echo-swagger](https://github.com/swaggo/echo-swagger)                             | Automatically generate RESTful API documentation with [Swagger](https://swagger.io/) 2.0.                                                                                                                |\n| [github.com/ziflex/lecho](https://github.com/ziflex/lecho)                                           | [Zerolog](https://github.com/rs/zerolog) logging library wrapper for Echo logger interface.                                                                                                              |\n| [github.com/brpaz/echozap](https://github.com/brpaz/echozap)                                         | Uber´s [Zap](https://github.com/uber-go/zap) logging library wrapper for Echo logger interface.                                                                                                          |\n| [github.com/samber/slog-echo](https://github.com/samber/slog-echo)                                   | Go [slog](https://pkg.go.dev/golang.org/x/exp/slog) logging library wrapper for Echo logger interface.                                                                                                   |\n| [github.com/darkweak/souin/plugins/echo](https://github.com/darkweak/souin/tree/master/plugins/echo) | HTTP cache system based on [Souin](https://github.com/darkweak/souin) to automatically get your endpoints cached. It supports some distributed and non-distributed storage systems depending your needs. |\n| [github.com/mikestefanello/pagoda](https://github.com/mikestefanello/pagoda)                         | Rapid, easy full-stack web development starter kit built with Echo.                                                                                                                                      |\n| [github.com/go-woo/protoc-gen-echo](https://github.com/go-woo/protoc-gen-echo)                       | ProtoBuf generate Echo server side code                                                                                                                                                                  |\n\nPlease send a PR to add your own library here.\n\n## Contribute\n\n**Use issues for everything**\n\n- For a small change, just send a PR.\n- For bigger changes open an issue for discussion before sending a PR.\n- PR should have:\n  - Test case\n  - Documentation\n  - Example (If it makes sense)\n- You can also contribute by:\n  - Reporting issues\n  - Suggesting new features or enhancements\n  - Improve/fix documentation\n\n## Credits\n\n- [Vishal Rana](https://github.com/vishr) (Author)\n- [Nitin Rana](https://github.com/nr17) (Consultant)\n- [Roland Lammel](https://github.com/lammel) (Maintainer)\n- [Martti T.](https://github.com/aldas) (Maintainer)\n- [Pablo Andres Fuente](https://github.com/pafuent) (Maintainer)\n- [Contributors](https://github.com/labstack/echo/graphs/contributors)\n\n## License\n\n[MIT](https://github.com/labstack/echo/blob/master/LICENSE)\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\n| Version   | Supported                           |\n|-----------|-------------------------------------|\n| 5.x.x     | :white_check_mark:                  |\n| >= 4.15.x | :white_check_mark: until 2026.12.31 |\n| < 4.15    | :x:                                 |\n\n## Reporting a Vulnerability\n\nhttps://github.com/labstack/echo/security/advisories/new\n\nor look for maintainers email(s) in commits and email them.\n"
  },
  {
    "path": "_fixture/_fixture/README.md",
    "content": "This directory is used for the static middleware test"
  },
  {
    "path": "_fixture/certs/README.md",
    "content": "To generate a valid certificate and private key use the following command:\n\n```bash\n# In OpenSSL ≥ 1.1.1\nopenssl req -x509 -newkey rsa:4096 -sha256 -days 9999 -nodes \\\n  -keyout key.pem -out cert.pem -subj \"/CN=localhost\" \\\n  -addext \"subjectAltName=DNS:localhost,IP:127.0.0.1,IP:::1\"\n```\n\nTo check a certificate use the following command:\n```bash\nopenssl x509 -in cert.pem -text\n```\n"
  },
  {
    "path": "_fixture/certs/cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFODCCAyCgAwIBAgIUaTvDluaMf+VJgYHQ0HFTS3yuCHYwDQYJKoZIhvcNAQEL\nBQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIxMDIyNzIxMzQ0MVoXDTQ4MDcx\nNDIxMzQ0MVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF\nAAOCAg8AMIICCgKCAgEAnqyyAAnWFH2TH7Epj5yfZxYrBvizydZe1Wo/1WpGR2IK\nQT+qIul5sEKX/ERqEOXsawSrL3fw9cuSM8Z2vD/57ZZdoSR7XIdVaMDEQenJ968a\nHObu4D27uBQwIwrM5ELgnd+fC4gis64nIu+2GSfHumZXi7lLW7DbNm8oWkMqI6tY\n2s2wx2hwGYNVJrwSn4WGnkzhQ5U5mkcsLELMx7GR0Qnv6P7sNGZVeqMU7awkcSpR\ncrKR1OUP7XCJkEq83WLHSx50+QZv7LiyDmGnujHevRbdSHlcFfHZtaufYat+qICe\nS3XADwRQe/0VSsmja6u3DAHy7VmL8PNisAdkopQZrhiI9OvGrpGZffs9zn+s/jeX\nN1bqVDihCMiEjqXMlHx2oj3AXrZTFxb7y7Ap9C07nf70lpxQWW9SjMYRF98JBiHF\neJbQkNVkmz6T8ielQbX0l46F2SGK98oyFCGNIAZBUdj5CcS1E6w/lk4t58/em0k7\n3wFC5qg0g0wfIbNSmxljBNxnaBYUqyaaAJJhpaEoOebm4RYV58hQ0FbMfpnLnSh4\ndYStsk6i1PumWoa7D45DTtxF3kH7TB3YOB5aWaNGAPQC1m4Qcd23YB5Rd/ABirSp\nux6/cFGosjSfJ/G+G0RhNUpmcbDJvFSOhD2WCuieVhCTAzp+VPIA9bSqD+InlT0C\nAwEAAaOBgTB/MB0GA1UdDgQWBBQZyM//SvzYKokQZI/0MVGb6PkH+zAfBgNVHSME\nGDAWgBQZyM//SvzYKokQZI/0MVGb6PkH+zAPBgNVHRMBAf8EBTADAQH/MCwGA1Ud\nEQQlMCOCCWxvY2FsaG9zdIcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG\n9w0BAQsFAAOCAgEAKGAJQmQ/KLw8iMb5QsyxxAonVjJ1eDAhNM3GWdHpM0/GFamO\nvVtATLQQldwDiZJvrsCQPEc8ctZ2Utvg/StLQ3+rZpsvt0+gcUlLJK61qguwYqb2\n+T7VK5s7V/OyI/tsuboOW50Pka9vQHV+Z0aM06Yu+HNDAq/UTpEOb/3MQvZd6Ooy\nPTpZtFb/+5jIQa1dIsfFWmpBxF0+wUd9GEkX3j7nekwoZfJ8Ze4GWYERZbOFpDAQ\nrIHdthH5VJztnpQJmaKqzgIOF+Rurwlp5ecSC33xNNjDaYtuf/fiWnoKGhHVSBhT\n61+0yxn3rTgh/Dsm95xY00rSX6lmcvI+kRNTUc8GGPz0ajBH6xyY7bNhfMjmnSW/\nC/XTEDbTAhT7ndWC5vvzp7ZU0TvN+WY6A0f2kxSnnrEk6QRUvRtKkjAkmAFz8exi\nttBBW0I3E5HNIC5CYRimq/9z+3clM/P1KbNblwuC65bL+PZ+nzFnn5hFaK9eLPol\nOwZQXv7IvAw8GfgLTrEUT7eBCQwe1IqesA7NTxF1BVwmNUb2XamvQZ7ly67QybRw\n0uJq80XjpVjBWYTTQy1dsnC2OTKdqGsV9TVIDR+UGfIG9cxL70pEbiSH2AX+IDCy\ni3kNIvpXgBliAyOjW6Hj1fv6dNfAat/hqEfnquWkfvcs3HNrG/InwpwNAUs=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "_fixture/certs/key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCerLIACdYUfZMf\nsSmPnJ9nFisG+LPJ1l7Vaj/VakZHYgpBP6oi6XmwQpf8RGoQ5exrBKsvd/D1y5Iz\nxna8P/ntll2hJHtch1VowMRB6cn3rxoc5u7gPbu4FDAjCszkQuCd358LiCKzrici\n77YZJ8e6ZleLuUtbsNs2byhaQyojq1jazbDHaHAZg1UmvBKfhYaeTOFDlTmaRyws\nQszHsZHRCe/o/uw0ZlV6oxTtrCRxKlFyspHU5Q/tcImQSrzdYsdLHnT5Bm/suLIO\nYae6Md69Ft1IeVwV8dm1q59hq36ogJ5LdcAPBFB7/RVKyaNrq7cMAfLtWYvw82Kw\nB2SilBmuGIj068aukZl9+z3Of6z+N5c3VupUOKEIyISOpcyUfHaiPcBetlMXFvvL\nsCn0LTud/vSWnFBZb1KMxhEX3wkGIcV4ltCQ1WSbPpPyJ6VBtfSXjoXZIYr3yjIU\nIY0gBkFR2PkJxLUTrD+WTi3nz96bSTvfAULmqDSDTB8hs1KbGWME3GdoFhSrJpoA\nkmGloSg55ubhFhXnyFDQVsx+mcudKHh1hK2yTqLU+6ZahrsPjkNO3EXeQftMHdg4\nHlpZo0YA9ALWbhBx3bdgHlF38AGKtKm7Hr9wUaiyNJ8n8b4bRGE1SmZxsMm8VI6E\nPZYK6J5WEJMDOn5U8gD1tKoP4ieVPQIDAQABAoICAEHF2CsH6MOpofi7GT08cR7s\nI33KTcxWngzc9ATk/qjMTO/rEf1Sxmx3zkR1n3nNtQhPcR5GG43nin0HwWQbKOCB\nOeJ4GuKp/o9jiHbCEEQpQyvD1jUBofSV+bYs3e2ogy8t6OGA1tGgWPy0XMlkoff0\nQEnczw3864FO5m0z9h2/Ax//r02ZTw5kUEG0KAwT709jEuVO0AfRhM/8CKKmSola\nEyaDtSmrWbdyLlSuzJRUNFrVBno3UTjdM0iqkks6jN3ojBhFwNNhY/1uIXafAXNk\nLOnD1JYMIHCb6X809VWnqvYgozIWWb5rlA3iM2mITmId1LLqMYX5fWj2R5LUzSek\nH+XG+F9FIouTaL1ACoXr0zyeY5N5YJdyXYa1tThdW+axX9ZrnPgeiQrmxzKPIyb7\nLLlVtNBQUg/t5tX80KyYjkNUu4j3oq/uBYPi0m//ovwMyi9bSbbyPT+cDXuXX5Bc\noY7wyn3evXX0c1R7vdJLZLkLu+ctVex/9hvMjeW/mMasDjLnqY7pF3Skct1SX5N2\nU8YVU9bGvFpLEwM9lmi/T7bcv+zbmGPlfTsZiFrCsixPLn7sX7y5M4L8au8O0jh0\nnHm/8rWVg1Qw0Hobg3tA8FjeMa8Sr2fYmkNLVKFzhuJLxknTJLaUbX5CymNqWP4H\nOctvfSY0nSZ1eQpBkQaJAoIBAQDTb/NhYCfaJBLXHVMy/VYd7kWGZ+I87artcE/l\n8u0pJ8XOP4kp0otFIumpHUFodysAeP6HrI79MuJB40fy91HzWZC+NrPufFFFuZ0z\nLd1o3Y5nAeoZmMlf1F12Oe3OQZy7nm9eNNkfeoVtKqDv4FhAqk+aoMor86HscKsR\nC6HlZFdGc7kX0ylrQAXPq9KLhcvUU9oAUpbqTbhYK83IebRJgFDG45HkVo9SUHpF\ndmCFSb91eZpRGpdfNLCuLiSu52TebayaUCnceeAt8SyeiChJ/TwWmRRDJS0QUv6h\ns3Wdp+cx9ANoujA4XzAs8Fld5IZ4bcG5jjwD62/tJyWrCC5DAoIBAQDAHfHjrYCK\nGHBrMj+MA7cK7fCJUn/iJLSLGgo2ANYF5oq9gaCwHCtKIyB9DN/KiY0JpJ6PWg+Q\n9Difq23YXiJjNEBS5EFTu9UwWAr1RhSAegrfHxm0sDbcAx31NtDYvBsADCWQYmzc\nKPfBshf5K4g/VCIj2VzC2CE6kNtdhqLU6AV2Pi1Tl1S82xWoAjHy91tDmlFQNWCj\nB2ZnZ7tY9zuwDfeBBOVCPHICgl5Q4PrY1KEWEXiNxgbtkNmOPAsY9WSqgOsP9pWK\nJ924gdCCvovINzZtgRisxKth6Fkhra+VCsheg9SWvgR09Deo6CCoSwYxOSb0cjh2\noyX5Rb1kJ7Z/AoIBAQCX2iNVoBV/GcFeNXV3fXLH9ESCj0FwuNC1zp/TanDhyerK\ngd8k5k2Xzcc66gP73vpHUJ6dGlVni4/r+ivGV9HHkF/f/LGlaiuEhBZel2YY1mZb\nnIhg8dZOuNqW+mvMYlsKdHNPmW0GqpwBF0iWfu1jI+4gA7Kvdj6o7RIvH8eaVEJK\nGvqoHcP1fvmteJ2yDtmhGMfMy4QPqtnmmS8l+CJ/V2SsMuyorXIpkBsAoFAZ6ilT\nWY53CT4F5nWt4v39j7pl9SatfT1TV0SmOjvtb6Rf3zu0jyR6RMzkmHa/839ZRylI\nOxPntzDCi7qxy7yjLmlVPJ6RgZGgzwqHrEHlX+65AoIBAQCEzu6d3x5B2N02LZli\neFr8MjqbI64GLiulEY5HgNJzZ8k3cjocJI0Ehj36VIEMaYRXSzbVkIO8SCgwsPiR\nn5mUDNX+t441jV62Odbxcc3Qdw226rABieOSupDmKEu92GOt57e8FV5939BOVYhf\nFunsJYQoViXbCEAIVYVgJSfBmNfVwuvgonfQyn8xErtm4/pyRGa71PqGGSKAj2Qi\n/16CuVUFGtZFsLV76JW8wZqHdI4bTF6TW3cEmaLbwcRGL7W0bMSS13rO8/pBh3QW\nPhUxhoGYt6rQHHEBkPa04nXDyZ10QRwgTSGVnBIyMK4KyTpxorm8OI2x7dzdcomX\niCCPAoIBAETwfr2JKPb/AzrKhhbZgU+sLVn3WH/nb68VheNEmGOzsqXaSHCR2NOq\n/ow7bawjc8yUIhBRzokR4F/7jGolOmfdq0MYFb6/YokssKfv1ugxBhmvOxpZ6F6E\ncERJ8Ex/ffQU053gLR/0ammddVuS1GR5I/jEdP0lJVh0xapoZNUlT5dWYCgo20hY\nZAmKpU+veyUn+5Li0pmm959vnLK5LJzEA5mpz3w1QPPtVwQs05dwmEV3CRAcCeeh\n8sXp49WNCSW4I3BxuTZzRV845SGIFhZwgVV42PTp2LPKl2p6E7Bk8xpUCCvBpALp\nQmA5yIMx+u2Jpr7fUsXEXEPTEhvjff0=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "_fixture/dist/private.txt",
    "content": "private file\n"
  },
  {
    "path": "_fixture/dist/public/assets/readme.md",
    "content": "readme in assets\n"
  },
  {
    "path": "_fixture/dist/public/assets/subfolder/subfolder.md",
    "content": "file inside subfolder\n"
  },
  {
    "path": "_fixture/dist/public/index.html",
    "content": "<h1>Hello from index</h1>\n"
  },
  {
    "path": "_fixture/dist/public/test.txt",
    "content": "test.txt contents\n"
  },
  {
    "path": "_fixture/folder/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <title>Echo</title>\n</head>\n<body>\n</body>\n</html>\n"
  },
  {
    "path": "_fixture/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <title>Echo</title>\n</head>\n<body>\n</body>\n</html>\n"
  },
  {
    "path": "bind.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"encoding\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n// Binder is the interface that wraps the Bind method.\ntype Binder interface {\n\tBind(c *Context, target any) error\n}\n\n// DefaultBinder is the default implementation of the Binder interface.\ntype DefaultBinder struct{}\n\n// BindUnmarshaler is the interface used to wrap the UnmarshalParam method.\n// Types that don't implement this, but do implement encoding.TextUnmarshaler\n// will use that interface instead.\ntype BindUnmarshaler interface {\n\t// UnmarshalParam decodes and assigns a value from an form or query param.\n\tUnmarshalParam(param string) error\n}\n\n// bindMultipleUnmarshaler is used by binder to unmarshal multiple values from request at once to\n// type implementing this interface. For example request could have multiple query fields `?a=1&a=2&b=test` in that case\n// for `a` following slice `[\"1\", \"2\"] will be passed to unmarshaller.\ntype bindMultipleUnmarshaler interface {\n\tUnmarshalParams(params []string) error\n}\n\n// BindPathValues binds path parameter values to bindable object\nfunc BindPathValues(c *Context, target any) error {\n\tparams := map[string][]string{}\n\tfor _, param := range c.PathValues() {\n\t\tparams[param.Name] = []string{param.Value}\n\t}\n\tif err := bindData(target, params, \"param\", nil); err != nil {\n\t\treturn ErrBadRequest.Wrap(err)\n\t}\n\treturn nil\n}\n\n// BindQueryParams binds query params to bindable object\nfunc BindQueryParams(c *Context, target any) error {\n\tif err := bindData(target, c.QueryParams(), \"query\", nil); err != nil {\n\t\treturn ErrBadRequest.Wrap(err)\n\t}\n\treturn nil\n}\n\n// BindBody binds request body contents to bindable object\n// NB: then binding forms take note that this implementation uses standard library form parsing\n// which parses form data from BOTH URL and BODY if content type is not MIMEMultipartForm\n// See non-MIMEMultipartForm: https://golang.org/pkg/net/http/#Request.ParseForm\n// See MIMEMultipartForm: https://golang.org/pkg/net/http/#Request.ParseMultipartForm\nfunc BindBody(c *Context, target any) (err error) {\n\treq := c.Request()\n\tif req.ContentLength == 0 {\n\t\treturn\n\t}\n\n\t// mediatype is found like `mime.ParseMediaType()` does it\n\tbase, _, _ := strings.Cut(req.Header.Get(HeaderContentType), \";\")\n\tmediatype := strings.TrimSpace(base)\n\n\tswitch mediatype {\n\tcase MIMEApplicationJSON:\n\t\tif err = c.Echo().JSONSerializer.Deserialize(c, target); err != nil {\n\t\t\tvar hErr *HTTPError\n\t\t\tif errors.As(err, &hErr) {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn ErrBadRequest.Wrap(err)\n\t\t}\n\tcase MIMEApplicationXML, MIMETextXML:\n\t\tif err = xml.NewDecoder(req.Body).Decode(target); err != nil {\n\t\t\treturn ErrBadRequest.Wrap(err)\n\t\t}\n\tcase MIMEApplicationForm:\n\t\tparams, err := c.FormValues()\n\t\tif err != nil {\n\t\t\treturn ErrBadRequest.Wrap(err)\n\t\t}\n\t\tif err = bindData(target, params, \"form\", nil); err != nil {\n\t\t\treturn ErrBadRequest.Wrap(err)\n\t\t}\n\tcase MIMEMultipartForm:\n\t\tparams, err := c.MultipartForm()\n\t\tif err != nil {\n\t\t\treturn ErrBadRequest.Wrap(err)\n\t\t}\n\t\tif err = bindData(target, params.Value, \"form\", params.File); err != nil {\n\t\t\treturn ErrBadRequest.Wrap(err)\n\t\t}\n\tdefault:\n\t\treturn &HTTPError{Code: http.StatusUnsupportedMediaType}\n\t}\n\treturn nil\n}\n\n// BindHeaders binds HTTP headers to a bindable object\nfunc BindHeaders(c *Context, target any) error {\n\tif err := bindData(target, c.Request().Header, \"header\", nil); err != nil {\n\t\treturn ErrBadRequest.Wrap(err)\n\t}\n\treturn nil\n}\n\n// Bind implements the `Binder#Bind` function.\n// Binding is done in following order: 1) path params; 2) query params; 3) request body. Each step COULD override previous\n// step bound values. For single source binding use their own methods BindBody, BindQueryParams, BindPathValues.\nfunc (b *DefaultBinder) Bind(c *Context, target any) error {\n\tif err := BindPathValues(c, target); err != nil {\n\t\treturn err\n\t}\n\t// Only bind query parameters for GET/DELETE/HEAD to avoid unexpected behavior with destination struct binding from body.\n\t// For example a request URL `&id=1&lang=en` with body `{\"id\":100,\"lang\":\"de\"}` would lead to precedence issues.\n\t// The HTTP method check restores pre-v4.1.11 behavior to avoid these problems (see issue #1670)\n\tmethod := c.Request().Method\n\tif method == http.MethodGet || method == http.MethodDelete || method == http.MethodHead {\n\t\tif err := BindQueryParams(c, target); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn BindBody(c, target)\n}\n\n// bindData will bind data ONLY fields in destination struct that have EXPLICIT tag\nfunc bindData(destination any, data map[string][]string, tag string, dataFiles map[string][]*multipart.FileHeader) error {\n\tif destination == nil || (len(data) == 0 && len(dataFiles) == 0) {\n\t\treturn nil\n\t}\n\thasFiles := len(dataFiles) > 0\n\ttyp := reflect.TypeOf(destination).Elem()\n\tval := reflect.ValueOf(destination).Elem()\n\n\t// Support binding to limited Map destinations:\n\t// - map[string][]string,\n\t// - map[string]string <-- (binds first value from data slice)\n\t// - map[string]any\n\t// You are better off binding to struct but there are user who want this map feature. Source of data for these cases are:\n\t// params,query,header,form as these sources produce string values, most of the time slice of strings, actually.\n\tif typ.Kind() == reflect.Map && typ.Key().Kind() == reflect.String {\n\t\tk := typ.Elem().Kind()\n\t\tisElemInterface := k == reflect.Interface\n\t\tisElemString := k == reflect.String\n\t\tisElemSliceOfStrings := k == reflect.Slice && typ.Elem().Elem().Kind() == reflect.String\n\t\tif !(isElemSliceOfStrings || isElemString || isElemInterface) {\n\t\t\treturn nil\n\t\t}\n\t\tif val.IsNil() {\n\t\t\tval.Set(reflect.MakeMap(typ))\n\t\t}\n\t\tfor k, v := range data {\n\t\t\tif isElemString {\n\t\t\t\tval.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v[0]))\n\t\t\t} else if isElemInterface {\n\t\t\t\t// To maintain backward compatibility, we always bind to the first string value\n\t\t\t\t// and not the slice of strings when dealing with map[string]any{}\n\t\t\t\tval.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v[0]))\n\t\t\t} else {\n\t\t\t\tval.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v))\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\t// !struct\n\tif typ.Kind() != reflect.Struct {\n\t\tif tag == \"param\" || tag == \"query\" || tag == \"header\" {\n\t\t\t// incompatible type, data is probably to be found in the body\n\t\t\treturn nil\n\t\t}\n\t\treturn errors.New(\"binding element must be a struct\")\n\t}\n\n\tfor i := 0; i < typ.NumField(); i++ { // iterate over all destination fields\n\t\ttypeField := typ.Field(i)\n\t\tstructField := val.Field(i)\n\t\tif typeField.Anonymous {\n\t\t\tif structField.Kind() == reflect.Ptr {\n\t\t\t\tstructField = structField.Elem()\n\t\t\t}\n\t\t}\n\t\tif !structField.CanSet() {\n\t\t\tcontinue\n\t\t}\n\t\tstructFieldKind := structField.Kind()\n\t\tinputFieldName := typeField.Tag.Get(tag)\n\t\tif typeField.Anonymous && structFieldKind == reflect.Struct && inputFieldName != \"\" {\n\t\t\t// if anonymous struct with query/param/form tags, report an error\n\t\t\treturn errors.New(\"query/param/form tags are not allowed with anonymous struct field\")\n\t\t}\n\n\t\tif inputFieldName == \"\" {\n\t\t\t// If tag is nil, we inspect if the field is a not BindUnmarshaler struct and try to bind data into it (might contain fields with tags).\n\t\t\t// structs that implement BindUnmarshaler are bound only when they have explicit tag\n\t\t\tif _, ok := structField.Addr().Interface().(BindUnmarshaler); !ok && structFieldKind == reflect.Struct {\n\t\t\t\tif err := bindData(structField.Addr().Interface(), data, tag, dataFiles); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\t// does not have explicit tag and is not an ordinary struct - so move to next field\n\t\t\tcontinue\n\t\t}\n\n\t\tif hasFiles {\n\t\t\tif ok, err := isFieldMultipartFile(structField.Type()); err != nil {\n\t\t\t\treturn err\n\t\t\t} else if ok {\n\t\t\t\tif ok := setMultipartFileHeaderTypes(structField, inputFieldName, dataFiles); ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tinputValue, exists := data[inputFieldName]\n\t\tif !exists {\n\t\t\t// Go json.Unmarshal supports case-insensitive binding.  However the\n\t\t\t// url params are bound case-sensitive which is inconsistent.  To\n\t\t\t// fix this we must check all of the map values in a\n\t\t\t// case-insensitive search.\n\t\t\tfor k, v := range data {\n\t\t\t\tif strings.EqualFold(k, inputFieldName) {\n\t\t\t\t\tinputValue = v\n\t\t\t\t\texists = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif !exists {\n\t\t\tcontinue\n\t\t}\n\n\t\t// NOTE: algorithm here is not particularly sophisticated. It probably does not work with absurd types like `**[]*int`\n\t\t// but it is smart enough to handle niche cases like `*int`,`*[]string`,`[]*int` .\n\n\t\t// try unmarshalling first, in case we're dealing with an alias to an array type\n\t\tif ok, err := unmarshalInputsToField(typeField.Type.Kind(), inputValue, structField); ok {\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tformatTag := typeField.Tag.Get(\"format\")\n\t\tif ok, err := unmarshalInputToField(typeField.Type.Kind(), inputValue[0], structField, formatTag); ok {\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\t// we could be dealing with pointer to slice `*[]string` so dereference it. There are weird OpenAPI generators\n\t\t// that could create struct fields like that.\n\t\tif structFieldKind == reflect.Pointer {\n\t\t\tstructFieldKind = structField.Elem().Kind()\n\t\t\tstructField = structField.Elem()\n\t\t}\n\n\t\tif structFieldKind == reflect.Slice {\n\t\t\tsliceOf := structField.Type().Elem().Kind()\n\t\t\tnumElems := len(inputValue)\n\t\t\tslice := reflect.MakeSlice(structField.Type(), numElems, numElems)\n\t\t\tfor j := 0; j < numElems; j++ {\n\t\t\t\tif err := setWithProperType(sliceOf, inputValue[j], slice.Index(j)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\tstructField.Set(slice)\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := setWithProperType(structFieldKind, inputValue[0], structField); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error {\n\t// But also call it here, in case we're dealing with an array of BindUnmarshalers\n\t// Note: format tag not available in this context, so empty string is passed\n\tif ok, err := unmarshalInputToField(valueKind, val, structField, \"\"); ok {\n\t\treturn err\n\t}\n\n\tswitch valueKind {\n\tcase reflect.Ptr:\n\t\treturn setWithProperType(structField.Elem().Kind(), val, structField.Elem())\n\tcase reflect.Int:\n\t\treturn setIntField(val, 0, structField)\n\tcase reflect.Int8:\n\t\treturn setIntField(val, 8, structField)\n\tcase reflect.Int16:\n\t\treturn setIntField(val, 16, structField)\n\tcase reflect.Int32:\n\t\treturn setIntField(val, 32, structField)\n\tcase reflect.Int64:\n\t\treturn setIntField(val, 64, structField)\n\tcase reflect.Uint:\n\t\treturn setUintField(val, 0, structField)\n\tcase reflect.Uint8:\n\t\treturn setUintField(val, 8, structField)\n\tcase reflect.Uint16:\n\t\treturn setUintField(val, 16, structField)\n\tcase reflect.Uint32:\n\t\treturn setUintField(val, 32, structField)\n\tcase reflect.Uint64:\n\t\treturn setUintField(val, 64, structField)\n\tcase reflect.Bool:\n\t\treturn setBoolField(val, structField)\n\tcase reflect.Float32:\n\t\treturn setFloatField(val, 32, structField)\n\tcase reflect.Float64:\n\t\treturn setFloatField(val, 64, structField)\n\tcase reflect.String:\n\t\tstructField.SetString(val)\n\tdefault:\n\t\treturn errors.New(\"unknown type\")\n\t}\n\treturn nil\n}\n\nfunc unmarshalInputsToField(valueKind reflect.Kind, values []string, field reflect.Value) (bool, error) {\n\tif valueKind == reflect.Ptr {\n\t\tif field.IsNil() {\n\t\t\tfield.Set(reflect.New(field.Type().Elem()))\n\t\t}\n\t\tfield = field.Elem()\n\t}\n\n\tfieldIValue := field.Addr().Interface()\n\tunmarshaler, ok := fieldIValue.(bindMultipleUnmarshaler)\n\tif !ok {\n\t\treturn false, nil\n\t}\n\treturn true, unmarshaler.UnmarshalParams(values)\n}\n\nfunc unmarshalInputToField(valueKind reflect.Kind, val string, field reflect.Value, formatTag string) (bool, error) {\n\tif valueKind == reflect.Ptr {\n\t\tif field.IsNil() {\n\t\t\tfield.Set(reflect.New(field.Type().Elem()))\n\t\t}\n\t\tfield = field.Elem()\n\t}\n\n\tfieldIValue := field.Addr().Interface()\n\t// Handle time.Time with custom format tag\n\tif formatTag != \"\" {\n\t\tif _, isTime := fieldIValue.(*time.Time); isTime {\n\t\t\tt, err := time.Parse(formatTag, val)\n\t\t\tif err != nil {\n\t\t\t\treturn true, err\n\t\t\t}\n\t\t\tfield.Set(reflect.ValueOf(t))\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\tswitch unmarshaler := fieldIValue.(type) {\n\tcase BindUnmarshaler:\n\t\treturn true, unmarshaler.UnmarshalParam(val)\n\tcase encoding.TextUnmarshaler:\n\t\treturn true, unmarshaler.UnmarshalText([]byte(val))\n\t}\n\n\treturn false, nil\n}\n\nfunc setIntField(value string, bitSize int, field reflect.Value) error {\n\tif value == \"\" {\n\t\tvalue = \"0\"\n\t}\n\tintVal, err := strconv.ParseInt(value, 10, bitSize)\n\tif err == nil {\n\t\tfield.SetInt(intVal)\n\t}\n\treturn err\n}\n\nfunc setUintField(value string, bitSize int, field reflect.Value) error {\n\tif value == \"\" {\n\t\tvalue = \"0\"\n\t}\n\tuintVal, err := strconv.ParseUint(value, 10, bitSize)\n\tif err == nil {\n\t\tfield.SetUint(uintVal)\n\t}\n\treturn err\n}\n\nfunc setBoolField(value string, field reflect.Value) error {\n\tif value == \"\" {\n\t\tvalue = \"false\"\n\t}\n\tboolVal, err := strconv.ParseBool(value)\n\tif err == nil {\n\t\tfield.SetBool(boolVal)\n\t}\n\treturn err\n}\n\nfunc setFloatField(value string, bitSize int, field reflect.Value) error {\n\tif value == \"\" {\n\t\tvalue = \"0.0\"\n\t}\n\tfloatVal, err := strconv.ParseFloat(value, bitSize)\n\tif err == nil {\n\t\tfield.SetFloat(floatVal)\n\t}\n\treturn err\n}\n\nvar (\n\t// NOT supported by bind as you can NOT check easily empty struct being actual file or not\n\tmultipartFileHeaderType = reflect.TypeFor[multipart.FileHeader]()\n\t// supported by bind as you can check by nil value if file existed or not\n\tmultipartFileHeaderPointerType      = reflect.TypeFor[*multipart.FileHeader]()\n\tmultipartFileHeaderSliceType        = reflect.TypeFor[[]multipart.FileHeader]()\n\tmultipartFileHeaderPointerSliceType = reflect.TypeFor[[]*multipart.FileHeader]()\n)\n\nfunc isFieldMultipartFile(field reflect.Type) (bool, error) {\n\tswitch field {\n\tcase multipartFileHeaderPointerType,\n\t\tmultipartFileHeaderSliceType,\n\t\tmultipartFileHeaderPointerSliceType:\n\t\treturn true, nil\n\tcase multipartFileHeaderType:\n\t\treturn true, errors.New(\"binding to multipart.FileHeader struct is not supported, use pointer to struct\")\n\tdefault:\n\t\treturn false, nil\n\t}\n}\n\nfunc setMultipartFileHeaderTypes(structField reflect.Value, inputFieldName string, files map[string][]*multipart.FileHeader) bool {\n\tfileHeaders := files[inputFieldName]\n\tif len(fileHeaders) == 0 {\n\t\treturn false\n\t}\n\n\tresult := true\n\tswitch structField.Type() {\n\tcase multipartFileHeaderPointerSliceType:\n\t\tstructField.Set(reflect.ValueOf(fileHeaders))\n\tcase multipartFileHeaderSliceType:\n\t\theaders := make([]multipart.FileHeader, len(fileHeaders))\n\t\tfor i, fileHeader := range fileHeaders {\n\t\t\theaders[i] = *fileHeader\n\t\t}\n\t\tstructField.Set(reflect.ValueOf(headers))\n\tcase multipartFileHeaderPointerType:\n\t\tstructField.Set(reflect.ValueOf(fileHeaders[0]))\n\tdefault:\n\t\tresult = false\n\t}\n\n\treturn result\n}\n"
  },
  {
    "path": "bind_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/http/httputil\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype bindTestStruct struct {\n\tT           Timestamp\n\tGoT         time.Time\n\tPtrI16      *int16\n\tPtrUI       *uint\n\tTptr        *Timestamp\n\tPtrF32      *float32\n\tPtrB        *bool\n\tPtrI32      *int32\n\tGoTptr      *time.Time\n\tPtrI64      *int64\n\tPtrI        *int\n\tPtrI8       *int8\n\tPtrF64      *float64\n\tPtrUI8      *uint8\n\tPtrUI64     *uint64\n\tPtrUI16     *uint16\n\tPtrS        *string\n\tPtrUI32     *uint32\n\tS           string\n\tcantSet     string\n\tDoesntExist string\n\tSA          StringArray\n\tF64         float64\n\tI           int\n\tUI64        uint64\n\tUI          uint\n\tI64         int64\n\tF32         float32\n\tUI32        uint32\n\tI32         int32\n\tUI16        uint16\n\tI16         int16\n\tB           bool\n\tUI8         uint8\n\tI8          int8\n}\n\ntype bindTestStructWithTags struct {\n\tT           Timestamp  `json:\"T\" form:\"T\"`\n\tGoT         time.Time  `json:\"GoT\" form:\"GoT\"`\n\tPtrI16      *int16     `json:\"PtrI16\" form:\"PtrI16\"`\n\tPtrUI       *uint      `json:\"PtrUI\" form:\"PtrUI\"`\n\tTptr        *Timestamp `json:\"Tptr\" form:\"Tptr\"`\n\tPtrF32      *float32   `json:\"PtrF32\" form:\"PtrF32\"`\n\tPtrB        *bool      `json:\"PtrB\" form:\"PtrB\"`\n\tPtrI32      *int32     `json:\"PtrI32\" form:\"PtrI32\"`\n\tGoTptr      *time.Time `json:\"GoTptr\" form:\"GoTptr\"`\n\tPtrI64      *int64     `json:\"PtrI64\" form:\"PtrI64\"`\n\tPtrI        *int       `json:\"PtrI\" form:\"PtrI\"`\n\tPtrI8       *int8      `json:\"PtrI8\" form:\"PtrI8\"`\n\tPtrF64      *float64   `json:\"PtrF64\" form:\"PtrF64\"`\n\tPtrUI8      *uint8     `json:\"PtrUI8\" form:\"PtrUI8\"`\n\tPtrUI64     *uint64    `json:\"PtrUI64\" form:\"PtrUI64\"`\n\tPtrUI16     *uint16    `json:\"PtrUI16\" form:\"PtrUI16\"`\n\tPtrS        *string    `json:\"PtrS\" form:\"PtrS\"`\n\tPtrUI32     *uint32    `json:\"PtrUI32\" form:\"PtrUI32\"`\n\tS           string     `json:\"S\" form:\"S\"`\n\tcantSet     string\n\tDoesntExist string      `json:\"DoesntExist\" form:\"DoesntExist\"`\n\tSA          StringArray `json:\"SA\" form:\"SA\"`\n\tF64         float64     `json:\"F64\" form:\"F64\"`\n\tI           int         `json:\"I\" form:\"I\"`\n\tUI64        uint64      `json:\"UI64\" form:\"UI64\"`\n\tUI          uint        `json:\"UI\" form:\"UI\"`\n\tI64         int64       `json:\"I64\" form:\"I64\"`\n\tF32         float32     `json:\"F32\" form:\"F32\"`\n\tUI32        uint32      `json:\"UI32\" form:\"UI32\"`\n\tI32         int32       `json:\"I32\" form:\"I32\"`\n\tUI16        uint16      `json:\"UI16\" form:\"UI16\"`\n\tI16         int16       `json:\"I16\" form:\"I16\"`\n\tB           bool        `json:\"B\" form:\"B\"`\n\tUI8         uint8       `json:\"UI8\" form:\"UI8\"`\n\tI8          int8        `json:\"I8\" form:\"I8\"`\n}\n\ntype Timestamp time.Time\ntype TA []Timestamp\ntype StringArray []string\ntype Struct struct {\n\tFoo string\n}\ntype Bar struct {\n\tBaz int `json:\"baz\" query:\"baz\"`\n}\n\nfunc (t *Timestamp) UnmarshalParam(src string) error {\n\tts, err := time.Parse(time.RFC3339, src)\n\t*t = Timestamp(ts)\n\treturn err\n}\n\nfunc (a *StringArray) UnmarshalParam(src string) error {\n\t*a = StringArray(strings.Split(src, \",\"))\n\treturn nil\n}\n\nfunc (s *Struct) UnmarshalParam(src string) error {\n\t*s = Struct{\n\t\tFoo: src,\n\t}\n\treturn nil\n}\n\nfunc (t bindTestStruct) GetCantSet() string {\n\treturn t.cantSet\n}\n\nvar values = map[string][]string{\n\t\"I\":       {\"0\"},\n\t\"PtrI\":    {\"0\"},\n\t\"I8\":      {\"8\"},\n\t\"PtrI8\":   {\"8\"},\n\t\"I16\":     {\"16\"},\n\t\"PtrI16\":  {\"16\"},\n\t\"I32\":     {\"32\"},\n\t\"PtrI32\":  {\"32\"},\n\t\"I64\":     {\"64\"},\n\t\"PtrI64\":  {\"64\"},\n\t\"UI\":      {\"0\"},\n\t\"PtrUI\":   {\"0\"},\n\t\"UI8\":     {\"8\"},\n\t\"PtrUI8\":  {\"8\"},\n\t\"UI16\":    {\"16\"},\n\t\"PtrUI16\": {\"16\"},\n\t\"UI32\":    {\"32\"},\n\t\"PtrUI32\": {\"32\"},\n\t\"UI64\":    {\"64\"},\n\t\"PtrUI64\": {\"64\"},\n\t\"B\":       {\"true\"},\n\t\"PtrB\":    {\"true\"},\n\t\"F32\":     {\"32.5\"},\n\t\"PtrF32\":  {\"32.5\"},\n\t\"F64\":     {\"64.5\"},\n\t\"PtrF64\":  {\"64.5\"},\n\t\"S\":       {\"test\"},\n\t\"PtrS\":    {\"test\"},\n\t\"cantSet\": {\"test\"},\n\t\"T\":       {\"2016-12-06T19:09:05+01:00\"},\n\t\"Tptr\":    {\"2016-12-06T19:09:05+01:00\"},\n\t\"GoT\":     {\"2016-12-06T19:09:05+01:00\"},\n\t\"GoTptr\":  {\"2016-12-06T19:09:05+01:00\"},\n\t\"ST\":      {\"bar\"},\n}\n\n// ptr return pointer to value. This is useful as `v := []*int8{&int8(1)}` will not compile\nfunc ptr[T any](value T) *T {\n\treturn &value\n}\n\nfunc TestToMultipleFields(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, \"/?id=1&ID=2\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\ttype Root struct {\n\t\tID     int64 `query:\"id\"`\n\t\tChild2 struct {\n\t\t\tID int64\n\t\t}\n\t\tChild1 struct {\n\t\t\tID int64 `query:\"id\"`\n\t\t}\n\t}\n\n\tu := new(Root)\n\terr := c.Bind(u)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, int64(1), u.ID)        // perfectly reasonable\n\t\tassert.Equal(t, int64(1), u.Child1.ID) // untagged struct containing tagged field gets filled (by tag)\n\t\tassert.Equal(t, int64(0), u.Child2.ID) // untagged struct containing untagged field should not be bind\n\t}\n}\n\nfunc TestBindJSON(t *testing.T) {\n\ttestBindOkay(t, strings.NewReader(userJSON), nil, MIMEApplicationJSON)\n\ttestBindOkay(t, strings.NewReader(userJSON), dummyQuery, MIMEApplicationJSON)\n\ttestBindArrayOkay(t, strings.NewReader(usersJSON), nil, MIMEApplicationJSON)\n\ttestBindArrayOkay(t, strings.NewReader(usersJSON), dummyQuery, MIMEApplicationJSON)\n\ttestBindError(t, strings.NewReader(invalidContent), MIMEApplicationJSON, &json.SyntaxError{})\n\ttestBindError(t, strings.NewReader(userJSONInvalidType), MIMEApplicationJSON, &json.UnmarshalTypeError{})\n}\n\nfunc TestBindXML(t *testing.T) {\n\ttestBindOkay(t, strings.NewReader(userXML), nil, MIMEApplicationXML)\n\ttestBindOkay(t, strings.NewReader(userXML), dummyQuery, MIMEApplicationXML)\n\ttestBindArrayOkay(t, strings.NewReader(userXML), nil, MIMEApplicationXML)\n\ttestBindArrayOkay(t, strings.NewReader(userXML), dummyQuery, MIMEApplicationXML)\n\ttestBindError(t, strings.NewReader(invalidContent), MIMEApplicationXML, errors.New(\"\"))\n\ttestBindError(t, strings.NewReader(userXMLConvertNumberError), MIMEApplicationXML, &strconv.NumError{})\n\ttestBindError(t, strings.NewReader(userXMLUnsupportedTypeError), MIMEApplicationXML, &xml.SyntaxError{})\n\ttestBindOkay(t, strings.NewReader(userXML), nil, MIMETextXML)\n\ttestBindOkay(t, strings.NewReader(userXML), dummyQuery, MIMETextXML)\n\ttestBindError(t, strings.NewReader(invalidContent), MIMETextXML, errors.New(\"\"))\n\ttestBindError(t, strings.NewReader(userXMLConvertNumberError), MIMETextXML, &strconv.NumError{})\n\ttestBindError(t, strings.NewReader(userXMLUnsupportedTypeError), MIMETextXML, &xml.SyntaxError{})\n}\n\nfunc TestBindForm(t *testing.T) {\n\n\ttestBindOkay(t, strings.NewReader(userForm), nil, MIMEApplicationForm)\n\ttestBindOkay(t, strings.NewReader(userForm), dummyQuery, MIMEApplicationForm)\n\te := New()\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(userForm))\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\treq.Header.Set(HeaderContentType, MIMEApplicationForm)\n\terr := c.Bind(&[]struct{ Field string }{})\n\tassert.Error(t, err)\n}\n\nfunc TestBindQueryParams(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, \"/?id=1&name=Jon+Snow\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tu := new(user)\n\terr := c.Bind(u)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, 1, u.ID)\n\t\tassert.Equal(t, \"Jon Snow\", u.Name)\n\t}\n}\n\nfunc TestBindQueryParamsCaseInsensitive(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, \"/?ID=1&NAME=Jon+Snow\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tu := new(user)\n\terr := c.Bind(u)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, 1, u.ID)\n\t\tassert.Equal(t, \"Jon Snow\", u.Name)\n\t}\n}\n\nfunc TestBindQueryParamsCaseSensitivePrioritized(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, \"/?id=1&ID=2&NAME=Jon+Snow&name=Jon+Doe\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tu := new(user)\n\terr := c.Bind(u)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, 1, u.ID)\n\t\tassert.Equal(t, \"Jon Doe\", u.Name)\n\t}\n}\n\nfunc TestBindHeaderParam(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Set(\"Name\", \"Jon Doe\")\n\treq.Header.Set(\"Id\", \"2\")\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tu := new(user)\n\terr := BindHeaders(c, u)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, 2, u.ID)\n\t\tassert.Equal(t, \"Jon Doe\", u.Name)\n\t}\n}\n\nfunc TestBindHeaderParamBadType(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Set(\"Id\", \"salamander\")\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tu := new(user)\n\terr := BindHeaders(c, u)\n\tassert.Error(t, err)\n\n\thttpErr, ok := err.(*HTTPError)\n\tif assert.True(t, ok) {\n\t\tassert.Equal(t, http.StatusBadRequest, httpErr.Code)\n\t}\n}\n\nfunc TestBindUnmarshalParam(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, \"/?ts=2016-12-06T19:09:05Z&sa=one,two,three&ta=2016-12-06T19:09:05Z&ta=2016-12-06T19:09:05Z&ST=baz\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tresult := struct {\n\t\tT         Timestamp `query:\"ts\"`\n\t\tST        Struct\n\t\tStWithTag struct {\n\t\t\tFoo string `query:\"st\"`\n\t\t}\n\t\tTA []Timestamp `query:\"ta\"`\n\t\tSA StringArray `query:\"sa\"`\n\t}{}\n\terr := c.Bind(&result)\n\tts := Timestamp(time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC))\n\n\tif assert.NoError(t, err) {\n\t\t//\t\tassert.Equal( Timestamp(reflect.TypeOf(&Timestamp{}), time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)), result.T)\n\t\tassert.Equal(t, ts, result.T)\n\t\tassert.Equal(t, StringArray([]string{\"one\", \"two\", \"three\"}), result.SA)\n\t\tassert.Equal(t, []Timestamp{ts, ts}, result.TA)\n\t\tassert.Equal(t, Struct{\"\"}, result.ST)       // child struct does not have a field with matching tag\n\t\tassert.Equal(t, \"baz\", result.StWithTag.Foo) // child struct has field with matching tag\n\t}\n}\n\nfunc TestBindUnmarshalText(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, \"/?ts=2016-12-06T19:09:05Z&sa=one,two,three&ta=2016-12-06T19:09:05Z&ta=2016-12-06T19:09:05Z&ST=baz\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tresult := struct {\n\t\tT  time.Time `query:\"ts\"`\n\t\tST Struct\n\t\tTA []time.Time `query:\"ta\"`\n\t\tSA StringArray `query:\"sa\"`\n\t}{}\n\terr := c.Bind(&result)\n\tts := time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)\n\tif assert.NoError(t, err) {\n\t\t//\t\tassert.Equal(t, Timestamp(reflect.TypeOf(&Timestamp{}), time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)), result.T)\n\t\tassert.Equal(t, ts, result.T)\n\t\tassert.Equal(t, StringArray([]string{\"one\", \"two\", \"three\"}), result.SA)\n\t\tassert.Equal(t, []time.Time{ts, ts}, result.TA)\n\t\tassert.Equal(t, Struct{\"\"}, result.ST) // field in child struct does not have tag\n\t}\n}\n\nfunc TestBindUnmarshalParamPtr(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, \"/?ts=2016-12-06T19:09:05Z\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tresult := struct {\n\t\tTptr *Timestamp `query:\"ts\"`\n\t}{}\n\terr := c.Bind(&result)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, Timestamp(time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)), *result.Tptr)\n\t}\n}\n\nfunc TestBindUnmarshalParamAnonymousFieldPtr(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, \"/?baz=1\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tresult := struct {\n\t\t*Bar\n\t}{&Bar{}}\n\terr := c.Bind(&result)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, 1, result.Baz)\n\t}\n}\n\nfunc TestBindUnmarshalParamAnonymousFieldPtrNil(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, \"/?baz=1\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tresult := struct {\n\t\t*Bar\n\t}{}\n\terr := c.Bind(&result)\n\tif assert.NoError(t, err) {\n\t\tassert.Nil(t, result.Bar)\n\t}\n}\n\nfunc TestBindUnmarshalParamAnonymousFieldPtrCustomTag(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, `/?bar={\"baz\":100}&baz=1`, nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tresult := struct {\n\t\t*Bar `json:\"bar\" query:\"bar\"`\n\t}{&Bar{}}\n\terr := c.Bind(&result)\n\tassert.Contains(t, err.Error(), \"query/param/form tags are not allowed with anonymous struct field\")\n}\n\nfunc TestBindUnmarshalTextPtr(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, \"/?ts=2016-12-06T19:09:05Z\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tresult := struct {\n\t\tTptr *time.Time `query:\"ts\"`\n\t}{}\n\terr := c.Bind(&result)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC), *result.Tptr)\n\t}\n}\n\nfunc TestBindMultipartForm(t *testing.T) {\n\tbodyBuffer := new(bytes.Buffer)\n\tmw := multipart.NewWriter(bodyBuffer)\n\tmw.WriteField(\"id\", \"1\")\n\tmw.WriteField(\"name\", \"Jon Snow\")\n\tmw.Close()\n\tbody := bodyBuffer.Bytes()\n\n\ttestBindOkay(t, bytes.NewReader(body), nil, mw.FormDataContentType())\n\ttestBindOkay(t, bytes.NewReader(body), dummyQuery, mw.FormDataContentType())\n}\n\nfunc TestBindUnsupportedMediaType(t *testing.T) {\n\ttestBindError(t, strings.NewReader(invalidContent), MIMEApplicationJSON, &json.SyntaxError{})\n}\n\nfunc TestDefaultBinder_bindDataToMap(t *testing.T) {\n\texampleData := map[string][]string{\n\t\t\"multiple\": {\"1\", \"2\"},\n\t\t\"single\":   {\"3\"},\n\t}\n\n\tt.Run(\"ok, bind to map[string]string\", func(t *testing.T) {\n\t\tdest := map[string]string{}\n\t\tassert.NoError(t, bindData(&dest, exampleData, \"param\", nil))\n\t\tassert.Equal(t,\n\t\t\tmap[string]string{\n\t\t\t\t\"multiple\": \"1\",\n\t\t\t\t\"single\":   \"3\",\n\t\t\t},\n\t\t\tdest,\n\t\t)\n\t})\n\n\tt.Run(\"ok, bind to map[string]string with nil map\", func(t *testing.T) {\n\t\tvar dest map[string]string\n\t\tassert.NoError(t, bindData(&dest, exampleData, \"param\", nil))\n\t\tassert.Equal(t,\n\t\t\tmap[string]string{\n\t\t\t\t\"multiple\": \"1\",\n\t\t\t\t\"single\":   \"3\",\n\t\t\t},\n\t\t\tdest,\n\t\t)\n\t})\n\n\tt.Run(\"ok, bind to map[string][]string\", func(t *testing.T) {\n\t\tdest := map[string][]string{}\n\t\tassert.NoError(t, bindData(&dest, exampleData, \"param\", nil))\n\t\tassert.Equal(t,\n\t\t\tmap[string][]string{\n\t\t\t\t\"multiple\": {\"1\", \"2\"},\n\t\t\t\t\"single\":   {\"3\"},\n\t\t\t},\n\t\t\tdest,\n\t\t)\n\t})\n\n\tt.Run(\"ok, bind to map[string][]string with nil map\", func(t *testing.T) {\n\t\tvar dest map[string][]string\n\t\tassert.NoError(t, bindData(&dest, exampleData, \"param\", nil))\n\t\tassert.Equal(t,\n\t\t\tmap[string][]string{\n\t\t\t\t\"multiple\": {\"1\", \"2\"},\n\t\t\t\t\"single\":   {\"3\"},\n\t\t\t},\n\t\t\tdest,\n\t\t)\n\t})\n\n\tt.Run(\"ok, bind to map[string]interface\", func(t *testing.T) {\n\t\tdest := map[string]any{}\n\t\tassert.NoError(t, bindData(&dest, exampleData, \"param\", nil))\n\t\tassert.Equal(t,\n\t\t\tmap[string]any{\n\t\t\t\t\"multiple\": \"1\",\n\t\t\t\t\"single\":   \"3\",\n\t\t\t},\n\t\t\tdest,\n\t\t)\n\t})\n\n\tt.Run(\"ok, bind to map[string]interface with nil map\", func(t *testing.T) {\n\t\tvar dest map[string]any\n\t\tassert.NoError(t, bindData(&dest, exampleData, \"param\", nil))\n\t\tassert.Equal(t,\n\t\t\tmap[string]any{\n\t\t\t\t\"multiple\": \"1\",\n\t\t\t\t\"single\":   \"3\",\n\t\t\t},\n\t\t\tdest,\n\t\t)\n\t})\n\n\tt.Run(\"ok, bind to map[string]int skips\", func(t *testing.T) {\n\t\tdest := map[string]int{}\n\t\tassert.NoError(t, bindData(&dest, exampleData, \"param\", nil))\n\t\tassert.Equal(t, map[string]int{}, dest)\n\t})\n\n\tt.Run(\"ok, bind to map[string]int skips with nil map\", func(t *testing.T) {\n\t\tvar dest map[string]int\n\t\tassert.NoError(t, bindData(&dest, exampleData, \"param\", nil))\n\t\tassert.Equal(t, map[string]int(nil), dest)\n\t})\n\n\tt.Run(\"ok, bind to map[string][]int skips\", func(t *testing.T) {\n\t\tdest := map[string][]int{}\n\t\tassert.NoError(t, bindData(&dest, exampleData, \"param\", nil))\n\t\tassert.Equal(t, map[string][]int{}, dest)\n\t})\n\n\tt.Run(\"ok, bind to map[string][]int skips with nil map\", func(t *testing.T) {\n\t\tvar dest map[string][]int\n\t\tassert.NoError(t, bindData(&dest, exampleData, \"param\", nil))\n\t\tassert.Equal(t, map[string][]int(nil), dest)\n\t})\n}\n\nfunc TestBindbindData(t *testing.T) {\n\tts := new(bindTestStruct)\n\terr := bindData(ts, values, \"form\", nil)\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, 0, ts.I)\n\tassert.Equal(t, int8(0), ts.I8)\n\tassert.Equal(t, int16(0), ts.I16)\n\tassert.Equal(t, int32(0), ts.I32)\n\tassert.Equal(t, int64(0), ts.I64)\n\tassert.Equal(t, uint(0), ts.UI)\n\tassert.Equal(t, uint8(0), ts.UI8)\n\tassert.Equal(t, uint16(0), ts.UI16)\n\tassert.Equal(t, uint32(0), ts.UI32)\n\tassert.Equal(t, uint64(0), ts.UI64)\n\tassert.Equal(t, false, ts.B)\n\tassert.Equal(t, float32(0), ts.F32)\n\tassert.Equal(t, float64(0), ts.F64)\n\tassert.Equal(t, \"\", ts.S)\n\tassert.Equal(t, \"\", ts.cantSet)\n}\n\nfunc TestBindParam(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tc.InitializeRoute(\n\t\t&RouteInfo{Path: \"/users/:id/:name\"},\n\t\t&PathValues{\n\t\t\t{Name: \"id\", Value: \"1\"},\n\t\t\t{Name: \"name\", Value: \"Jon Snow\"},\n\t\t},\n\t)\n\n\tu := new(user)\n\terr := c.Bind(u)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, 1, u.ID)\n\t\tassert.Equal(t, \"Jon Snow\", u.Name)\n\t}\n\n\t// Second test for the absence of a param\n\tc2 := e.NewContext(req, rec)\n\tc2.InitializeRoute(\n\t\t&RouteInfo{Path: \"/users/:id\"},\n\t\t&PathValues{\n\t\t\t{Name: \"id\", Value: \"1\"},\n\t\t},\n\t)\n\n\tu = new(user)\n\terr = c2.Bind(u)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, 1, u.ID)\n\t\tassert.Equal(t, \"\", u.Name)\n\t}\n\n\t// Bind something with param and post data payload\n\tbody := bytes.NewBufferString(`{ \"name\": \"Jon Snow\" }`)\n\te2 := New()\n\treq2 := httptest.NewRequest(http.MethodPost, \"/\", body)\n\treq2.Header.Set(HeaderContentType, MIMEApplicationJSON)\n\n\trec2 := httptest.NewRecorder()\n\n\tc3 := e2.NewContext(req2, rec2)\n\tc3.InitializeRoute(\n\t\t&RouteInfo{Path: \"/users/:id\"},\n\t\t&PathValues{\n\t\t\t{Name: \"id\", Value: \"1\"},\n\t\t},\n\t)\n\n\tu = new(user)\n\terr = c3.Bind(u)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, 1, u.ID)\n\t\tassert.Equal(t, \"Jon Snow\", u.Name)\n\t}\n}\n\nfunc TestBindUnmarshalTypeError(t *testing.T) {\n\tbody := bytes.NewBufferString(`{ \"id\": \"text\" }`)\n\te := New()\n\treq := httptest.NewRequest(http.MethodPost, \"/\", body)\n\treq.Header.Set(HeaderContentType, MIMEApplicationJSON)\n\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tu := new(user)\n\n\terr := c.Bind(u)\n\n\tassert.EqualError(t, err, `code=400, message=Bad Request, err=json: cannot unmarshal string into Go struct field user.id of type int`)\n}\n\nfunc TestBindSetWithProperType(t *testing.T) {\n\tts := new(bindTestStruct)\n\ttyp := reflect.TypeOf(ts).Elem()\n\tval := reflect.ValueOf(ts).Elem()\n\tfor i := 0; i < typ.NumField(); i++ {\n\t\ttypeField := typ.Field(i)\n\t\tstructField := val.Field(i)\n\t\tif !structField.CanSet() {\n\t\t\tcontinue\n\t\t}\n\t\tif len(values[typeField.Name]) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tval := values[typeField.Name][0]\n\t\terr := setWithProperType(typeField.Type.Kind(), val, structField)\n\t\tassert.NoError(t, err)\n\t}\n\tassertBindTestStruct(t, ts)\n\n\ttype foo struct {\n\t\tBar bytes.Buffer\n\t}\n\tv := &foo{}\n\ttyp = reflect.TypeOf(v).Elem()\n\tval = reflect.ValueOf(v).Elem()\n\tassert.Error(t, setWithProperType(typ.Field(0).Type.Kind(), \"5\", val.Field(0)))\n}\n\nfunc BenchmarkBindbindDataWithTags(b *testing.B) {\n\tb.ReportAllocs()\n\tts := new(bindTestStructWithTags)\n\tvar err error\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\terr = bindData(ts, values, \"form\", nil)\n\t}\n\tassert.NoError(b, err)\n\tassertBindTestStruct(b, (*bindTestStruct)(ts))\n}\n\nfunc assertBindTestStruct(tb testing.TB, ts *bindTestStruct) {\n\tassert.Equal(tb, 0, ts.I)\n\tassert.Equal(tb, int8(8), ts.I8)\n\tassert.Equal(tb, int16(16), ts.I16)\n\tassert.Equal(tb, int32(32), ts.I32)\n\tassert.Equal(tb, int64(64), ts.I64)\n\tassert.Equal(tb, uint(0), ts.UI)\n\tassert.Equal(tb, uint8(8), ts.UI8)\n\tassert.Equal(tb, uint16(16), ts.UI16)\n\tassert.Equal(tb, uint32(32), ts.UI32)\n\tassert.Equal(tb, uint64(64), ts.UI64)\n\tassert.Equal(tb, true, ts.B)\n\tassert.Equal(tb, float32(32.5), ts.F32)\n\tassert.Equal(tb, float64(64.5), ts.F64)\n\tassert.Equal(tb, \"test\", ts.S)\n\tassert.Equal(tb, \"\", ts.GetCantSet())\n}\n\nfunc testBindOkay(t *testing.T, r io.Reader, query url.Values, ctype string) {\n\te := New()\n\tpath := \"/\"\n\tif len(query) > 0 {\n\t\tpath += \"?\" + query.Encode()\n\t}\n\treq := httptest.NewRequest(http.MethodPost, path, r)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\treq.Header.Set(HeaderContentType, ctype)\n\tu := new(user)\n\terr := c.Bind(u)\n\tif assert.Equal(t, nil, err) {\n\t\tassert.Equal(t, 1, u.ID)\n\t\tassert.Equal(t, \"Jon Snow\", u.Name)\n\t}\n}\n\nfunc testBindArrayOkay(t *testing.T, r io.Reader, query url.Values, ctype string) {\n\te := New()\n\tpath := \"/\"\n\tif len(query) > 0 {\n\t\tpath += \"?\" + query.Encode()\n\t}\n\treq := httptest.NewRequest(http.MethodPost, path, r)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\treq.Header.Set(HeaderContentType, ctype)\n\tu := []user{}\n\terr := c.Bind(&u)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, 1, len(u))\n\t\tassert.Equal(t, 1, u[0].ID)\n\t\tassert.Equal(t, \"Jon Snow\", u[0].Name)\n\t}\n}\n\nfunc testBindError(t *testing.T, r io.Reader, ctype string, expectedInternal error) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodPost, \"/\", r)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\treq.Header.Set(HeaderContentType, ctype)\n\tu := new(user)\n\terr := c.Bind(u)\n\n\tswitch {\n\tcase strings.HasPrefix(ctype, MIMEApplicationJSON), strings.HasPrefix(ctype, MIMEApplicationXML), strings.HasPrefix(ctype, MIMETextXML),\n\t\tstrings.HasPrefix(ctype, MIMEApplicationForm), strings.HasPrefix(ctype, MIMEMultipartForm):\n\t\tif assert.IsType(t, new(HTTPError), err) {\n\t\t\tassert.Equal(t, http.StatusBadRequest, err.(*HTTPError).Code)\n\t\t\tassert.IsType(t, expectedInternal, err.(*HTTPError).Unwrap())\n\t\t}\n\tdefault:\n\t\tif assert.IsType(t, new(HTTPError), err) {\n\t\t\tassert.Equal(t, ErrUnsupportedMediaType, err)\n\t\t\tassert.IsType(t, expectedInternal, err.(*HTTPError).Unwrap())\n\t\t}\n\t}\n}\n\nfunc TestDefaultBinder_BindToStructFromMixedSources(t *testing.T) {\n\t// tests to check binding behaviour when multiple sources (path params, query params and request body) are in use\n\t// binding is done in steps and one source could overwrite previous source bound data\n\t// these tests are to document this behaviour and detect further possible regressions when bind implementation is changed\n\n\ttype Opts struct {\n\t\tNode string `json:\"node\" form:\"node\" query:\"node\" param:\"node\"`\n\t\tLang string\n\t\tID   int `json:\"id\" form:\"id\" query:\"id\"`\n\t}\n\n\tvar testCases = []struct {\n\t\tgivenContent     io.Reader\n\t\twhenBindTarget   any\n\t\texpect           any\n\t\tname             string\n\t\tgivenURL         string\n\t\tgivenMethod      string\n\t\texpectError      string\n\t\twhenNoPathValues bool\n\t}{\n\t\t{\n\t\t\tname:         \"ok, POST bind to struct with: path param + query param + body\",\n\t\t\tgivenMethod:  http.MethodPost,\n\t\t\tgivenURL:     \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenContent: strings.NewReader(`{\"id\": 1}`),\n\t\t\texpect:       &Opts{ID: 1, Node: \"node_from_path\"}, // query params are not used, node is filled from path\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, PUT bind to struct with: path param + query param + body\",\n\t\t\tgivenMethod:  http.MethodPut,\n\t\t\tgivenURL:     \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenContent: strings.NewReader(`{\"id\": 1}`),\n\t\t\texpect:       &Opts{ID: 1, Node: \"node_from_path\"}, // query params are not used\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, GET bind to struct with: path param + query param + body\",\n\t\t\tgivenMethod:  http.MethodGet,\n\t\t\tgivenURL:     \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenContent: strings.NewReader(`{\"id\": 1}`),\n\t\t\texpect:       &Opts{ID: 1, Node: \"xxx\"}, // query overwrites previous path value\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, GET bind to struct with: path param + query param + body\",\n\t\t\tgivenMethod:  http.MethodGet,\n\t\t\tgivenURL:     \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenContent: strings.NewReader(`{\"id\": 1, \"node\": \"zzz\"}`),\n\t\t\texpect:       &Opts{ID: 1, Node: \"zzz\"}, // body is bound last and overwrites previous (path,query) values\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, DELETE bind to struct with: path param + query param + body\",\n\t\t\tgivenMethod:  http.MethodDelete,\n\t\t\tgivenURL:     \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenContent: strings.NewReader(`{\"id\": 1, \"node\": \"zzz\"}`),\n\t\t\texpect:       &Opts{ID: 1, Node: \"zzz\"}, // for DELETE body is bound after query params\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, POST bind to struct with: path param + body\",\n\t\t\tgivenMethod:  http.MethodPost,\n\t\t\tgivenURL:     \"/api/real_node/endpoint\",\n\t\t\tgivenContent: strings.NewReader(`{\"id\": 1}`),\n\t\t\texpect:       &Opts{ID: 1, Node: \"node_from_path\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, POST bind to struct with path + query + body = body has priority\",\n\t\t\tgivenMethod:  http.MethodPost,\n\t\t\tgivenURL:     \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenContent: strings.NewReader(`{\"id\": 1, \"node\": \"zzz\"}`),\n\t\t\texpect:       &Opts{ID: 1, Node: \"zzz\"}, // field value from content has higher priority\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, POST body bind failure\",\n\t\t\tgivenMethod:  http.MethodPost,\n\t\t\tgivenURL:     \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenContent: strings.NewReader(`{`),\n\t\t\texpect:       &Opts{ID: 0, Node: \"node_from_path\"}, // query binding has already modified bind target\n\t\t\texpectError:  \"code=400, message=Bad Request, err=unexpected EOF\",\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, GET with body bind failure when types are not convertible\",\n\t\t\tgivenMethod:  http.MethodGet,\n\t\t\tgivenURL:     \"/api/real_node/endpoint?id=nope\",\n\t\t\tgivenContent: strings.NewReader(`{\"id\": 1, \"node\": \"zzz\"}`),\n\t\t\texpect:       &Opts{ID: 0, Node: \"node_from_path\"}, // path params binding has already modified bind target\n\t\t\texpectError:  `code=400, message=Bad Request, err=strconv.ParseInt: parsing \"nope\": invalid syntax`,\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, GET body bind failure - trying to bind json array to struct\",\n\t\t\tgivenMethod:  http.MethodGet,\n\t\t\tgivenURL:     \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenContent: strings.NewReader(`[{\"id\": 1}]`),\n\t\t\texpect:       &Opts{ID: 0, Node: \"xxx\"}, // query binding has already modified bind target\n\t\t\texpectError:  `code=400, message=Bad Request, err=json: cannot unmarshal array into Go value of type echo.Opts`,\n\t\t},\n\t\t{ // query param is ignored as we do not know where exactly to bind it in slice\n\t\t\tname:             \"ok, GET bind to struct slice, ignore query param\",\n\t\t\tgivenMethod:      http.MethodGet,\n\t\t\tgivenURL:         \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenContent:     strings.NewReader(`[{\"id\": 1}]`),\n\t\t\twhenNoPathValues: true,\n\t\t\twhenBindTarget:   &[]Opts{},\n\t\t\texpect: &[]Opts{\n\t\t\t\t{ID: 1, Node: \"\"},\n\t\t\t},\n\t\t},\n\t\t{ // binding query params interferes with body. b.BindBody() should be used to bind only body to slice\n\t\t\tname:             \"ok, POST binding to slice should not be affected query params types\",\n\t\t\tgivenMethod:      http.MethodPost,\n\t\t\tgivenURL:         \"/api/real_node/endpoint?id=nope&node=xxx\",\n\t\t\tgivenContent:     strings.NewReader(`[{\"id\": 1}]`),\n\t\t\twhenNoPathValues: true,\n\t\t\twhenBindTarget:   &[]Opts{},\n\t\t\texpect:           &[]Opts{{ID: 1}},\n\t\t\texpectError:      \"\",\n\t\t},\n\t\t{ // path param is ignored as we do not know where exactly to bind it in slice\n\t\t\tname:           \"ok, GET bind to struct slice, ignore path param\",\n\t\t\tgivenMethod:    http.MethodGet,\n\t\t\tgivenURL:       \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenContent:   strings.NewReader(`[{\"id\": 1}]`),\n\t\t\twhenBindTarget: &[]Opts{},\n\t\t\texpect: &[]Opts{\n\t\t\t\t{ID: 1, Node: \"\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, GET body bind json array to slice\",\n\t\t\tgivenMethod:      http.MethodGet,\n\t\t\tgivenURL:         \"/api/real_node/endpoint\",\n\t\t\tgivenContent:     strings.NewReader(`[{\"id\": 1}]`),\n\t\t\twhenNoPathValues: true,\n\t\t\twhenBindTarget:   &[]Opts{},\n\t\t\texpect:           &[]Opts{{ID: 1, Node: \"\"}},\n\t\t\texpectError:      \"\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\t\t\t// assume route we are testing is \"/api/:node/endpoint?some_query_params=here\"\n\t\t\treq := httptest.NewRequest(tc.givenMethod, tc.givenURL, tc.givenContent)\n\t\t\treq.Header.Set(HeaderContentType, MIMEApplicationJSON)\n\t\t\trec := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\tif !tc.whenNoPathValues {\n\t\t\t\tc.SetPathValues(PathValues{\n\t\t\t\t\t{Name: \"node\", Value: \"node_from_path\"},\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tvar bindTarget any\n\t\t\tif tc.whenBindTarget != nil {\n\t\t\t\tbindTarget = tc.whenBindTarget\n\t\t\t} else {\n\t\t\t\tbindTarget = &Opts{}\n\t\t\t}\n\t\t\tb := new(DefaultBinder)\n\n\t\t\terr := b.Bind(c, bindTarget)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, bindTarget)\n\t\t})\n\t}\n}\n\nfunc TestDefaultBinder_BindBody(t *testing.T) {\n\t// tests to check binding behaviour when multiple sources (path params, query params and request body) are in use\n\t// generally when binding from request body - URL and path params are ignored - unless form is being bound.\n\t// these tests are to document this behaviour and detect further possible regressions when bind implementation is changed\n\n\ttype Node struct {\n\t\tNode string `json:\"node\" xml:\"node\" form:\"node\" query:\"node\" param:\"node\"`\n\t\tID   int    `json:\"id\" xml:\"id\" form:\"id\" query:\"id\"`\n\t}\n\ttype Nodes struct {\n\t\tNodes []Node `xml:\"node\" form:\"node\"`\n\t}\n\n\tvar testCases = []struct {\n\t\tgivenContent     io.Reader\n\t\twhenBindTarget   any\n\t\texpect           any\n\t\tname             string\n\t\tgivenURL         string\n\t\tgivenMethod      string\n\t\tgivenContentType string\n\t\texpectError      string\n\t\twhenNoPathValues bool\n\t\twhenChunkedBody  bool\n\t}{\n\t\t{\n\t\t\tname:             \"ok, JSON POST bind to struct with: path + query + empty field in body\",\n\t\t\tgivenURL:         \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenMethod:      http.MethodPost,\n\t\t\tgivenContentType: MIMEApplicationJSON,\n\t\t\tgivenContent:     strings.NewReader(`{\"id\": 1}`),\n\t\t\texpect:           &Node{ID: 1, Node: \"\"}, // path params or query params should not interfere with body\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, JSON POST bind to struct with: path + query + body\",\n\t\t\tgivenURL:         \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenMethod:      http.MethodPost,\n\t\t\tgivenContentType: MIMEApplicationJSON,\n\t\t\tgivenContent:     strings.NewReader(`{\"id\": 1, \"node\": \"zzz\"}`),\n\t\t\texpect:           &Node{ID: 1, Node: \"zzz\"}, // field value from content has higher priority\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, JSON POST body bind json array to slice (has matching path/query params)\",\n\t\t\tgivenURL:         \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenMethod:      http.MethodPost,\n\t\t\tgivenContentType: MIMEApplicationJSON,\n\t\t\tgivenContent:     strings.NewReader(`[{\"id\": 1}]`),\n\t\t\twhenNoPathValues: true,\n\t\t\twhenBindTarget:   &[]Node{},\n\t\t\texpect:           &[]Node{{ID: 1, Node: \"\"}},\n\t\t\texpectError:      \"\",\n\t\t},\n\t\t{ // rare case as GET is not usually used to send request body\n\t\t\tname:             \"ok, JSON GET bind to struct with: path + query + empty field in body\",\n\t\t\tgivenURL:         \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenMethod:      http.MethodGet,\n\t\t\tgivenContentType: MIMEApplicationJSON,\n\t\t\tgivenContent:     strings.NewReader(`{\"id\": 1}`),\n\t\t\texpect:           &Node{ID: 1, Node: \"\"}, // path params or query params should not interfere with body\n\t\t},\n\t\t{ // rare case as GET is not usually used to send request body\n\t\t\tname:             \"ok, JSON GET bind to struct with: path + query + body\",\n\t\t\tgivenURL:         \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenMethod:      http.MethodGet,\n\t\t\tgivenContentType: MIMEApplicationJSON,\n\t\t\tgivenContent:     strings.NewReader(`{\"id\": 1, \"node\": \"zzz\"}`),\n\t\t\texpect:           &Node{ID: 1, Node: \"zzz\"}, // field value from content has higher priority\n\t\t},\n\t\t{\n\t\t\tname:             \"nok, JSON POST body bind failure\",\n\t\t\tgivenURL:         \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenMethod:      http.MethodPost,\n\t\t\tgivenContentType: MIMEApplicationJSON,\n\t\t\tgivenContent:     strings.NewReader(`{`),\n\t\t\texpect:           &Node{ID: 0, Node: \"\"},\n\t\t\texpectError:      \"code=400, message=Bad Request, err=unexpected EOF\",\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, XML POST bind to struct with: path + query + empty body\",\n\t\t\tgivenURL:         \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenMethod:      http.MethodPost,\n\t\t\tgivenContentType: MIMEApplicationXML,\n\t\t\tgivenContent:     strings.NewReader(`<node><id>1</id><node>yyy</node></node>`),\n\t\t\texpect:           &Node{ID: 1, Node: \"yyy\"},\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, XML POST bind array to slice with: path + query + body\",\n\t\t\tgivenURL:         \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenMethod:      http.MethodPost,\n\t\t\tgivenContentType: MIMEApplicationXML,\n\t\t\tgivenContent:     strings.NewReader(`<nodes><node><id>1</id><node>yyy</node></node></nodes>`),\n\t\t\twhenBindTarget:   &Nodes{},\n\t\t\texpect:           &Nodes{Nodes: []Node{{ID: 1, Node: \"yyy\"}}},\n\t\t},\n\t\t{\n\t\t\tname:             \"nok, XML POST bind failure\",\n\t\t\tgivenURL:         \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenMethod:      http.MethodPost,\n\t\t\tgivenContentType: MIMEApplicationXML,\n\t\t\tgivenContent:     strings.NewReader(`<node><`),\n\t\t\texpect:           &Node{ID: 0, Node: \"\"},\n\t\t\texpectError:      \"code=400, message=Bad Request, err=XML syntax error on line 1: unexpected EOF\",\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, FORM POST bind to struct with: path + query + body\",\n\t\t\tgivenURL:         \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenMethod:      http.MethodPost,\n\t\t\tgivenContentType: MIMEApplicationForm,\n\t\t\tgivenContent:     strings.NewReader(`id=1&node=yyy`),\n\t\t\texpect:           &Node{ID: 1, Node: \"yyy\"},\n\t\t},\n\t\t{\n\t\t\t// NB: form values are taken from BOTH body and query for POST/PUT/PATCH by standard library implementation\n\t\t\t// See: https://golang.org/pkg/net/http/#Request.ParseForm\n\t\t\tname:             \"ok, FORM POST bind to struct with: path + query + empty field in body\",\n\t\t\tgivenURL:         \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenMethod:      http.MethodPost,\n\t\t\tgivenContentType: MIMEApplicationForm,\n\t\t\tgivenContent:     strings.NewReader(`id=1`),\n\t\t\texpect:           &Node{ID: 1, Node: \"xxx\"},\n\t\t},\n\t\t{\n\t\t\t// NB: form values are taken from query by standard library implementation\n\t\t\t// See: https://golang.org/pkg/net/http/#Request.ParseForm\n\t\t\tname:             \"ok, FORM GET bind to struct with: path + query + empty field in body\",\n\t\t\tgivenURL:         \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenMethod:      http.MethodGet,\n\t\t\tgivenContentType: MIMEApplicationForm,\n\t\t\tgivenContent:     strings.NewReader(`id=1`),\n\t\t\texpect:           &Node{ID: 0, Node: \"xxx\"}, // 'xxx' is taken from URL and body is not used with GET by implementation\n\t\t},\n\t\t{\n\t\t\tname:             \"nok, unsupported content type\",\n\t\t\tgivenURL:         \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenMethod:      http.MethodPost,\n\t\t\tgivenContentType: MIMETextPlain,\n\t\t\tgivenContent:     strings.NewReader(`<html></html>`),\n\t\t\texpect:           &Node{ID: 0, Node: \"\"},\n\t\t\texpectError:      \"code=415, message=Unsupported Media Type\",\n\t\t},\n\t\t// FIXME: REASON in Go 1.24 and earlier http.NoBody would result ContentLength=-1\n\t\t// \t\tbut as of Go 1.25 http.NoBody would result ContentLength=0\n\t\t//\t\tI am too lazy to bother documenting this as 2 version specific tests.\n\t\t//{\n\t\t//\tname:             \"nok, JSON POST with http.NoBody\",\n\t\t//\tgivenURL:         \"/api/real_node/endpoint?node=xxx\",\n\t\t//\tgivenMethod:      http.MethodPost,\n\t\t//\tgivenContentType: MIMEApplicationJSON,\n\t\t//\tgivenContent:     http.NoBody,\n\t\t//\texpect:           &Node{ID: 0, Node: \"\"},\n\t\t//\texpectError:      \"code=400, message=EOF, internal=EOF\",\n\t\t//},\n\t\t{\n\t\t\tname:             \"ok, JSON POST with empty body\",\n\t\t\tgivenURL:         \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenMethod:      http.MethodPost,\n\t\t\tgivenContentType: MIMEApplicationJSON,\n\t\t\tgivenContent:     strings.NewReader(\"\"),\n\t\t\texpect:           &Node{ID: 0, Node: \"\"},\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, JSON POST bind to struct with: path + query + chunked body\",\n\t\t\tgivenURL:         \"/api/real_node/endpoint?node=xxx\",\n\t\t\tgivenMethod:      http.MethodPost,\n\t\t\tgivenContentType: MIMEApplicationJSON,\n\t\t\tgivenContent:     httputil.NewChunkedReader(strings.NewReader(\"18\\r\\n\" + `{\"id\": 1, \"node\": \"zzz\"}` + \"\\r\\n0\\r\\n\")),\n\t\t\twhenChunkedBody:  true,\n\t\t\texpect:           &Node{ID: 1, Node: \"zzz\"},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\t\t\t// assume route we are testing is \"/api/:node/endpoint?some_query_params=here\"\n\t\t\treq := httptest.NewRequest(tc.givenMethod, tc.givenURL, tc.givenContent)\n\t\t\tswitch tc.givenContentType {\n\t\t\tcase MIMEApplicationXML:\n\t\t\t\treq.Header.Set(HeaderContentType, MIMEApplicationXML)\n\t\t\tcase MIMEApplicationForm:\n\t\t\t\treq.Header.Set(HeaderContentType, MIMEApplicationForm)\n\t\t\tcase MIMEApplicationJSON:\n\t\t\t\treq.Header.Set(HeaderContentType, MIMEApplicationJSON)\n\t\t\t}\n\t\t\tif tc.whenChunkedBody {\n\t\t\t\treq.ContentLength = -1\n\t\t\t\treq.TransferEncoding = append(req.TransferEncoding, \"chunked\")\n\t\t\t}\n\t\t\trec := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\tif !tc.whenNoPathValues {\n\t\t\t\tc.SetPathValues(PathValues{\n\t\t\t\t\t{Name: \"node\", Value: \"real_node\"},\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tvar bindTarget any\n\t\t\tif tc.whenBindTarget != nil {\n\t\t\t\tbindTarget = tc.whenBindTarget\n\t\t\t} else {\n\t\t\t\tbindTarget = &Node{}\n\t\t\t}\n\n\t\t\terr := BindBody(c, bindTarget)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, bindTarget)\n\t\t})\n\t}\n}\n\nfunc testBindURL(queryString string, target any) error {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, queryString, nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\treturn c.Bind(target)\n}\n\ntype unixTimestamp struct {\n\tTime time.Time\n}\n\nfunc (t *unixTimestamp) UnmarshalParam(param string) error {\n\tn, err := strconv.ParseInt(param, 10, 64)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"'%s' is not an integer\", param)\n\t}\n\t*t = unixTimestamp{Time: time.Unix(n, 0)}\n\treturn err\n}\n\ntype IntArrayA []int\n\n// UnmarshalParam converts value to *Int64Slice.  This allows the API to accept\n// a comma-separated list of integers as a query parameter.\nfunc (i *IntArrayA) UnmarshalParam(value string) error {\n\tvar values = strings.Split(value, \",\")\n\tvar numbers = make([]int, 0, len(values))\n\n\tfor _, v := range values {\n\t\tn, err := strconv.ParseInt(v, 10, 64)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"'%s' is not an integer\", v)\n\t\t}\n\n\t\tnumbers = append(numbers, int(n))\n\t}\n\n\t*i = append(*i, numbers...)\n\treturn nil\n}\n\nfunc TestBindUnmarshalParamExtras(t *testing.T) {\n\t// this test documents how bind handles `BindUnmarshaler` interface:\n\t// NOTE: BindUnmarshaler chooses first input value to be bound.\n\n\tt.Run(\"nok, unmarshalling fails\", func(t *testing.T) {\n\t\tresult := struct {\n\t\t\tV unixTimestamp `query:\"t\"`\n\t\t}{}\n\t\terr := testBindURL(\"/?t=xxxx\", &result)\n\n\t\tassert.EqualError(t, err, `code=400, message=Bad Request, err='xxxx' is not an integer`)\n\t})\n\n\tt.Run(\"ok, target is struct\", func(t *testing.T) {\n\t\tresult := struct {\n\t\t\tV unixTimestamp `query:\"t\"`\n\t\t}{}\n\t\terr := testBindURL(\"/?t=1710095540&t=1710095541\", &result)\n\n\t\tassert.NoError(t, err)\n\t\texpect := unixTimestamp{\n\t\t\tTime: time.Unix(1710095540, 0),\n\t\t}\n\t\tassert.Equal(t, expect, result.V)\n\t})\n\n\tt.Run(\"ok, target is an alias to slice and is nil, append only values from first\", func(t *testing.T) {\n\t\tresult := struct {\n\t\t\tV IntArrayA `query:\"a\"`\n\t\t}{}\n\t\terr := testBindURL(\"/?a=1,2,3&a=4,5,6\", &result)\n\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, IntArrayA([]int{1, 2, 3}), result.V)\n\t})\n\n\tt.Run(\"ok, target is an alias to slice and is nil, single input\", func(t *testing.T) {\n\t\tresult := struct {\n\t\t\tV IntArrayA `query:\"a\"`\n\t\t}{}\n\t\terr := testBindURL(\"/?a=1,2\", &result)\n\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, IntArrayA([]int{1, 2}), result.V)\n\t})\n\n\tt.Run(\"ok, target is pointer an alias to slice and is nil\", func(t *testing.T) {\n\t\tresult := struct {\n\t\t\tV *IntArrayA `query:\"a\"`\n\t\t}{}\n\t\terr := testBindURL(\"/?a=1&a=4,5,6\", &result)\n\n\t\tassert.NoError(t, err)\n\t\tvar expected = IntArrayA([]int{1})\n\t\tassert.Equal(t, &expected, result.V)\n\t})\n\n\tt.Run(\"ok, target is pointer an alias to slice and is NOT nil\", func(t *testing.T) {\n\t\tresult := struct {\n\t\t\tV *IntArrayA `query:\"a\"`\n\t\t}{}\n\t\tresult.V = new(IntArrayA) // NOT nil\n\n\t\terr := testBindURL(\"/?a=1&a=4,5,6\", &result)\n\n\t\tassert.NoError(t, err)\n\t\tvar expected = IntArrayA([]int{1})\n\t\tassert.Equal(t, &expected, result.V)\n\t})\n}\n\ntype unixTimestampLast struct {\n\tTime time.Time\n}\n\n// this is silly example for `bindMultipleUnmarshaler` for type that uses last input value for unmarshalling\nfunc (t *unixTimestampLast) UnmarshalParams(params []string) error {\n\tlastInput := params[len(params)-1]\n\tn, err := strconv.ParseInt(lastInput, 10, 64)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"'%s' is not an integer\", lastInput)\n\t}\n\t*t = unixTimestampLast{Time: time.Unix(n, 0)}\n\treturn err\n}\n\ntype IntArrayB []int\n\nfunc (i *IntArrayB) UnmarshalParams(params []string) error {\n\tvar numbers = make([]int, 0, len(params))\n\n\tfor _, param := range params {\n\t\tvar values = strings.Split(param, \",\")\n\t\tfor _, v := range values {\n\t\t\tn, err := strconv.ParseInt(v, 10, 64)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"'%s' is not an integer\", v)\n\t\t\t}\n\t\t\tnumbers = append(numbers, int(n))\n\t\t}\n\t}\n\n\t*i = append(*i, numbers...)\n\treturn nil\n}\n\nfunc TestBindUnmarshalParams(t *testing.T) {\n\t// this test documents how bind handles `bindMultipleUnmarshaler` interface:\n\n\tt.Run(\"nok, unmarshalling fails\", func(t *testing.T) {\n\t\tresult := struct {\n\t\t\tV unixTimestampLast `query:\"t\"`\n\t\t}{}\n\t\terr := testBindURL(\"/?t=xxxx\", &result)\n\n\t\tassert.EqualError(t, err, \"code=400, message=Bad Request, err='xxxx' is not an integer\")\n\t})\n\n\tt.Run(\"ok, target is struct\", func(t *testing.T) {\n\t\tresult := struct {\n\t\t\tV unixTimestampLast `query:\"t\"`\n\t\t}{}\n\t\terr := testBindURL(\"/?t=1710095540&t=1710095541\", &result)\n\n\t\tassert.NoError(t, err)\n\t\texpect := unixTimestampLast{\n\t\t\tTime: time.Unix(1710095541, 0),\n\t\t}\n\t\tassert.Equal(t, expect, result.V)\n\t})\n\n\tt.Run(\"ok, target is an alias to slice and is nil, append multiple inputs\", func(t *testing.T) {\n\t\tresult := struct {\n\t\t\tV IntArrayB `query:\"a\"`\n\t\t}{}\n\t\terr := testBindURL(\"/?a=1,2,3&a=4,5,6\", &result)\n\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, IntArrayB([]int{1, 2, 3, 4, 5, 6}), result.V)\n\t})\n\n\tt.Run(\"ok, target is an alias to slice and is nil, single input\", func(t *testing.T) {\n\t\tresult := struct {\n\t\t\tV IntArrayB `query:\"a\"`\n\t\t}{}\n\t\terr := testBindURL(\"/?a=1,2\", &result)\n\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, IntArrayB([]int{1, 2}), result.V)\n\t})\n\n\tt.Run(\"ok, target is pointer an alias to slice and is nil\", func(t *testing.T) {\n\t\tresult := struct {\n\t\t\tV *IntArrayB `query:\"a\"`\n\t\t}{}\n\t\terr := testBindURL(\"/?a=1&a=4,5,6\", &result)\n\n\t\tassert.NoError(t, err)\n\t\tvar expected = IntArrayB([]int{1, 4, 5, 6})\n\t\tassert.Equal(t, &expected, result.V)\n\t})\n\n\tt.Run(\"ok, target is pointer an alias to slice and is NOT nil\", func(t *testing.T) {\n\t\tresult := struct {\n\t\t\tV *IntArrayB `query:\"a\"`\n\t\t}{}\n\t\tresult.V = new(IntArrayB) // NOT nil\n\n\t\terr := testBindURL(\"/?a=1&a=4,5,6\", &result)\n\t\tassert.NoError(t, err)\n\t\tvar expected = IntArrayB([]int{1, 4, 5, 6})\n\t\tassert.Equal(t, &expected, result.V)\n\t})\n}\n\nfunc TestBindInt8(t *testing.T) {\n\tt.Run(\"nok, binding fails\", func(t *testing.T) {\n\t\ttype target struct {\n\t\t\tV int8 `query:\"v\"`\n\t\t}\n\t\tp := target{}\n\t\terr := testBindURL(\"/?v=x&v=2\", &p)\n\t\tassert.EqualError(t, err, `code=400, message=Bad Request, err=strconv.ParseInt: parsing \"x\": invalid syntax`)\n\t})\n\n\tt.Run(\"nok, int8 embedded in struct\", func(t *testing.T) {\n\t\ttype target struct {\n\t\t\tint8 `query:\"v\"` // embedded field is `Anonymous`. We can only set public fields\n\t\t}\n\t\tp := target{}\n\t\terr := testBindURL(\"/?v=1&v=2\", &p)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, target{0}, p)\n\t})\n\n\tt.Run(\"nok, pointer to int8 embedded in struct\", func(t *testing.T) {\n\t\ttype target struct {\n\t\t\t*int8 `query:\"v\"` // embedded field is `Anonymous`. We can only set public fields\n\t\t}\n\t\tp := target{}\n\t\terr := testBindURL(\"/?v=1&v=2\", &p)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, target{int8: nil}, p)\n\t})\n\n\tt.Run(\"ok, bind int8 as struct field\", func(t *testing.T) {\n\t\ttype target struct {\n\t\t\tV int8 `query:\"v\"`\n\t\t}\n\t\tp := target{V: 127}\n\t\terr := testBindURL(\"/?v=1&v=2\", &p)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, target{V: 1}, p)\n\t})\n\n\tt.Run(\"ok, bind pointer to int8 as struct field, value is nil\", func(t *testing.T) {\n\t\ttype target struct {\n\t\t\tV *int8 `query:\"v\"`\n\t\t}\n\t\tp := target{}\n\t\terr := testBindURL(\"/?v=1&v=2\", &p)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, target{V: ptr(int8(1))}, p)\n\t})\n\n\tt.Run(\"ok, bind pointer to int8 as struct field, value is set\", func(t *testing.T) {\n\t\ttype target struct {\n\t\t\tV *int8 `query:\"v\"`\n\t\t}\n\t\tp := target{V: ptr(int8(127))}\n\t\terr := testBindURL(\"/?v=1&v=2\", &p)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, target{V: ptr(int8(1))}, p)\n\t})\n\n\tt.Run(\"ok, bind int8 slice as struct field, value is nil\", func(t *testing.T) {\n\t\ttype target struct {\n\t\t\tV []int8 `query:\"v\"`\n\t\t}\n\t\tp := target{}\n\t\terr := testBindURL(\"/?v=1&v=2\", &p)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, target{V: []int8{1, 2}}, p)\n\t})\n\n\tt.Run(\"ok, bind slice of int8 as struct field, value is set\", func(t *testing.T) {\n\t\ttype target struct {\n\t\t\tV []int8 `query:\"v\"`\n\t\t}\n\t\tp := target{V: []int8{111}}\n\t\terr := testBindURL(\"/?v=1&v=2\", &p)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, target{V: []int8{1, 2}}, p)\n\t})\n\n\tt.Run(\"ok, bind slice of pointer to int8 as struct field, value is set\", func(t *testing.T) {\n\t\ttype target struct {\n\t\t\tV []*int8 `query:\"v\"`\n\t\t}\n\t\tp := target{V: []*int8{ptr(int8(127))}}\n\t\terr := testBindURL(\"/?v=1&v=2\", &p)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, target{V: []*int8{ptr(int8(1)), ptr(int8(2))}}, p)\n\t})\n\n\tt.Run(\"ok, bind pointer to slice of int8 as struct field, value is set\", func(t *testing.T) {\n\t\ttype target struct {\n\t\t\tV *[]int8 `query:\"v\"`\n\t\t}\n\t\tp := target{V: &[]int8{111}}\n\t\terr := testBindURL(\"/?v=1&v=2\", &p)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, target{V: &[]int8{1, 2}}, p)\n\t})\n}\n\nfunc TestBindMultipartFormFiles(t *testing.T) {\n\tfile1 := createTestFormFile(\"file\", \"file1.txt\")\n\tfile11 := createTestFormFile(\"file\", \"file11.txt\")\n\tfile2 := createTestFormFile(\"file2\", \"file2.txt\")\n\tfilesA := createTestFormFile(\"files\", \"filesA.txt\")\n\tfilesB := createTestFormFile(\"files\", \"filesB.txt\")\n\n\tt.Run(\"nok, can not bind to multipart file struct\", func(t *testing.T) {\n\t\tvar target struct {\n\t\t\tFile multipart.FileHeader `form:\"file\"`\n\t\t}\n\t\terr := bindMultipartFiles(t, &target, file1, file2) // file2 should be ignored\n\n\t\tassert.EqualError(t, err, `code=400, message=Bad Request, err=binding to multipart.FileHeader struct is not supported, use pointer to struct`)\n\t})\n\n\tt.Run(\"ok, bind single multipart file to pointer to multipart file\", func(t *testing.T) {\n\t\tvar target struct {\n\t\t\tFile *multipart.FileHeader `form:\"file\"`\n\t\t}\n\t\terr := bindMultipartFiles(t, &target, file1, file2) // file2 should be ignored\n\n\t\tassert.NoError(t, err)\n\t\tassertMultipartFileHeader(t, target.File, file1)\n\t})\n\n\tt.Run(\"ok, bind multiple multipart files to pointer to multipart file\", func(t *testing.T) {\n\t\tvar target struct {\n\t\t\tFile *multipart.FileHeader `form:\"file\"`\n\t\t}\n\t\terr := bindMultipartFiles(t, &target, file1, file11)\n\n\t\tassert.NoError(t, err)\n\t\tassertMultipartFileHeader(t, target.File, file1) // should choose first one\n\t})\n\n\tt.Run(\"ok, bind multiple multipart files to slice of multipart file\", func(t *testing.T) {\n\t\tvar target struct {\n\t\t\tFiles []multipart.FileHeader `form:\"files\"`\n\t\t}\n\t\terr := bindMultipartFiles(t, &target, filesA, filesB, file1)\n\n\t\tassert.NoError(t, err)\n\n\t\tassert.Len(t, target.Files, 2)\n\t\tassertMultipartFileHeader(t, &target.Files[0], filesA)\n\t\tassertMultipartFileHeader(t, &target.Files[1], filesB)\n\t})\n\n\tt.Run(\"ok, bind multiple multipart files to slice of pointer to multipart file\", func(t *testing.T) {\n\t\tvar target struct {\n\t\t\tFiles []*multipart.FileHeader `form:\"files\"`\n\t\t}\n\t\terr := bindMultipartFiles(t, &target, filesA, filesB, file1)\n\n\t\tassert.NoError(t, err)\n\n\t\tassert.Len(t, target.Files, 2)\n\t\tassertMultipartFileHeader(t, target.Files[0], filesA)\n\t\tassertMultipartFileHeader(t, target.Files[1], filesB)\n\t})\n}\n\ntype testFormFile struct {\n\tFieldname string\n\tFilename  string\n\tContent   []byte\n}\n\nfunc createTestFormFile(formFieldName string, filename string) testFormFile {\n\treturn testFormFile{\n\t\tFieldname: formFieldName,\n\t\tFilename:  filename,\n\t\tContent:   []byte(strings.Repeat(filename, 10)),\n\t}\n}\n\nfunc bindMultipartFiles(t *testing.T, target any, files ...testFormFile) error {\n\tvar body bytes.Buffer\n\tmw := multipart.NewWriter(&body)\n\n\tfor _, file := range files {\n\t\tfw, err := mw.CreateFormFile(file.Fieldname, file.Filename)\n\t\tassert.NoError(t, err)\n\n\t\tn, err := fw.Write(file.Content)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, len(file.Content), n)\n\t}\n\n\terr := mw.Close()\n\tassert.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodPost, \"/\", &body)\n\tassert.NoError(t, err)\n\treq.Header.Set(\"Content-Type\", mw.FormDataContentType())\n\n\trec := httptest.NewRecorder()\n\n\te := New()\n\tc := e.NewContext(req, rec)\n\treturn c.Bind(target)\n}\n\nfunc assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file testFormFile) {\n\tassert.Equal(t, file.Filename, fh.Filename)\n\tassert.Equal(t, int64(len(file.Content)), fh.Size)\n\tfl, err := fh.Open()\n\tassert.NoError(t, err)\n\tbody, err := io.ReadAll(fl)\n\tassert.NoError(t, err)\n\tassert.Equal(t, string(file.Content), string(body))\n\terr = fl.Close()\n\tassert.NoError(t, err)\n}\n\nfunc TestTimeFormatBinding(t *testing.T) {\n\ttype TestStruct struct {\n\t\tDateTimeLocal time.Time  `form:\"datetime_local\" format:\"2006-01-02T15:04\"`\n\t\tDate          time.Time  `query:\"date\" format:\"2006-01-02\"`\n\t\tCustomFormat  time.Time  `form:\"custom\" format:\"01/02/2006 15:04:05\"`\n\t\tDefaultTime   time.Time  `form:\"default_time\"` // No format tag - should use default parsing\n\t\tPtrTime       *time.Time `query:\"ptr_time\" format:\"2006-01-02\"`\n\t}\n\n\ttestCases := []struct {\n\t\tname        string\n\t\tcontentType string\n\t\tdata        string\n\t\tqueryParams string\n\t\texpect      TestStruct\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, datetime-local format binding\",\n\t\t\tcontentType: MIMEApplicationForm,\n\t\t\tdata:        \"datetime_local=2023-12-25T14:30&default_time=2023-12-25T14:30:45Z\",\n\t\t\texpect: TestStruct{\n\t\t\t\tDateTimeLocal: time.Date(2023, 12, 25, 14, 30, 0, 0, time.UTC),\n\t\t\t\tDefaultTime:   time.Date(2023, 12, 25, 14, 30, 45, 0, time.UTC),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, date format binding via query params\",\n\t\t\tqueryParams: \"?date=2023-01-15&ptr_time=2023-02-20\",\n\t\t\texpect: TestStruct{\n\t\t\t\tDate:    time.Date(2023, 1, 15, 0, 0, 0, 0, time.UTC),\n\t\t\t\tPtrTime: &time.Time{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, custom format via form data\",\n\t\t\tcontentType: MIMEApplicationForm,\n\t\t\tdata:        \"custom=12/25/2023 14:30:45\",\n\t\t\texpect: TestStruct{\n\t\t\t\tCustomFormat: time.Date(2023, 12, 25, 14, 30, 45, 0, time.UTC),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, invalid format should fail\",\n\t\t\tcontentType: MIMEApplicationForm,\n\t\t\tdata:        \"datetime_local=invalid-date\",\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, wrong format should fail\",\n\t\t\tcontentType: MIMEApplicationForm,\n\t\t\tdata:        \"datetime_local=2023-12-25\", // Missing time part\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\t\t\tvar req *http.Request\n\n\t\t\tif tc.contentType == MIMEApplicationJSON {\n\t\t\t\treq = httptest.NewRequest(http.MethodPost, \"/\"+tc.queryParams, strings.NewReader(tc.data))\n\t\t\t\treq.Header.Set(HeaderContentType, tc.contentType)\n\t\t\t} else if tc.contentType == MIMEApplicationForm {\n\t\t\t\treq = httptest.NewRequest(http.MethodPost, \"/\"+tc.queryParams, strings.NewReader(tc.data))\n\t\t\t\treq.Header.Set(HeaderContentType, tc.contentType)\n\t\t\t} else {\n\t\t\t\treq = httptest.NewRequest(http.MethodGet, \"/\"+tc.queryParams, nil)\n\t\t\t}\n\n\t\t\trec := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\tvar result TestStruct\n\t\t\terr := c.Bind(&result)\n\n\t\t\tif tc.expectError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\n\t\t\t// Check individual fields since time comparison can be tricky\n\t\t\tif !tc.expect.DateTimeLocal.IsZero() {\n\t\t\t\tassert.True(t, tc.expect.DateTimeLocal.Equal(result.DateTimeLocal),\n\t\t\t\t\t\"DateTimeLocal: expected %v, got %v\", tc.expect.DateTimeLocal, result.DateTimeLocal)\n\t\t\t}\n\t\t\tif !tc.expect.Date.IsZero() {\n\t\t\t\tassert.True(t, tc.expect.Date.Equal(result.Date),\n\t\t\t\t\t\"Date: expected %v, got %v\", tc.expect.Date, result.Date)\n\t\t\t}\n\t\t\tif !tc.expect.CustomFormat.IsZero() {\n\t\t\t\tassert.True(t, tc.expect.CustomFormat.Equal(result.CustomFormat),\n\t\t\t\t\t\"CustomFormat: expected %v, got %v\", tc.expect.CustomFormat, result.CustomFormat)\n\t\t\t}\n\t\t\tif !tc.expect.DefaultTime.IsZero() {\n\t\t\t\tassert.True(t, tc.expect.DefaultTime.Equal(result.DefaultTime),\n\t\t\t\t\t\"DefaultTime: expected %v, got %v\", tc.expect.DefaultTime, result.DefaultTime)\n\t\t\t}\n\t\t\tif tc.expect.PtrTime != nil {\n\t\t\t\tassert.NotNil(t, result.PtrTime)\n\t\t\t\tif result.PtrTime != nil {\n\t\t\t\t\texpectedPtr := time.Date(2023, 2, 20, 0, 0, 0, 0, time.UTC)\n\t\t\t\t\tassert.True(t, expectedPtr.Equal(*result.PtrTime),\n\t\t\t\t\t\t\"PtrTime: expected %v, got %v\", expectedPtr, *result.PtrTime)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "binder.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"encoding\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n/**\n\tFollowing functions provide handful of methods for binding to Go native types from request query or path parameters.\n    * QueryParamsBinder(c) - binds query parameters (source URL)\n    * PathValuesBinder(c) - binds path parameters (source URL)\n    * FormFieldBinder(c) - binds form fields (source URL + body)\n\n\tExample:\n  ```go\n  var length int64\n  err := echo.QueryParamsBinder(c).Int64(\"length\", &length).BindError()\n  ```\n\n\tFor every supported type there are following methods:\n\t\t* <Type>(\"param\", &destination) - if parameter value exists then binds it to given destination of that type i.e Int64(...).\n\t\t* Must<Type>(\"param\", &destination) - parameter value is required to exist, binds it to given destination of that type i.e MustInt64(...).\n\t\t* <Type>s(\"param\", &destination) - (for slices) if parameter values exists then binds it to given destination of that type i.e Int64s(...).\n\t\t* Must<Type>s(\"param\", &destination) - (for slices) parameter value is required to exist, binds it to given destination of that type i.e MustInt64s(...).\n\n  for some slice types `BindWithDelimiter(\"param\", &dest, \",\")` supports splitting parameter values before type conversion is done\n  i.e. URL `/api/search?id=1,2,3&id=1` can be bind to `[]int64{1,2,3,1}`\n\n\t`FailFast` flags binder to stop binding after first bind error during binder call chain. Enabled by default.\n  `BindError()` returns first bind error from binder and resets errors in binder. Useful along with `FailFast()` method\n\t\tto do binding and returns on first problem\n  `BindErrors()` returns all bind errors from binder and resets errors in binder.\n\n\tTypes that are supported:\n\t\t* bool\n\t\t* float32\n\t\t* float64\n\t\t* int\n\t\t* int8\n\t\t* int16\n\t\t* int32\n\t\t* int64\n\t\t* uint\n\t\t* uint8/byte (does not support `bytes()`. Use BindUnmarshaler/CustomFunc to convert value from base64 etc to []byte{})\n\t\t* uint16\n\t\t* uint32\n\t\t* uint64\n\t\t* string\n\t\t* time\n\t\t* duration\n\t\t* BindUnmarshaler() interface\n\t\t* TextUnmarshaler() interface\n\t\t* JSONUnmarshaler() interface\n\t\t* UnixTime() - converts unix time (integer) to time.Time\n\t\t* UnixTimeMilli() - converts unix time with millisecond precision (integer) to time.Time\n\t\t* UnixTimeNano() - converts unix time with nanosecond precision (integer) to time.Time\n\t\t* CustomFunc() - callback function for your custom conversion logic. Signature `func(values []string) []error`\n*/\n\n// BindingError represents an error that occurred while binding request data.\ntype BindingError struct {\n\t// Field is the field name where value binding failed\n\tField string `json:\"field\"`\n\t*HTTPError\n\t// Values of parameter that failed to bind.\n\tValues []string `json:\"-\"`\n}\n\n// NewBindingError creates new instance of binding error\nfunc NewBindingError(sourceParam string, values []string, message string, err error) error {\n\treturn &BindingError{\n\t\tField:     sourceParam,\n\t\tValues:    values,\n\t\tHTTPError: &HTTPError{Code: http.StatusBadRequest, Message: message, err: err},\n\t}\n}\n\n// Error returns error message\nfunc (be *BindingError) Error() string {\n\treturn fmt.Sprintf(\"%s, field=%s\", be.HTTPError.Error(), be.Field)\n}\n\n// ValueBinder provides utility methods for binding query or path parameter to various Go built-in types\ntype ValueBinder struct {\n\t// ValueFunc is used to get single parameter (first) value from request\n\tValueFunc func(sourceParam string) string\n\t// ValuesFunc is used to get all values for parameter from request. i.e. `/api/search?ids=1&ids=2`\n\tValuesFunc func(sourceParam string) []string\n\t// ErrorFunc is used to create errors. Allows you to use your own error type, that for example marshals to your specific json response\n\tErrorFunc func(sourceParam string, values []string, message string, internalError error) error\n\terrors    []error\n\t// failFast is flag for binding methods to return without attempting to bind when previous binding already failed\n\tfailFast bool\n}\n\n// QueryParamsBinder creates query parameter value binder\nfunc QueryParamsBinder(c *Context) *ValueBinder {\n\treturn &ValueBinder{\n\t\tfailFast:  true,\n\t\tValueFunc: c.QueryParam,\n\t\tValuesFunc: func(sourceParam string) []string {\n\t\t\tvalues, ok := c.QueryParams()[sourceParam]\n\t\t\tif !ok {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn values\n\t\t},\n\t\tErrorFunc: NewBindingError,\n\t}\n}\n\n// PathValuesBinder creates path parameter value binder\nfunc PathValuesBinder(c *Context) *ValueBinder {\n\treturn &ValueBinder{\n\t\tfailFast:  true,\n\t\tValueFunc: c.Param,\n\t\tValuesFunc: func(sourceParam string) []string {\n\t\t\t// path parameter should not have multiple values so getting values does not make sense but lets not error out here\n\t\t\tvalue := c.Param(sourceParam)\n\t\t\tif value == \"\" {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn []string{value}\n\t\t},\n\t\tErrorFunc: NewBindingError,\n\t}\n}\n\n// FormFieldBinder creates form field value binder\n// For all requests, FormFieldBinder parses the raw query from the URL and uses query params as form fields\n//\n// For POST, PUT, and PATCH requests, it also reads the request body, parses it\n// as a form and uses query params as form fields. Request body parameters take precedence over URL query\n// string values in r.Form.\n//\n// NB: when binding forms take note that this implementation uses standard library form parsing\n// which parses form data from BOTH URL and BODY if content type is not MIMEMultipartForm\n// See https://golang.org/pkg/net/http/#Request.ParseForm\nfunc FormFieldBinder(c *Context) *ValueBinder {\n\tvb := &ValueBinder{\n\t\tfailFast: true,\n\t\tValueFunc: func(sourceParam string) string {\n\t\t\treturn c.Request().FormValue(sourceParam)\n\t\t},\n\t\tErrorFunc: NewBindingError,\n\t}\n\tvb.ValuesFunc = func(sourceParam string) []string {\n\t\tif c.Request().Form == nil {\n\t\t\t// this is same as `Request().FormValue()` does internally\n\t\t\t_, _ = c.MultipartForm() // we want to trigger c.request.ParseMultipartForm(c.formParseMaxMemory)\n\t\t}\n\t\tvalues, ok := c.Request().Form[sourceParam]\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\t\treturn values\n\t}\n\n\treturn vb\n}\n\n// FailFast set internal flag to indicate if binding methods will return early (without binding) when previous bind failed\n// NB: call this method before any other binding methods as it modifies binding methods behaviour\nfunc (b *ValueBinder) FailFast(value bool) *ValueBinder {\n\tb.failFast = value\n\treturn b\n}\n\nfunc (b *ValueBinder) setError(err error) {\n\tif b.errors == nil {\n\t\tb.errors = []error{err}\n\t\treturn\n\t}\n\tb.errors = append(b.errors, err)\n}\n\n// BindError returns first seen bind error and resets/empties binder errors for further calls\nfunc (b *ValueBinder) BindError() error {\n\tif b.errors == nil {\n\t\treturn nil\n\t}\n\terr := b.errors[0]\n\tb.errors = nil // reset errors so next chain will start from zero\n\treturn err\n}\n\n// BindErrors returns all bind errors and resets/empties binder errors for further calls\nfunc (b *ValueBinder) BindErrors() []error {\n\tif b.errors == nil {\n\t\treturn nil\n\t}\n\terrors := b.errors\n\tb.errors = nil // reset errors so next chain will start from zero\n\treturn errors\n}\n\n// CustomFunc binds parameter values with Func. Func is called only when parameter values exist.\nfunc (b *ValueBinder) CustomFunc(sourceParam string, customFunc func(values []string) []error) *ValueBinder {\n\treturn b.customFunc(sourceParam, customFunc, false)\n}\n\n// MustCustomFunc requires parameter values to exist to bind with Func. Returns error when value does not exist.\nfunc (b *ValueBinder) MustCustomFunc(sourceParam string, customFunc func(values []string) []error) *ValueBinder {\n\treturn b.customFunc(sourceParam, customFunc, true)\n}\n\nfunc (b *ValueBinder) customFunc(sourceParam string, customFunc func(values []string) []error, valueMustExist bool) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\tvalues := b.ValuesFunc(sourceParam)\n\tif len(values) == 0 {\n\t\tif valueMustExist {\n\t\t\tb.setError(b.ErrorFunc(sourceParam, []string{}, \"required field value is empty\", nil))\n\t\t}\n\t\treturn b\n\t}\n\tif errs := customFunc(values); errs != nil {\n\t\tb.errors = append(b.errors, errs...)\n\t}\n\treturn b\n}\n\n// String binds parameter to string variable\nfunc (b *ValueBinder) String(sourceParam string, dest *string) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\tvalue := b.ValueFunc(sourceParam)\n\tif value == \"\" {\n\t\treturn b\n\t}\n\t*dest = value\n\treturn b\n}\n\n// MustString requires parameter value to exist to bind to string variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustString(sourceParam string, dest *string) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\tvalue := b.ValueFunc(sourceParam)\n\tif value == \"\" {\n\t\tb.setError(b.ErrorFunc(sourceParam, []string{value}, \"required field value is empty\", nil))\n\t\treturn b\n\t}\n\t*dest = value\n\treturn b\n}\n\n// Strings binds parameter values to slice of string\nfunc (b *ValueBinder) Strings(sourceParam string, dest *[]string) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\tvalue := b.ValuesFunc(sourceParam)\n\tif value == nil {\n\t\treturn b\n\t}\n\t*dest = value\n\treturn b\n}\n\n// MustStrings requires parameter values to exist to bind to slice of string variables. Returns error when value does not exist\nfunc (b *ValueBinder) MustStrings(sourceParam string, dest *[]string) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\tvalue := b.ValuesFunc(sourceParam)\n\tif value == nil {\n\t\tb.setError(b.ErrorFunc(sourceParam, []string{}, \"required field value is empty\", nil))\n\t\treturn b\n\t}\n\t*dest = value\n\treturn b\n}\n\n// BindUnmarshaler binds parameter to destination implementing BindUnmarshaler interface\nfunc (b *ValueBinder) BindUnmarshaler(sourceParam string, dest BindUnmarshaler) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\ttmp := b.ValueFunc(sourceParam)\n\tif tmp == \"\" {\n\t\treturn b\n\t}\n\n\tif err := dest.UnmarshalParam(tmp); err != nil {\n\t\tb.setError(b.ErrorFunc(sourceParam, []string{tmp}, \"failed to bind field value to BindUnmarshaler interface\", err))\n\t}\n\treturn b\n}\n\n// MustBindUnmarshaler requires parameter value to exist to bind to destination implementing BindUnmarshaler interface.\n// Returns error when value does not exist\nfunc (b *ValueBinder) MustBindUnmarshaler(sourceParam string, dest BindUnmarshaler) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\tvalue := b.ValueFunc(sourceParam)\n\tif value == \"\" {\n\t\tb.setError(b.ErrorFunc(sourceParam, []string{value}, \"required field value is empty\", nil))\n\t\treturn b\n\t}\n\n\tif err := dest.UnmarshalParam(value); err != nil {\n\t\tb.setError(b.ErrorFunc(sourceParam, []string{value}, \"failed to bind field value to BindUnmarshaler interface\", err))\n\t}\n\treturn b\n}\n\n// JSONUnmarshaler binds parameter to destination implementing json.Unmarshaler interface\nfunc (b *ValueBinder) JSONUnmarshaler(sourceParam string, dest json.Unmarshaler) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\ttmp := b.ValueFunc(sourceParam)\n\tif tmp == \"\" {\n\t\treturn b\n\t}\n\n\tif err := dest.UnmarshalJSON([]byte(tmp)); err != nil {\n\t\tb.setError(b.ErrorFunc(sourceParam, []string{tmp}, \"failed to bind field value to json.Unmarshaler interface\", err))\n\t}\n\treturn b\n}\n\n// MustJSONUnmarshaler requires parameter value to exist to bind to destination implementing json.Unmarshaler interface.\n// Returns error when value does not exist\nfunc (b *ValueBinder) MustJSONUnmarshaler(sourceParam string, dest json.Unmarshaler) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\ttmp := b.ValueFunc(sourceParam)\n\tif tmp == \"\" {\n\t\tb.setError(b.ErrorFunc(sourceParam, []string{tmp}, \"required field value is empty\", nil))\n\t\treturn b\n\t}\n\n\tif err := dest.UnmarshalJSON([]byte(tmp)); err != nil {\n\t\tb.setError(b.ErrorFunc(sourceParam, []string{tmp}, \"failed to bind field value to json.Unmarshaler interface\", err))\n\t}\n\treturn b\n}\n\n// TextUnmarshaler binds parameter to destination implementing encoding.TextUnmarshaler interface\nfunc (b *ValueBinder) TextUnmarshaler(sourceParam string, dest encoding.TextUnmarshaler) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\ttmp := b.ValueFunc(sourceParam)\n\tif tmp == \"\" {\n\t\treturn b\n\t}\n\n\tif err := dest.UnmarshalText([]byte(tmp)); err != nil {\n\t\tb.setError(b.ErrorFunc(sourceParam, []string{tmp}, \"failed to bind field value to encoding.TextUnmarshaler interface\", err))\n\t}\n\treturn b\n}\n\n// MustTextUnmarshaler requires parameter value to exist to bind to destination implementing encoding.TextUnmarshaler interface.\n// Returns error when value does not exist\nfunc (b *ValueBinder) MustTextUnmarshaler(sourceParam string, dest encoding.TextUnmarshaler) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\ttmp := b.ValueFunc(sourceParam)\n\tif tmp == \"\" {\n\t\tb.setError(b.ErrorFunc(sourceParam, []string{tmp}, \"required field value is empty\", nil))\n\t\treturn b\n\t}\n\n\tif err := dest.UnmarshalText([]byte(tmp)); err != nil {\n\t\tb.setError(b.ErrorFunc(sourceParam, []string{tmp}, \"failed to bind field value to encoding.TextUnmarshaler interface\", err))\n\t}\n\treturn b\n}\n\n// BindWithDelimiter binds parameter to destination by suitable conversion function.\n// Delimiter is used before conversion to split parameter value to separate values\nfunc (b *ValueBinder) BindWithDelimiter(sourceParam string, dest any, delimiter string) *ValueBinder {\n\treturn b.bindWithDelimiter(sourceParam, dest, delimiter, false)\n}\n\n// MustBindWithDelimiter requires parameter value to exist to bind destination by suitable conversion function.\n// Delimiter is used before conversion to split parameter value to separate values\nfunc (b *ValueBinder) MustBindWithDelimiter(sourceParam string, dest any, delimiter string) *ValueBinder {\n\treturn b.bindWithDelimiter(sourceParam, dest, delimiter, true)\n}\n\nfunc (b *ValueBinder) bindWithDelimiter(sourceParam string, dest any, delimiter string, valueMustExist bool) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\tvalues := b.ValuesFunc(sourceParam)\n\tif len(values) == 0 {\n\t\tif valueMustExist {\n\t\t\tb.setError(b.ErrorFunc(sourceParam, []string{}, \"required field value is empty\", nil))\n\t\t}\n\t\treturn b\n\t}\n\ttmpValues := make([]string, 0, len(values))\n\tfor _, v := range values {\n\t\ttmpValues = append(tmpValues, strings.Split(v, delimiter)...)\n\t}\n\n\tswitch d := dest.(type) {\n\tcase *[]string:\n\t\t*d = tmpValues\n\t\treturn b\n\tcase *[]bool:\n\t\treturn b.bools(sourceParam, tmpValues, d)\n\tcase *[]int64, *[]int32, *[]int16, *[]int8, *[]int:\n\t\treturn b.ints(sourceParam, tmpValues, d)\n\tcase *[]uint64, *[]uint32, *[]uint16, *[]uint8, *[]uint: // *[]byte is same as *[]uint8\n\t\treturn b.uints(sourceParam, tmpValues, d)\n\tcase *[]float64, *[]float32:\n\t\treturn b.floats(sourceParam, tmpValues, d)\n\tcase *[]time.Duration:\n\t\treturn b.durations(sourceParam, tmpValues, d)\n\tdefault:\n\t\t// support only cases when destination is slice\n\t\t// does not support time.Time as it needs argument (layout) for parsing or BindUnmarshaler\n\t\tb.setError(b.ErrorFunc(sourceParam, []string{}, \"unsupported bind type\", nil))\n\t\treturn b\n\t}\n}\n\n// Int64 binds parameter to int64 variable\nfunc (b *ValueBinder) Int64(sourceParam string, dest *int64) *ValueBinder {\n\treturn b.intValue(sourceParam, dest, 64, false)\n}\n\n// MustInt64 requires parameter value to exist to bind to int64 variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustInt64(sourceParam string, dest *int64) *ValueBinder {\n\treturn b.intValue(sourceParam, dest, 64, true)\n}\n\n// Int32 binds parameter to int32 variable\nfunc (b *ValueBinder) Int32(sourceParam string, dest *int32) *ValueBinder {\n\treturn b.intValue(sourceParam, dest, 32, false)\n}\n\n// MustInt32 requires parameter value to exist to bind to int32 variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustInt32(sourceParam string, dest *int32) *ValueBinder {\n\treturn b.intValue(sourceParam, dest, 32, true)\n}\n\n// Int16 binds parameter to int16 variable\nfunc (b *ValueBinder) Int16(sourceParam string, dest *int16) *ValueBinder {\n\treturn b.intValue(sourceParam, dest, 16, false)\n}\n\n// MustInt16 requires parameter value to exist to bind to int16 variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustInt16(sourceParam string, dest *int16) *ValueBinder {\n\treturn b.intValue(sourceParam, dest, 16, true)\n}\n\n// Int8 binds parameter to int8 variable\nfunc (b *ValueBinder) Int8(sourceParam string, dest *int8) *ValueBinder {\n\treturn b.intValue(sourceParam, dest, 8, false)\n}\n\n// MustInt8 requires parameter value to exist to bind to int8 variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustInt8(sourceParam string, dest *int8) *ValueBinder {\n\treturn b.intValue(sourceParam, dest, 8, true)\n}\n\n// Int binds parameter to int variable\nfunc (b *ValueBinder) Int(sourceParam string, dest *int) *ValueBinder {\n\treturn b.intValue(sourceParam, dest, 0, false)\n}\n\n// MustInt requires parameter value to exist to bind to int variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustInt(sourceParam string, dest *int) *ValueBinder {\n\treturn b.intValue(sourceParam, dest, 0, true)\n}\n\nfunc (b *ValueBinder) intValue(sourceParam string, dest any, bitSize int, valueMustExist bool) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\tvalue := b.ValueFunc(sourceParam)\n\tif value == \"\" {\n\t\tif valueMustExist {\n\t\t\tb.setError(b.ErrorFunc(sourceParam, []string{}, \"required field value is empty\", nil))\n\t\t}\n\t\treturn b\n\t}\n\n\treturn b.int(sourceParam, value, dest, bitSize)\n}\n\nfunc (b *ValueBinder) int(sourceParam string, value string, dest any, bitSize int) *ValueBinder {\n\tn, err := strconv.ParseInt(value, 10, bitSize)\n\tif err != nil {\n\t\tif bitSize == 0 {\n\t\t\tb.setError(b.ErrorFunc(sourceParam, []string{value}, \"failed to bind field value to int\", err))\n\t\t} else {\n\t\t\tb.setError(b.ErrorFunc(sourceParam, []string{value}, fmt.Sprintf(\"failed to bind field value to int%v\", bitSize), err))\n\t\t}\n\t\treturn b\n\t}\n\n\tswitch d := dest.(type) {\n\tcase *int64:\n\t\t*d = n\n\tcase *int32:\n\t\t*d = int32(n) // #nosec G115\n\tcase *int16:\n\t\t*d = int16(n) // #nosec G115\n\tcase *int8:\n\t\t*d = int8(n) // #nosec G115\n\tcase *int:\n\t\t*d = int(n)\n\t}\n\treturn b\n}\n\nfunc (b *ValueBinder) intsValue(sourceParam string, dest any, valueMustExist bool) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\tvalues := b.ValuesFunc(sourceParam)\n\tif len(values) == 0 {\n\t\tif valueMustExist {\n\t\t\tb.setError(b.ErrorFunc(sourceParam, values, \"required field value is empty\", nil))\n\t\t}\n\t\treturn b\n\t}\n\treturn b.ints(sourceParam, values, dest)\n}\n\nfunc (b *ValueBinder) ints(sourceParam string, values []string, dest any) *ValueBinder {\n\tswitch d := dest.(type) {\n\tcase *[]int64:\n\t\ttmp := make([]int64, len(values))\n\t\tfor i, v := range values {\n\t\t\tb.int(sourceParam, v, &tmp[i], 64)\n\t\t\tif b.failFast && b.errors != nil {\n\t\t\t\treturn b\n\t\t\t}\n\t\t}\n\t\tif b.errors == nil {\n\t\t\t*d = tmp\n\t\t}\n\tcase *[]int32:\n\t\ttmp := make([]int32, len(values))\n\t\tfor i, v := range values {\n\t\t\tb.int(sourceParam, v, &tmp[i], 32)\n\t\t\tif b.failFast && b.errors != nil {\n\t\t\t\treturn b\n\t\t\t}\n\t\t}\n\t\tif b.errors == nil {\n\t\t\t*d = tmp\n\t\t}\n\tcase *[]int16:\n\t\ttmp := make([]int16, len(values))\n\t\tfor i, v := range values {\n\t\t\tb.int(sourceParam, v, &tmp[i], 16)\n\t\t\tif b.failFast && b.errors != nil {\n\t\t\t\treturn b\n\t\t\t}\n\t\t}\n\t\tif b.errors == nil {\n\t\t\t*d = tmp\n\t\t}\n\tcase *[]int8:\n\t\ttmp := make([]int8, len(values))\n\t\tfor i, v := range values {\n\t\t\tb.int(sourceParam, v, &tmp[i], 8)\n\t\t\tif b.failFast && b.errors != nil {\n\t\t\t\treturn b\n\t\t\t}\n\t\t}\n\t\tif b.errors == nil {\n\t\t\t*d = tmp\n\t\t}\n\tcase *[]int:\n\t\ttmp := make([]int, len(values))\n\t\tfor i, v := range values {\n\t\t\tb.int(sourceParam, v, &tmp[i], 0)\n\t\t\tif b.failFast && b.errors != nil {\n\t\t\t\treturn b\n\t\t\t}\n\t\t}\n\t\tif b.errors == nil {\n\t\t\t*d = tmp\n\t\t}\n\t}\n\treturn b\n}\n\n// Int64s binds parameter to slice of int64\nfunc (b *ValueBinder) Int64s(sourceParam string, dest *[]int64) *ValueBinder {\n\treturn b.intsValue(sourceParam, dest, false)\n}\n\n// MustInt64s requires parameter value to exist to bind to int64 slice variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustInt64s(sourceParam string, dest *[]int64) *ValueBinder {\n\treturn b.intsValue(sourceParam, dest, true)\n}\n\n// Int32s binds parameter to slice of int32\nfunc (b *ValueBinder) Int32s(sourceParam string, dest *[]int32) *ValueBinder {\n\treturn b.intsValue(sourceParam, dest, false)\n}\n\n// MustInt32s requires parameter value to exist to bind to int32 slice variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustInt32s(sourceParam string, dest *[]int32) *ValueBinder {\n\treturn b.intsValue(sourceParam, dest, true)\n}\n\n// Int16s binds parameter to slice of int16\nfunc (b *ValueBinder) Int16s(sourceParam string, dest *[]int16) *ValueBinder {\n\treturn b.intsValue(sourceParam, dest, false)\n}\n\n// MustInt16s requires parameter value to exist to bind to int16 slice variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustInt16s(sourceParam string, dest *[]int16) *ValueBinder {\n\treturn b.intsValue(sourceParam, dest, true)\n}\n\n// Int8s binds parameter to slice of int8\nfunc (b *ValueBinder) Int8s(sourceParam string, dest *[]int8) *ValueBinder {\n\treturn b.intsValue(sourceParam, dest, false)\n}\n\n// MustInt8s requires parameter value to exist to bind to int8 slice variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustInt8s(sourceParam string, dest *[]int8) *ValueBinder {\n\treturn b.intsValue(sourceParam, dest, true)\n}\n\n// Ints binds parameter to slice of int\nfunc (b *ValueBinder) Ints(sourceParam string, dest *[]int) *ValueBinder {\n\treturn b.intsValue(sourceParam, dest, false)\n}\n\n// MustInts requires parameter value to exist to bind to int slice variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustInts(sourceParam string, dest *[]int) *ValueBinder {\n\treturn b.intsValue(sourceParam, dest, true)\n}\n\n// Uint64 binds parameter to uint64 variable\nfunc (b *ValueBinder) Uint64(sourceParam string, dest *uint64) *ValueBinder {\n\treturn b.uintValue(sourceParam, dest, 64, false)\n}\n\n// MustUint64 requires parameter value to exist to bind to uint64 variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustUint64(sourceParam string, dest *uint64) *ValueBinder {\n\treturn b.uintValue(sourceParam, dest, 64, true)\n}\n\n// Uint32 binds parameter to uint32 variable\nfunc (b *ValueBinder) Uint32(sourceParam string, dest *uint32) *ValueBinder {\n\treturn b.uintValue(sourceParam, dest, 32, false)\n}\n\n// MustUint32 requires parameter value to exist to bind to uint32 variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustUint32(sourceParam string, dest *uint32) *ValueBinder {\n\treturn b.uintValue(sourceParam, dest, 32, true)\n}\n\n// Uint16 binds parameter to uint16 variable\nfunc (b *ValueBinder) Uint16(sourceParam string, dest *uint16) *ValueBinder {\n\treturn b.uintValue(sourceParam, dest, 16, false)\n}\n\n// MustUint16 requires parameter value to exist to bind to uint16 variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustUint16(sourceParam string, dest *uint16) *ValueBinder {\n\treturn b.uintValue(sourceParam, dest, 16, true)\n}\n\n// Uint8 binds parameter to uint8 variable\nfunc (b *ValueBinder) Uint8(sourceParam string, dest *uint8) *ValueBinder {\n\treturn b.uintValue(sourceParam, dest, 8, false)\n}\n\n// MustUint8 requires parameter value to exist to bind to uint8 variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustUint8(sourceParam string, dest *uint8) *ValueBinder {\n\treturn b.uintValue(sourceParam, dest, 8, true)\n}\n\n// Byte binds parameter to byte variable\nfunc (b *ValueBinder) Byte(sourceParam string, dest *byte) *ValueBinder {\n\treturn b.uintValue(sourceParam, dest, 8, false)\n}\n\n// MustByte requires parameter value to exist to bind to byte variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustByte(sourceParam string, dest *byte) *ValueBinder {\n\treturn b.uintValue(sourceParam, dest, 8, true)\n}\n\n// Uint binds parameter to uint variable\nfunc (b *ValueBinder) Uint(sourceParam string, dest *uint) *ValueBinder {\n\treturn b.uintValue(sourceParam, dest, 0, false)\n}\n\n// MustUint requires parameter value to exist to bind to uint variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustUint(sourceParam string, dest *uint) *ValueBinder {\n\treturn b.uintValue(sourceParam, dest, 0, true)\n}\n\nfunc (b *ValueBinder) uintValue(sourceParam string, dest any, bitSize int, valueMustExist bool) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\tvalue := b.ValueFunc(sourceParam)\n\tif value == \"\" {\n\t\tif valueMustExist {\n\t\t\tb.setError(b.ErrorFunc(sourceParam, []string{}, \"required field value is empty\", nil))\n\t\t}\n\t\treturn b\n\t}\n\n\treturn b.uint(sourceParam, value, dest, bitSize)\n}\n\nfunc (b *ValueBinder) uint(sourceParam string, value string, dest any, bitSize int) *ValueBinder {\n\tn, err := strconv.ParseUint(value, 10, bitSize)\n\tif err != nil {\n\t\tif bitSize == 0 {\n\t\t\tb.setError(b.ErrorFunc(sourceParam, []string{value}, \"failed to bind field value to uint\", err))\n\t\t} else {\n\t\t\tb.setError(b.ErrorFunc(sourceParam, []string{value}, fmt.Sprintf(\"failed to bind field value to uint%v\", bitSize), err))\n\t\t}\n\t\treturn b\n\t}\n\n\tswitch d := dest.(type) {\n\tcase *uint64:\n\t\t*d = n\n\tcase *uint32:\n\t\t*d = uint32(n) // #nosec G115\n\tcase *uint16:\n\t\t*d = uint16(n) // #nosec G115\n\tcase *uint8: // byte is alias to uint8\n\t\t*d = uint8(n) // #nosec G115\n\tcase *uint:\n\t\t*d = uint(n) // #nosec G115\n\t}\n\treturn b\n}\n\nfunc (b *ValueBinder) uintsValue(sourceParam string, dest any, valueMustExist bool) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\tvalues := b.ValuesFunc(sourceParam)\n\tif len(values) == 0 {\n\t\tif valueMustExist {\n\t\t\tb.setError(b.ErrorFunc(sourceParam, values, \"required field value is empty\", nil))\n\t\t}\n\t\treturn b\n\t}\n\treturn b.uints(sourceParam, values, dest)\n}\n\nfunc (b *ValueBinder) uints(sourceParam string, values []string, dest any) *ValueBinder {\n\tswitch d := dest.(type) {\n\tcase *[]uint64:\n\t\ttmp := make([]uint64, len(values))\n\t\tfor i, v := range values {\n\t\t\tb.uint(sourceParam, v, &tmp[i], 64)\n\t\t\tif b.failFast && b.errors != nil {\n\t\t\t\treturn b\n\t\t\t}\n\t\t}\n\t\tif b.errors == nil {\n\t\t\t*d = tmp\n\t\t}\n\tcase *[]uint32:\n\t\ttmp := make([]uint32, len(values))\n\t\tfor i, v := range values {\n\t\t\tb.uint(sourceParam, v, &tmp[i], 32)\n\t\t\tif b.failFast && b.errors != nil {\n\t\t\t\treturn b\n\t\t\t}\n\t\t}\n\t\tif b.errors == nil {\n\t\t\t*d = tmp\n\t\t}\n\tcase *[]uint16:\n\t\ttmp := make([]uint16, len(values))\n\t\tfor i, v := range values {\n\t\t\tb.uint(sourceParam, v, &tmp[i], 16)\n\t\t\tif b.failFast && b.errors != nil {\n\t\t\t\treturn b\n\t\t\t}\n\t\t}\n\t\tif b.errors == nil {\n\t\t\t*d = tmp\n\t\t}\n\tcase *[]uint8: // byte is alias to uint8\n\t\ttmp := make([]uint8, len(values))\n\t\tfor i, v := range values {\n\t\t\tb.uint(sourceParam, v, &tmp[i], 8)\n\t\t\tif b.failFast && b.errors != nil {\n\t\t\t\treturn b\n\t\t\t}\n\t\t}\n\t\tif b.errors == nil {\n\t\t\t*d = tmp\n\t\t}\n\tcase *[]uint:\n\t\ttmp := make([]uint, len(values))\n\t\tfor i, v := range values {\n\t\t\tb.uint(sourceParam, v, &tmp[i], 0)\n\t\t\tif b.failFast && b.errors != nil {\n\t\t\t\treturn b\n\t\t\t}\n\t\t}\n\t\tif b.errors == nil {\n\t\t\t*d = tmp\n\t\t}\n\t}\n\treturn b\n}\n\n// Uint64s binds parameter to slice of uint64\nfunc (b *ValueBinder) Uint64s(sourceParam string, dest *[]uint64) *ValueBinder {\n\treturn b.uintsValue(sourceParam, dest, false)\n}\n\n// MustUint64s requires parameter value to exist to bind to uint64 slice variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustUint64s(sourceParam string, dest *[]uint64) *ValueBinder {\n\treturn b.uintsValue(sourceParam, dest, true)\n}\n\n// Uint32s binds parameter to slice of uint32\nfunc (b *ValueBinder) Uint32s(sourceParam string, dest *[]uint32) *ValueBinder {\n\treturn b.uintsValue(sourceParam, dest, false)\n}\n\n// MustUint32s requires parameter value to exist to bind to uint32 slice variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustUint32s(sourceParam string, dest *[]uint32) *ValueBinder {\n\treturn b.uintsValue(sourceParam, dest, true)\n}\n\n// Uint16s binds parameter to slice of uint16\nfunc (b *ValueBinder) Uint16s(sourceParam string, dest *[]uint16) *ValueBinder {\n\treturn b.uintsValue(sourceParam, dest, false)\n}\n\n// MustUint16s requires parameter value to exist to bind to uint16 slice variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustUint16s(sourceParam string, dest *[]uint16) *ValueBinder {\n\treturn b.uintsValue(sourceParam, dest, true)\n}\n\n// Uint8s binds parameter to slice of uint8\nfunc (b *ValueBinder) Uint8s(sourceParam string, dest *[]uint8) *ValueBinder {\n\treturn b.uintsValue(sourceParam, dest, false)\n}\n\n// MustUint8s requires parameter value to exist to bind to uint8 slice variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustUint8s(sourceParam string, dest *[]uint8) *ValueBinder {\n\treturn b.uintsValue(sourceParam, dest, true)\n}\n\n// Uints binds parameter to slice of uint\nfunc (b *ValueBinder) Uints(sourceParam string, dest *[]uint) *ValueBinder {\n\treturn b.uintsValue(sourceParam, dest, false)\n}\n\n// MustUints requires parameter value to exist to bind to uint slice variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustUints(sourceParam string, dest *[]uint) *ValueBinder {\n\treturn b.uintsValue(sourceParam, dest, true)\n}\n\n// Bool binds parameter to bool variable\nfunc (b *ValueBinder) Bool(sourceParam string, dest *bool) *ValueBinder {\n\treturn b.boolValue(sourceParam, dest, false)\n}\n\n// MustBool requires parameter value to exist to bind to bool variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustBool(sourceParam string, dest *bool) *ValueBinder {\n\treturn b.boolValue(sourceParam, dest, true)\n}\n\nfunc (b *ValueBinder) boolValue(sourceParam string, dest *bool, valueMustExist bool) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\tvalue := b.ValueFunc(sourceParam)\n\tif value == \"\" {\n\t\tif valueMustExist {\n\t\t\tb.setError(b.ErrorFunc(sourceParam, []string{}, \"required field value is empty\", nil))\n\t\t}\n\t\treturn b\n\t}\n\treturn b.bool(sourceParam, value, dest)\n}\n\nfunc (b *ValueBinder) bool(sourceParam string, value string, dest *bool) *ValueBinder {\n\tn, err := strconv.ParseBool(value)\n\tif err != nil {\n\t\tb.setError(b.ErrorFunc(sourceParam, []string{value}, \"failed to bind field value to bool\", err))\n\t\treturn b\n\t}\n\n\t*dest = n\n\treturn b\n}\n\nfunc (b *ValueBinder) boolsValue(sourceParam string, dest *[]bool, valueMustExist bool) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\tvalues := b.ValuesFunc(sourceParam)\n\tif len(values) == 0 {\n\t\tif valueMustExist {\n\t\t\tb.setError(b.ErrorFunc(sourceParam, []string{}, \"required field value is empty\", nil))\n\t\t}\n\t\treturn b\n\t}\n\treturn b.bools(sourceParam, values, dest)\n}\n\nfunc (b *ValueBinder) bools(sourceParam string, values []string, dest *[]bool) *ValueBinder {\n\ttmp := make([]bool, len(values))\n\tfor i, v := range values {\n\t\tb.bool(sourceParam, v, &tmp[i])\n\t\tif b.failFast && b.errors != nil {\n\t\t\treturn b\n\t\t}\n\t}\n\tif b.errors == nil {\n\t\t*dest = tmp\n\t}\n\treturn b\n}\n\n// Bools binds parameter values to slice of bool variables\nfunc (b *ValueBinder) Bools(sourceParam string, dest *[]bool) *ValueBinder {\n\treturn b.boolsValue(sourceParam, dest, false)\n}\n\n// MustBools requires parameter values to exist to bind to slice of bool variables. Returns error when values does not exist\nfunc (b *ValueBinder) MustBools(sourceParam string, dest *[]bool) *ValueBinder {\n\treturn b.boolsValue(sourceParam, dest, true)\n}\n\n// Float64 binds parameter to float64 variable\nfunc (b *ValueBinder) Float64(sourceParam string, dest *float64) *ValueBinder {\n\treturn b.floatValue(sourceParam, dest, 64, false)\n}\n\n// MustFloat64 requires parameter value to exist to bind to float64 variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustFloat64(sourceParam string, dest *float64) *ValueBinder {\n\treturn b.floatValue(sourceParam, dest, 64, true)\n}\n\n// Float32 binds parameter to float32 variable\nfunc (b *ValueBinder) Float32(sourceParam string, dest *float32) *ValueBinder {\n\treturn b.floatValue(sourceParam, dest, 32, false)\n}\n\n// MustFloat32 requires parameter value to exist to bind to float32 variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustFloat32(sourceParam string, dest *float32) *ValueBinder {\n\treturn b.floatValue(sourceParam, dest, 32, true)\n}\n\nfunc (b *ValueBinder) floatValue(sourceParam string, dest any, bitSize int, valueMustExist bool) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\tvalue := b.ValueFunc(sourceParam)\n\tif value == \"\" {\n\t\tif valueMustExist {\n\t\t\tb.setError(b.ErrorFunc(sourceParam, []string{}, \"required field value is empty\", nil))\n\t\t}\n\t\treturn b\n\t}\n\n\treturn b.float(sourceParam, value, dest, bitSize)\n}\n\nfunc (b *ValueBinder) float(sourceParam string, value string, dest any, bitSize int) *ValueBinder {\n\tn, err := strconv.ParseFloat(value, bitSize)\n\tif err != nil {\n\t\tb.setError(b.ErrorFunc(sourceParam, []string{value}, fmt.Sprintf(\"failed to bind field value to float%v\", bitSize), err))\n\t\treturn b\n\t}\n\n\tswitch d := dest.(type) {\n\tcase *float64:\n\t\t*d = n\n\tcase *float32:\n\t\t*d = float32(n)\n\t}\n\treturn b\n}\n\nfunc (b *ValueBinder) floatsValue(sourceParam string, dest any, valueMustExist bool) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\tvalues := b.ValuesFunc(sourceParam)\n\tif len(values) == 0 {\n\t\tif valueMustExist {\n\t\t\tb.setError(b.ErrorFunc(sourceParam, []string{}, \"required field value is empty\", nil))\n\t\t}\n\t\treturn b\n\t}\n\treturn b.floats(sourceParam, values, dest)\n}\n\nfunc (b *ValueBinder) floats(sourceParam string, values []string, dest any) *ValueBinder {\n\tswitch d := dest.(type) {\n\tcase *[]float64:\n\t\ttmp := make([]float64, len(values))\n\t\tfor i, v := range values {\n\t\t\tb.float(sourceParam, v, &tmp[i], 64)\n\t\t\tif b.failFast && b.errors != nil {\n\t\t\t\treturn b\n\t\t\t}\n\t\t}\n\t\tif b.errors == nil {\n\t\t\t*d = tmp\n\t\t}\n\tcase *[]float32:\n\t\ttmp := make([]float32, len(values))\n\t\tfor i, v := range values {\n\t\t\tb.float(sourceParam, v, &tmp[i], 32)\n\t\t\tif b.failFast && b.errors != nil {\n\t\t\t\treturn b\n\t\t\t}\n\t\t}\n\t\tif b.errors == nil {\n\t\t\t*d = tmp\n\t\t}\n\t}\n\treturn b\n}\n\n// Float64s binds parameter values to slice of float64 variables\nfunc (b *ValueBinder) Float64s(sourceParam string, dest *[]float64) *ValueBinder {\n\treturn b.floatsValue(sourceParam, dest, false)\n}\n\n// MustFloat64s requires parameter values to exist to bind to slice of float64 variables. Returns error when values does not exist\nfunc (b *ValueBinder) MustFloat64s(sourceParam string, dest *[]float64) *ValueBinder {\n\treturn b.floatsValue(sourceParam, dest, true)\n}\n\n// Float32s binds parameter values to slice of float32 variables\nfunc (b *ValueBinder) Float32s(sourceParam string, dest *[]float32) *ValueBinder {\n\treturn b.floatsValue(sourceParam, dest, false)\n}\n\n// MustFloat32s requires parameter values to exist to bind to slice of float32 variables. Returns error when values does not exist\nfunc (b *ValueBinder) MustFloat32s(sourceParam string, dest *[]float32) *ValueBinder {\n\treturn b.floatsValue(sourceParam, dest, true)\n}\n\n// Time binds parameter to time.Time variable\nfunc (b *ValueBinder) Time(sourceParam string, dest *time.Time, layout string) *ValueBinder {\n\treturn b.time(sourceParam, dest, layout, false)\n}\n\n// MustTime requires parameter value to exist to bind to time.Time variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustTime(sourceParam string, dest *time.Time, layout string) *ValueBinder {\n\treturn b.time(sourceParam, dest, layout, true)\n}\n\nfunc (b *ValueBinder) time(sourceParam string, dest *time.Time, layout string, valueMustExist bool) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\tvalue := b.ValueFunc(sourceParam)\n\tif value == \"\" {\n\t\tif valueMustExist {\n\t\t\tb.setError(b.ErrorFunc(sourceParam, []string{value}, \"required field value is empty\", nil))\n\t\t}\n\t\treturn b\n\t}\n\tt, err := time.Parse(layout, value)\n\tif err != nil {\n\t\tb.setError(b.ErrorFunc(sourceParam, []string{value}, \"failed to bind field value to Time\", err))\n\t\treturn b\n\t}\n\t*dest = t\n\treturn b\n}\n\n// Times binds parameter values to slice of time.Time variables\nfunc (b *ValueBinder) Times(sourceParam string, dest *[]time.Time, layout string) *ValueBinder {\n\treturn b.times(sourceParam, dest, layout, false)\n}\n\n// MustTimes requires parameter values to exist to bind to slice of time.Time variables. Returns error when values does not exist\nfunc (b *ValueBinder) MustTimes(sourceParam string, dest *[]time.Time, layout string) *ValueBinder {\n\treturn b.times(sourceParam, dest, layout, true)\n}\n\nfunc (b *ValueBinder) times(sourceParam string, dest *[]time.Time, layout string, valueMustExist bool) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\tvalues := b.ValuesFunc(sourceParam)\n\tif len(values) == 0 {\n\t\tif valueMustExist {\n\t\t\tb.setError(b.ErrorFunc(sourceParam, []string{}, \"required field value is empty\", nil))\n\t\t}\n\t\treturn b\n\t}\n\n\ttmp := make([]time.Time, len(values))\n\tfor i, v := range values {\n\t\tt, err := time.Parse(layout, v)\n\t\tif err != nil {\n\t\t\tb.setError(b.ErrorFunc(sourceParam, []string{v}, \"failed to bind field value to Time\", err))\n\t\t\tif b.failFast {\n\t\t\t\treturn b\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\ttmp[i] = t\n\t}\n\tif b.errors == nil {\n\t\t*dest = tmp\n\t}\n\treturn b\n}\n\n// Duration binds parameter to time.Duration variable\nfunc (b *ValueBinder) Duration(sourceParam string, dest *time.Duration) *ValueBinder {\n\treturn b.duration(sourceParam, dest, false)\n}\n\n// MustDuration requires parameter value to exist to bind to time.Duration variable. Returns error when value does not exist\nfunc (b *ValueBinder) MustDuration(sourceParam string, dest *time.Duration) *ValueBinder {\n\treturn b.duration(sourceParam, dest, true)\n}\n\nfunc (b *ValueBinder) duration(sourceParam string, dest *time.Duration, valueMustExist bool) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\tvalue := b.ValueFunc(sourceParam)\n\tif value == \"\" {\n\t\tif valueMustExist {\n\t\t\tb.setError(b.ErrorFunc(sourceParam, []string{value}, \"required field value is empty\", nil))\n\t\t}\n\t\treturn b\n\t}\n\tt, err := time.ParseDuration(value)\n\tif err != nil {\n\t\tb.setError(b.ErrorFunc(sourceParam, []string{value}, \"failed to bind field value to Duration\", err))\n\t\treturn b\n\t}\n\t*dest = t\n\treturn b\n}\n\n// Durations binds parameter values to slice of time.Duration variables\nfunc (b *ValueBinder) Durations(sourceParam string, dest *[]time.Duration) *ValueBinder {\n\treturn b.durationsValue(sourceParam, dest, false)\n}\n\n// MustDurations requires parameter values to exist to bind to slice of time.Duration variables. Returns error when values does not exist\nfunc (b *ValueBinder) MustDurations(sourceParam string, dest *[]time.Duration) *ValueBinder {\n\treturn b.durationsValue(sourceParam, dest, true)\n}\n\nfunc (b *ValueBinder) durationsValue(sourceParam string, dest *[]time.Duration, valueMustExist bool) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\tvalues := b.ValuesFunc(sourceParam)\n\tif len(values) == 0 {\n\t\tif valueMustExist {\n\t\t\tb.setError(b.ErrorFunc(sourceParam, []string{}, \"required field value is empty\", nil))\n\t\t}\n\t\treturn b\n\t}\n\treturn b.durations(sourceParam, values, dest)\n}\n\nfunc (b *ValueBinder) durations(sourceParam string, values []string, dest *[]time.Duration) *ValueBinder {\n\ttmp := make([]time.Duration, len(values))\n\tfor i, v := range values {\n\t\tt, err := time.ParseDuration(v)\n\t\tif err != nil {\n\t\t\tb.setError(b.ErrorFunc(sourceParam, []string{v}, \"failed to bind field value to Duration\", err))\n\t\t\tif b.failFast {\n\t\t\t\treturn b\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\ttmp[i] = t\n\t}\n\tif b.errors == nil {\n\t\t*dest = tmp\n\t}\n\treturn b\n}\n\n// UnixTime binds parameter to time.Time variable (in local Time corresponding to the given Unix time).\n//\n// Example: 1609180603 bind to 2020-12-28T18:36:43.000000000+00:00\n//\n// Note:\n//   - time.Time{} (param is empty) and time.Unix(0,0) (param = \"0\") are not equal\nfunc (b *ValueBinder) UnixTime(sourceParam string, dest *time.Time) *ValueBinder {\n\treturn b.unixTime(sourceParam, dest, false, time.Second)\n}\n\n// MustUnixTime requires parameter value to exist to bind to time.Duration variable (in local time corresponding\n// to the given Unix time). Returns error when value does not exist.\n//\n// Example: 1609180603 bind to 2020-12-28T18:36:43.000000000+00:00\n//\n// Note:\n//   - time.Time{} (param is empty) and time.Unix(0,0) (param = \"0\") are not equal\nfunc (b *ValueBinder) MustUnixTime(sourceParam string, dest *time.Time) *ValueBinder {\n\treturn b.unixTime(sourceParam, dest, true, time.Second)\n}\n\n// UnixTimeMilli binds parameter to time.Time variable (in local time corresponding to the given Unix time in millisecond precision).\n//\n// Example: 1647184410140 bind to 2022-03-13T15:13:30.140000000+00:00\n//\n// Note:\n//   - time.Time{} (param is empty) and time.Unix(0,0) (param = \"0\") are not equal\nfunc (b *ValueBinder) UnixTimeMilli(sourceParam string, dest *time.Time) *ValueBinder {\n\treturn b.unixTime(sourceParam, dest, false, time.Millisecond)\n}\n\n// MustUnixTimeMilli requires parameter value to exist to bind to time.Duration variable  (in local time corresponding\n// to the given Unix time in millisecond precision). Returns error when value does not exist.\n//\n// Example: 1647184410140 bind to 2022-03-13T15:13:30.140000000+00:00\n//\n// Note:\n//   - time.Time{} (param is empty) and time.Unix(0,0) (param = \"0\") are not equal\nfunc (b *ValueBinder) MustUnixTimeMilli(sourceParam string, dest *time.Time) *ValueBinder {\n\treturn b.unixTime(sourceParam, dest, true, time.Millisecond)\n}\n\n// UnixTimeNano binds parameter to time.Time variable (in local time corresponding to the given Unix time in nanosecond precision).\n//\n// Example: 1609180603123456789 binds to 2020-12-28T18:36:43.123456789+00:00\n// Example:          1000000000 binds to 1970-01-01T00:00:01.000000000+00:00\n// Example:           999999999 binds to 1970-01-01T00:00:00.999999999+00:00\n//\n// Note:\n//   - time.Time{} (param is empty) and time.Unix(0,0) (param = \"0\") are not equal\n//   - Javascript's Number type only has about 53 bits of precision (Number.MAX_SAFE_INTEGER = 9007199254740991). Compare it to 1609180603123456789 in example.\nfunc (b *ValueBinder) UnixTimeNano(sourceParam string, dest *time.Time) *ValueBinder {\n\treturn b.unixTime(sourceParam, dest, false, time.Nanosecond)\n}\n\n// MustUnixTimeNano requires parameter value to exist to bind to time.Duration variable  (in local Time corresponding\n// to the given Unix time value in nano second precision). Returns error when value does not exist.\n//\n// Example: 1609180603123456789 binds to 2020-12-28T18:36:43.123456789+00:00\n// Example:          1000000000 binds to 1970-01-01T00:00:01.000000000+00:00\n// Example:           999999999 binds to 1970-01-01T00:00:00.999999999+00:00\n//\n// Note:\n//   - time.Time{} (param is empty) and time.Unix(0,0) (param = \"0\") are not equal\n//   - Javascript's Number type only has about 53 bits of precision (Number.MAX_SAFE_INTEGER = 9007199254740991). Compare it to 1609180603123456789 in example.\nfunc (b *ValueBinder) MustUnixTimeNano(sourceParam string, dest *time.Time) *ValueBinder {\n\treturn b.unixTime(sourceParam, dest, true, time.Nanosecond)\n}\n\nfunc (b *ValueBinder) unixTime(sourceParam string, dest *time.Time, valueMustExist bool, precision time.Duration) *ValueBinder {\n\tif b.failFast && b.errors != nil {\n\t\treturn b\n\t}\n\n\tvalue := b.ValueFunc(sourceParam)\n\tif value == \"\" {\n\t\tif valueMustExist {\n\t\t\tb.setError(b.ErrorFunc(sourceParam, []string{value}, \"required field value is empty\", nil))\n\t\t}\n\t\treturn b\n\t}\n\n\tn, err := strconv.ParseInt(value, 10, 64)\n\tif err != nil {\n\t\tb.setError(b.ErrorFunc(sourceParam, []string{value}, \"failed to bind field value to Time\", err))\n\t\treturn b\n\t}\n\n\tswitch precision {\n\tcase time.Second:\n\t\t*dest = time.Unix(n, 0)\n\tcase time.Millisecond:\n\t\t*dest = time.UnixMilli(n)\n\tcase time.Nanosecond:\n\t\t*dest = time.Unix(0, n)\n\t}\n\treturn b\n}\n"
  },
  {
    "path": "binder_external_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\n// run tests as external package to get real feel for API\npackage echo_test\n\nimport (\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\nfunc ExampleValueBinder_BindErrors() {\n\t// example route function that binds query params to different destinations and returns all bind errors in one go\n\trouteFunc := func(c *echo.Context) error {\n\t\tvar opts struct {\n\t\t\tIDs    []int64\n\t\t\tActive bool\n\t\t}\n\t\tlength := int64(50) // default length is 50\n\n\t\tb := echo.QueryParamsBinder(c)\n\n\t\terrs := b.Int64(\"length\", &length).\n\t\t\tInt64s(\"ids\", &opts.IDs).\n\t\t\tBool(\"active\", &opts.Active).\n\t\t\tBindErrors() // returns all errors\n\t\tif errs != nil {\n\t\t\tfor _, err := range errs {\n\t\t\t\tbErr := err.(*echo.BindingError)\n\t\t\t\tlog.Printf(\"in case you want to access what field: %s values: %v failed\", bErr.Field, bErr.Values)\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"%v fields failed to bind\", len(errs))\n\t\t}\n\t\tfmt.Printf(\"active = %v, length = %v, ids = %v\", opts.Active, length, opts.IDs)\n\n\t\treturn c.JSON(http.StatusOK, opts)\n\t}\n\n\te := echo.New()\n\tc := e.NewContext(\n\t\thttptest.NewRequest(http.MethodGet, \"/api/endpoint?active=true&length=25&ids=1&ids=2&ids=3\", nil),\n\t\thttptest.NewRecorder(),\n\t)\n\n\t_ = routeFunc(c)\n\n\t// Output: active = true, length = 25, ids = [1 2 3]\n}\n\nfunc ExampleValueBinder_BindError() {\n\t// example route function that binds query params to different destinations and stops binding on first bind error\n\tfailFastRouteFunc := func(c *echo.Context) error {\n\t\tvar opts struct {\n\t\t\tIDs    []int64\n\t\t\tActive bool\n\t\t}\n\t\tlength := int64(50) // default length is 50\n\n\t\t// create binder that stops binding at first error\n\t\tb := echo.QueryParamsBinder(c)\n\n\t\terr := b.Int64(\"length\", &length).\n\t\t\tInt64s(\"ids\", &opts.IDs).\n\t\t\tBool(\"active\", &opts.Active).\n\t\t\tBindError() // returns first binding error\n\t\tif err != nil {\n\t\t\tbErr := err.(*echo.BindingError)\n\t\t\treturn fmt.Errorf(\"my own custom error for field: %s values: %v\", bErr.Field, bErr.Values)\n\t\t}\n\t\tfmt.Printf(\"active = %v, length = %v, ids = %v\\n\", opts.Active, length, opts.IDs)\n\n\t\treturn c.JSON(http.StatusOK, opts)\n\t}\n\n\te := echo.New()\n\tc := e.NewContext(\n\t\thttptest.NewRequest(http.MethodGet, \"/api/endpoint?active=true&length=25&ids=1&ids=2&ids=3\", nil),\n\t\thttptest.NewRecorder(),\n\t)\n\n\t_ = failFastRouteFunc(c)\n\n\t// Output: active = true, length = 25, ids = [1 2 3]\n}\n\nfunc ExampleValueBinder_CustomFunc() {\n\t// example route function that binds query params using custom function closure\n\trouteFunc := func(c *echo.Context) error {\n\t\tlength := int64(50) // default length is 50\n\t\tvar binary []byte\n\n\t\tb := echo.QueryParamsBinder(c)\n\t\terrs := b.Int64(\"length\", &length).\n\t\t\tCustomFunc(\"base64\", func(values []string) []error {\n\t\t\t\tif len(values) == 0 {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tdecoded, err := base64.URLEncoding.DecodeString(values[0])\n\t\t\t\tif err != nil {\n\t\t\t\t\t// in this example we use only first param value but url could contain multiple params in reality and\n\t\t\t\t\t// therefore in theory produce multiple binding errors\n\t\t\t\t\treturn []error{echo.NewBindingError(\"base64\", values[0:1], \"failed to decode base64\", err)}\n\t\t\t\t}\n\t\t\t\tbinary = decoded\n\t\t\t\treturn nil\n\t\t\t}).\n\t\t\tBindErrors() // returns all errors\n\n\t\tif errs != nil {\n\t\t\tfor _, err := range errs {\n\t\t\t\tbErr := err.(*echo.BindingError)\n\t\t\t\tlog.Printf(\"in case you want to access what field: %s values: %v failed\", bErr.Field, bErr.Values)\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"%v fields failed to bind\", len(errs))\n\t\t}\n\t\tfmt.Printf(\"length = %v, base64 = %s\", length, binary)\n\n\t\treturn c.JSON(http.StatusOK, \"ok\")\n\t}\n\n\te := echo.New()\n\tc := e.NewContext(\n\t\thttptest.NewRequest(http.MethodGet, \"/api/endpoint?length=25&base64=SGVsbG8gV29ybGQ%3D\", nil),\n\t\thttptest.NewRecorder(),\n\t)\n\t_ = routeFunc(c)\n\n\t// Output: length = 25, base64 = Hello World\n}\n"
  },
  {
    "path": "binder_generic.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"encoding\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n)\n\n// TimeLayout specifies the format for parsing time values in request parameters.\n// It can be a standard Go time layout string or one of the special Unix time layouts.\ntype TimeLayout string\n\n// TimeOpts is options for parsing time.Time values\ntype TimeOpts struct {\n\t// Layout specifies the format for parsing time values in request parameters.\n\t// It can be a standard Go time layout string or one of the special Unix time layouts.\n\t//\n\t// Parsing layout defaults to: echo.TimeLayout(time.RFC3339Nano)\n\t// - To convert to custom layout use `echo.TimeLayout(\"2006-01-02\")`\n\t// - To convert unix timestamp (integer) to time.Time use `echo.TimeLayoutUnixTime`\n\t// - To convert unix timestamp in milliseconds to time.Time use `echo.TimeLayoutUnixTimeMilli`\n\t// - To convert unix timestamp in nanoseconds to time.Time use `echo.TimeLayoutUnixTimeNano`\n\tLayout TimeLayout\n\n\t// ParseInLocation is location used with time.ParseInLocation for layout that do not contain\n\t// timezone information to set output time in given location.\n\t// Defaults to time.UTC\n\tParseInLocation *time.Location\n\n\t// ToInLocation is location to which parsed time is converted to after parsing.\n\t// The parsed time will be converted using time.In(ToInLocation).\n\t// Defaults to time.UTC\n\tToInLocation *time.Location\n}\n\n// TimeLayout constants for parsing Unix timestamps in different precisions.\nconst (\n\tTimeLayoutUnixTime      = TimeLayout(\"UnixTime\")      // Unix timestamp in seconds\n\tTimeLayoutUnixTimeMilli = TimeLayout(\"UnixTimeMilli\") // Unix timestamp in milliseconds\n\tTimeLayoutUnixTimeNano  = TimeLayout(\"UnixTimeNano\")  // Unix timestamp in nanoseconds\n)\n\n// PathParam extracts and parses a path parameter from the context by name.\n// It returns the typed value and an error if binding fails. Returns ErrNonExistentKey if parameter not found.\n//\n// Empty String Handling:\n//   If the parameter exists but has an empty value, the zero value of type T is returned\n//   with no error. For example, a path parameter with value \"\" returns (0, nil) for int types.\n//   This differs from standard library behavior where parsing empty strings returns errors.\n//   To treat empty values as errors, validate the result separately or check the raw value.\n//\n// See ParseValue for supported types and options\nfunc PathParam[T any](c *Context, paramName string, opts ...any) (T, error) {\n\tfor _, pv := range c.PathValues() {\n\t\tif pv.Name == paramName {\n\t\t\tv, err := ParseValue[T](pv.Value, opts...)\n\t\t\tif err != nil {\n\t\t\t\treturn v, NewBindingError(paramName, []string{pv.Value}, \"path value\", err)\n\t\t\t}\n\t\t\treturn v, nil\n\t\t}\n\t}\n\tvar zero T\n\treturn zero, ErrNonExistentKey\n}\n\n// PathParamOr extracts and parses a path parameter from the context by name.\n// Returns defaultValue if the parameter is not found or has an empty value.\n// Returns an error only if parsing fails (e.g., \"abc\" for int type).\n//\n// Example:\n//   id, err := echo.PathParamOr[int](c, \"id\", 0)\n//   // If \"id\" is missing: returns (0, nil)\n//   // If \"id\" is \"123\": returns (123, nil)\n//   // If \"id\" is \"abc\": returns (0, BindingError)\n//\n// See ParseValue for supported types and options\nfunc PathParamOr[T any](c *Context, paramName string, defaultValue T, opts ...any) (T, error) {\n\tfor _, pv := range c.PathValues() {\n\t\tif pv.Name == paramName {\n\t\t\tv, err := ParseValueOr[T](pv.Value, defaultValue, opts...)\n\t\t\tif err != nil {\n\t\t\t\treturn v, NewBindingError(paramName, []string{pv.Value}, \"path value\", err)\n\t\t\t}\n\t\t\treturn v, nil\n\t\t}\n\t}\n\treturn defaultValue, nil\n}\n\n// QueryParam extracts and parses a single query parameter from the request by key.\n// It returns the typed value and an error if binding fails. Returns ErrNonExistentKey if parameter not found.\n//\n// Empty String Handling:\n//   If the parameter exists but has an empty value (?key=), the zero value of type T is returned\n//   with no error. For example, \"?count=\" returns (0, nil) for int types.\n//   This differs from standard library behavior where parsing empty strings returns errors.\n//   To treat empty values as errors, validate the result separately or check the raw value.\n//\n// Behavior Summary:\n//   - Missing key (?other=value): returns (zero, ErrNonExistentKey)\n//   - Empty value (?key=): returns (zero, nil)\n//   - Invalid value (?key=abc for int): returns (zero, BindingError)\n//\n// See ParseValue for supported types and options\nfunc QueryParam[T any](c *Context, key string, opts ...any) (T, error) {\n\tvalues, ok := c.QueryParams()[key]\n\tif !ok {\n\t\tvar zero T\n\t\treturn zero, ErrNonExistentKey\n\t}\n\tif len(values) == 0 {\n\t\tvar zero T\n\t\treturn zero, nil\n\t}\n\tvalue := values[0]\n\tv, err := ParseValue[T](value, opts...)\n\tif err != nil {\n\t\treturn v, NewBindingError(key, []string{value}, \"query param\", err)\n\t}\n\treturn v, nil\n}\n\n// QueryParamOr extracts and parses a single query parameter from the request by key.\n// Returns defaultValue if the parameter is not found or has an empty value.\n// Returns an error only if parsing fails (e.g., \"abc\" for int type).\n//\n// Example:\n//   page, err := echo.QueryParamOr[int](c, \"page\", 1)\n//   // If \"page\" is missing: returns (1, nil)\n//   // If \"page\" is \"5\": returns (5, nil)\n//   // If \"page\" is \"abc\": returns (1, BindingError)\n//\n// See ParseValue for supported types and options\nfunc QueryParamOr[T any](c *Context, key string, defaultValue T, opts ...any) (T, error) {\n\tvalues, ok := c.QueryParams()[key]\n\tif !ok {\n\t\treturn defaultValue, nil\n\t}\n\tif len(values) == 0 {\n\t\treturn defaultValue, nil\n\t}\n\tvalue := values[0]\n\tv, err := ParseValueOr[T](value, defaultValue, opts...)\n\tif err != nil {\n\t\treturn v, NewBindingError(key, []string{value}, \"query param\", err)\n\t}\n\treturn v, nil\n}\n\n// QueryParams extracts and parses all values for a query parameter key as a slice.\n// It returns the typed slice and an error if binding any value fails. Returns ErrNonExistentKey if parameter not found.\n//\n// See ParseValues for supported types and options\nfunc QueryParams[T any](c *Context, key string, opts ...any) ([]T, error) {\n\tvalues, ok := c.QueryParams()[key]\n\tif !ok {\n\t\treturn nil, ErrNonExistentKey\n\t}\n\n\tresult, err := ParseValues[T](values, opts...)\n\tif err != nil {\n\t\treturn nil, NewBindingError(key, values, \"query params\", err)\n\t}\n\treturn result, nil\n}\n\n// QueryParamsOr extracts and parses all values for a query parameter key as a slice.\n// Returns defaultValue if the parameter is not found.\n// Returns an error only if parsing any value fails.\n//\n// Example:\n//   ids, err := echo.QueryParamsOr[int](c, \"ids\", []int{})\n//   // If \"ids\" is missing: returns ([], nil)\n//   // If \"ids\" is \"1&ids=2\": returns ([1, 2], nil)\n//   // If \"ids\" contains \"abc\": returns ([], BindingError)\n//\n// See ParseValues for supported types and options\nfunc QueryParamsOr[T any](c *Context, key string, defaultValue []T, opts ...any) ([]T, error) {\n\tvalues, ok := c.QueryParams()[key]\n\tif !ok {\n\t\treturn defaultValue, nil\n\t}\n\n\tresult, err := ParseValuesOr[T](values, defaultValue, opts...)\n\tif err != nil {\n\t\treturn nil, NewBindingError(key, values, \"query params\", err)\n\t}\n\treturn result, nil\n}\n\n// FormValue extracts and parses a single form value from the request by key.\n// It returns the typed value and an error if binding fails. Returns ErrNonExistentKey if parameter not found.\n//\n// Empty String Handling:\n//   If the form field exists but has an empty value, the zero value of type T is returned\n//   with no error. For example, an empty form field returns (0, nil) for int types.\n//   This differs from standard library behavior where parsing empty strings returns errors.\n//   To treat empty values as errors, validate the result separately or check the raw value.\n//\n// See ParseValue for supported types and options\nfunc FormValue[T any](c *Context, key string, opts ...any) (T, error) {\n\tformValues, err := c.FormValues()\n\tif err != nil {\n\t\tvar zero T\n\t\treturn zero, fmt.Errorf(\"failed to parse form value, key: %s, err: %w\", key, err)\n\t}\n\tvalues, ok := formValues[key]\n\tif !ok {\n\t\tvar zero T\n\t\treturn zero, ErrNonExistentKey\n\t}\n\tif len(values) == 0 {\n\t\tvar zero T\n\t\treturn zero, nil\n\t}\n\tvalue := values[0]\n\tv, err := ParseValue[T](value, opts...)\n\tif err != nil {\n\t\treturn v, NewBindingError(key, []string{value}, \"form value\", err)\n\t}\n\treturn v, nil\n}\n\n// FormValueOr extracts and parses a single form value from the request by key.\n// Returns defaultValue if the parameter is not found or has an empty value.\n// Returns an error only if parsing fails or form parsing errors occur.\n//\n// Example:\n//   limit, err := echo.FormValueOr[int](c, \"limit\", 100)\n//   // If \"limit\" is missing: returns (100, nil)\n//   // If \"limit\" is \"50\": returns (50, nil)\n//   // If \"limit\" is \"abc\": returns (100, BindingError)\n//\n// See ParseValue for supported types and options\nfunc FormValueOr[T any](c *Context, key string, defaultValue T, opts ...any) (T, error) {\n\tformValues, err := c.FormValues()\n\tif err != nil {\n\t\tvar zero T\n\t\treturn zero, fmt.Errorf(\"failed to parse form value, key: %s, err: %w\", key, err)\n\t}\n\tvalues, ok := formValues[key]\n\tif !ok {\n\t\treturn defaultValue, nil\n\t}\n\tif len(values) == 0 {\n\t\treturn defaultValue, nil\n\t}\n\tvalue := values[0]\n\tv, err := ParseValueOr[T](value, defaultValue, opts...)\n\tif err != nil {\n\t\treturn v, NewBindingError(key, []string{value}, \"form value\", err)\n\t}\n\treturn v, nil\n}\n\n// FormValues extracts and parses all values for a form values key as a slice.\n// It returns the typed slice and an error if binding any value fails. Returns ErrNonExistentKey if parameter not found.\n//\n// See ParseValues for supported types and options\nfunc FormValues[T any](c *Context, key string, opts ...any) ([]T, error) {\n\tformValues, err := c.FormValues()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse form values, key: %s, err: %w\", key, err)\n\t}\n\tvalues, ok := formValues[key]\n\tif !ok {\n\t\treturn nil, ErrNonExistentKey\n\t}\n\tresult, err := ParseValues[T](values, opts...)\n\tif err != nil {\n\t\treturn nil, NewBindingError(key, values, \"form values\", err)\n\t}\n\treturn result, nil\n}\n\n// FormValuesOr extracts and parses all values for a form values key as a slice.\n// Returns defaultValue if the parameter is not found.\n// Returns an error only if parsing any value fails or form parsing errors occur.\n//\n// Example:\n//   tags, err := echo.FormValuesOr[string](c, \"tags\", []string{})\n//   // If \"tags\" is missing: returns ([], nil)\n//   // If form parsing fails: returns (nil, error)\n//\n// See ParseValues for supported types and options\nfunc FormValuesOr[T any](c *Context, key string, defaultValue []T, opts ...any) ([]T, error) {\n\tformValues, err := c.FormValues()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse form values, key: %s, err: %w\", key, err)\n\t}\n\tvalues, ok := formValues[key]\n\tif !ok {\n\t\treturn defaultValue, nil\n\t}\n\tresult, err := ParseValuesOr[T](values, defaultValue, opts...)\n\tif err != nil {\n\t\treturn nil, NewBindingError(key, values, \"form values\", err)\n\t}\n\treturn result, nil\n}\n\n// ParseValues parses value to generic type slice. Same types are supported as ParseValue\n// function but the result type is slice instead of scalar value.\n//\n// See ParseValue for supported types and options\nfunc ParseValues[T any](values []string, opts ...any) ([]T, error) {\n\tvar zero []T\n\treturn ParseValuesOr(values, zero, opts...)\n}\n\n// ParseValuesOr parses value to generic type slice, when value is empty defaultValue is returned.\n// Same types are supported as ParseValue function but the result type is slice instead of scalar value.\n//\n// See ParseValue for supported types and options\nfunc ParseValuesOr[T any](values []string, defaultValue []T, opts ...any) ([]T, error) {\n\tif len(values) == 0 {\n\t\treturn defaultValue, nil\n\t}\n\tresult := make([]T, 0, len(values))\n\tfor _, v := range values {\n\t\ttmp, err := ParseValue[T](v, opts...)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tresult = append(result, tmp)\n\t}\n\treturn result, nil\n}\n\n// ParseValue parses value to generic type\n//\n// Types that are supported:\n//   - bool\n//   - float32\n//   - float64\n//   - int\n//   - int8\n//   - int16\n//   - int32\n//   - int64\n//   - uint\n//   - uint8/byte\n//   - uint16\n//   - uint32\n//   - uint64\n//   - string\n//   - echo.BindUnmarshaler interface\n//   - encoding.TextUnmarshaler interface\n//   - json.Unmarshaler interface\n//   - time.Duration\n//   - time.Time use echo.TimeOpts or echo.TimeLayout to set time parsing configuration\nfunc ParseValue[T any](value string, opts ...any) (T, error) {\n\tvar zero T\n\treturn ParseValueOr(value, zero, opts...)\n}\n\n// ParseValueOr parses value to generic type, when value is empty defaultValue is returned.\n//\n// Types that are supported:\n//   - bool\n//   - float32\n//   - float64\n//   - int\n//   - int8\n//   - int16\n//   - int32\n//   - int64\n//   - uint\n//   - uint8/byte\n//   - uint16\n//   - uint32\n//   - uint64\n//   - string\n//   - echo.BindUnmarshaler interface\n//   - encoding.TextUnmarshaler interface\n//   - json.Unmarshaler interface\n//   - time.Duration\n//   - time.Time use echo.TimeOpts or echo.TimeLayout to set time parsing configuration\nfunc ParseValueOr[T any](value string, defaultValue T, opts ...any) (T, error) {\n\tif len(value) == 0 {\n\t\treturn defaultValue, nil\n\t}\n\tvar tmp T\n\tif err := bindValue(value, &tmp, opts...); err != nil {\n\t\tvar zero T\n\t\treturn zero, fmt.Errorf(\"failed to parse value, err: %w\", err)\n\t}\n\treturn tmp, nil\n}\n\nfunc bindValue(value string, dest any, opts ...any) error {\n\t// NOTE: if this function is ever made public the dest should be checked for nil\n\t// values when dealing with interfaces\n\tif len(opts) > 0 {\n\t\tif _, isTime := dest.(*time.Time); !isTime {\n\t\t\treturn fmt.Errorf(\"options are only supported for time.Time, got %T\", dest)\n\t\t}\n\t}\n\n\tswitch d := dest.(type) {\n\tcase *bool:\n\t\tn, err := strconv.ParseBool(value)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*d = n\n\tcase *float32:\n\t\tn, err := strconv.ParseFloat(value, 32)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*d = float32(n)\n\tcase *float64:\n\t\tn, err := strconv.ParseFloat(value, 64)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*d = n\n\tcase *int:\n\t\tn, err := strconv.ParseInt(value, 10, 0)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*d = int(n)\n\tcase *int8:\n\t\tn, err := strconv.ParseInt(value, 10, 8)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*d = int8(n)\n\tcase *int16:\n\t\tn, err := strconv.ParseInt(value, 10, 16)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*d = int16(n)\n\tcase *int32:\n\t\tn, err := strconv.ParseInt(value, 10, 32)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*d = int32(n)\n\tcase *int64:\n\t\tn, err := strconv.ParseInt(value, 10, 64)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*d = n\n\tcase *uint:\n\t\tn, err := strconv.ParseUint(value, 10, 0)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*d = uint(n)\n\tcase *uint8:\n\t\tn, err := strconv.ParseUint(value, 10, 8)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*d = uint8(n)\n\tcase *uint16:\n\t\tn, err := strconv.ParseUint(value, 10, 16)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*d = uint16(n)\n\tcase *uint32:\n\t\tn, err := strconv.ParseUint(value, 10, 32)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*d = uint32(n)\n\tcase *uint64:\n\t\tn, err := strconv.ParseUint(value, 10, 64)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*d = n\n\tcase *string:\n\t\t*d = value\n\tcase *time.Duration:\n\t\tt, err := time.ParseDuration(value)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*d = t\n\tcase *time.Time:\n\t\tto := TimeOpts{\n\t\t\tLayout:          TimeLayout(time.RFC3339Nano),\n\t\t\tParseInLocation: time.UTC,\n\t\t\tToInLocation:    time.UTC,\n\t\t}\n\t\tfor _, o := range opts {\n\t\t\tswitch v := o.(type) {\n\t\t\tcase TimeOpts:\n\t\t\t\tif v.Layout != \"\" {\n\t\t\t\t\tto.Layout = v.Layout\n\t\t\t\t}\n\t\t\t\tif v.ParseInLocation != nil {\n\t\t\t\t\tto.ParseInLocation = v.ParseInLocation\n\t\t\t\t}\n\t\t\t\tif v.ToInLocation != nil {\n\t\t\t\t\tto.ToInLocation = v.ToInLocation\n\t\t\t\t}\n\t\t\tcase TimeLayout:\n\t\t\t\tto.Layout = v\n\t\t\t}\n\t\t}\n\t\tvar t time.Time\n\t\tvar err error\n\t\tswitch to.Layout {\n\t\tcase TimeLayoutUnixTime:\n\t\t\tn, err := strconv.ParseInt(value, 10, 64)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tt = time.Unix(n, 0)\n\t\tcase TimeLayoutUnixTimeMilli:\n\t\t\tn, err := strconv.ParseInt(value, 10, 64)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tt = time.UnixMilli(n)\n\t\tcase TimeLayoutUnixTimeNano:\n\t\t\tn, err := strconv.ParseInt(value, 10, 64)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tt = time.Unix(0, n)\n\t\tdefault:\n\t\t\tif to.ParseInLocation != nil {\n\t\t\t\tt, err = time.ParseInLocation(string(to.Layout), value, to.ParseInLocation)\n\t\t\t} else {\n\t\t\t\tt, err = time.Parse(string(to.Layout), value)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\t*d = t.In(to.ToInLocation)\n\tcase BindUnmarshaler:\n\t\tif err := d.UnmarshalParam(value); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase encoding.TextUnmarshaler:\n\t\tif err := d.UnmarshalText([]byte(value)); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase json.Unmarshaler:\n\t\tif err := d.UnmarshalJSON([]byte(value)); err != nil {\n\t\t\treturn err\n\t\t}\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported value type: %T\", dest)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "binder_generic_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"cmp\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"math\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// TextUnmarshalerType implements encoding.TextUnmarshaler but NOT BindUnmarshaler\ntype TextUnmarshalerType struct {\n\tValue string\n}\n\nfunc (t *TextUnmarshalerType) UnmarshalText(data []byte) error {\n\ts := string(data)\n\tif s == \"invalid\" {\n\t\treturn fmt.Errorf(\"invalid value: %s\", s)\n\t}\n\tt.Value = strings.ToUpper(s)\n\treturn nil\n}\n\n// JSONUnmarshalerType implements json.Unmarshaler but NOT BindUnmarshaler or TextUnmarshaler\ntype JSONUnmarshalerType struct {\n\tValue string\n}\n\nfunc (j *JSONUnmarshalerType) UnmarshalJSON(data []byte) error {\n\treturn json.Unmarshal(data, &j.Value)\n}\n\nfunc TestPathParam(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname       string\n\t\tgivenKey   string\n\t\tgivenValue string\n\t\texpect     bool\n\t\texpectErr  string\n\t}{\n\t\t{\n\t\t\tname:       \"ok\",\n\t\t\tgivenValue: \"true\",\n\t\t\texpect:     true,\n\t\t},\n\t\t{\n\t\t\tname:       \"nok, non existent key\",\n\t\t\tgivenKey:   \"missing\",\n\t\t\tgivenValue: \"true\",\n\t\t\texpect:     false,\n\t\t\texpectErr:  ErrNonExistentKey.Error(),\n\t\t},\n\t\t{\n\t\t\tname:       \"nok, invalid value\",\n\t\t\tgivenValue: \"can_parse_me\",\n\t\t\texpect:     false,\n\t\t\texpectErr:  `code=400, message=path value, err=failed to parse value, err: strconv.ParseBool: parsing \"can_parse_me\": invalid syntax, field=key`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := NewContext(nil, nil)\n\t\t\tc.SetPathValues(PathValues{{\n\t\t\t\tName:  cmp.Or(tc.givenKey, \"key\"),\n\t\t\t\tValue: tc.givenValue,\n\t\t\t}})\n\n\t\t\tv, err := PathParam[bool](c, \"key\")\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestPathParam_UnsupportedType(t *testing.T) {\n\tc := NewContext(nil, nil)\n\tc.SetPathValues(PathValues{{Name: \"key\", Value: \"true\"}})\n\n\tv, err := PathParam[[]bool](c, \"key\")\n\n\texpectErr := \"code=400, message=path value, err=failed to parse value, err: unsupported value type: *[]bool, field=key\"\n\tassert.EqualError(t, err, expectErr)\n\tassert.Equal(t, []bool(nil), v)\n}\n\nfunc TestQueryParam(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\tgivenURL  string\n\t\texpect    bool\n\t\texpectErr string\n\t}{\n\t\t{\n\t\t\tname:     \"ok\",\n\t\t\tgivenURL: \"/?key=true\",\n\t\t\texpect:   true,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, non existent key\",\n\t\t\tgivenURL:  \"/?different=true\",\n\t\t\texpect:    false,\n\t\t\texpectErr: ErrNonExistentKey.Error(),\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, invalid value\",\n\t\t\tgivenURL:  \"/?key=invalidbool\",\n\t\t\texpect:    false,\n\t\t\texpectErr: `code=400, message=query param, err=failed to parse value, err: strconv.ParseBool: parsing \"invalidbool\": invalid syntax, field=key`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodPost, tc.givenURL, nil)\n\t\t\tc := NewContext(req, nil)\n\n\t\t\tv, err := QueryParam[bool](c, \"key\")\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestQueryParam_UnsupportedType(t *testing.T) {\n\treq := httptest.NewRequest(http.MethodPost, \"/?key=bool\", nil)\n\tc := NewContext(req, nil)\n\n\tv, err := QueryParam[[]bool](c, \"key\")\n\n\texpectErr := \"code=400, message=query param, err=failed to parse value, err: unsupported value type: *[]bool, field=key\"\n\tassert.EqualError(t, err, expectErr)\n\tassert.Equal(t, []bool(nil), v)\n}\n\nfunc TestQueryParams(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\tgivenURL  string\n\t\texpect    []bool\n\t\texpectErr string\n\t}{\n\t\t{\n\t\t\tname:     \"ok\",\n\t\t\tgivenURL: \"/?key=true&key=false\",\n\t\t\texpect:   []bool{true, false},\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, non existent key\",\n\t\t\tgivenURL:  \"/?different=true\",\n\t\t\texpect:    []bool(nil),\n\t\t\texpectErr: ErrNonExistentKey.Error(),\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, invalid value\",\n\t\t\tgivenURL:  \"/?key=true&key=invalidbool\",\n\t\t\texpect:    []bool(nil),\n\t\t\texpectErr: `code=400, message=query params, err=failed to parse value, err: strconv.ParseBool: parsing \"invalidbool\": invalid syntax, field=key`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodPost, tc.givenURL, nil)\n\t\t\tc := NewContext(req, nil)\n\n\t\t\tv, err := QueryParams[bool](c, \"key\")\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestQueryParams_UnsupportedType(t *testing.T) {\n\treq := httptest.NewRequest(http.MethodPost, \"/?key=bool\", nil)\n\tc := NewContext(req, nil)\n\n\tv, err := QueryParams[[]bool](c, \"key\")\n\n\texpectErr := \"code=400, message=query params, err=failed to parse value, err: unsupported value type: *[]bool, field=key\"\n\tassert.EqualError(t, err, expectErr)\n\tassert.Equal(t, [][]bool(nil), v)\n}\n\nfunc TestFormValue(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\tgivenURL  string\n\t\texpect    bool\n\t\texpectErr string\n\t}{\n\t\t{\n\t\t\tname:     \"ok\",\n\t\t\tgivenURL: \"/?key=true\",\n\t\t\texpect:   true,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, non existent key\",\n\t\t\tgivenURL:  \"/?different=true\",\n\t\t\texpect:    false,\n\t\t\texpectErr: ErrNonExistentKey.Error(),\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, invalid value\",\n\t\t\tgivenURL:  \"/?key=invalidbool\",\n\t\t\texpect:    false,\n\t\t\texpectErr: `code=400, message=form value, err=failed to parse value, err: strconv.ParseBool: parsing \"invalidbool\": invalid syntax, field=key`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodPost, tc.givenURL, nil)\n\t\t\tc := NewContext(req, nil)\n\n\t\t\tv, err := FormValue[bool](c, \"key\")\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestFormValue_UnsupportedType(t *testing.T) {\n\treq := httptest.NewRequest(http.MethodPost, \"/?key=bool\", nil)\n\tc := NewContext(req, nil)\n\n\tv, err := FormValue[[]bool](c, \"key\")\n\n\texpectErr := \"code=400, message=form value, err=failed to parse value, err: unsupported value type: *[]bool, field=key\"\n\tassert.EqualError(t, err, expectErr)\n\tassert.Equal(t, []bool(nil), v)\n}\n\nfunc TestFormValues(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\tgivenURL  string\n\t\texpect    []bool\n\t\texpectErr string\n\t}{\n\t\t{\n\t\t\tname:     \"ok\",\n\t\t\tgivenURL: \"/?key=true&key=false\",\n\t\t\texpect:   []bool{true, false},\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, non existent key\",\n\t\t\tgivenURL:  \"/?different=true\",\n\t\t\texpect:    []bool(nil),\n\t\t\texpectErr: ErrNonExistentKey.Error(),\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, invalid value\",\n\t\t\tgivenURL:  \"/?key=true&key=invalidbool\",\n\t\t\texpect:    []bool(nil),\n\t\t\texpectErr: `code=400, message=form values, err=failed to parse value, err: strconv.ParseBool: parsing \"invalidbool\": invalid syntax, field=key`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodPost, tc.givenURL, nil)\n\t\t\tc := NewContext(req, nil)\n\n\t\t\tv, err := FormValues[bool](c, \"key\")\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestFormValues_UnsupportedType(t *testing.T) {\n\treq := httptest.NewRequest(http.MethodPost, \"/?key=bool\", nil)\n\tc := NewContext(req, nil)\n\n\tv, err := FormValues[[]bool](c, \"key\")\n\n\texpectErr := \"code=400, message=form values, err=failed to parse value, err: unsupported value type: *[]bool, field=key\"\n\tassert.EqualError(t, err, expectErr)\n\tassert.Equal(t, [][]bool(nil), v)\n}\n\nfunc TestParseValue_bool(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\twhen      string\n\t\texpect    bool\n\t\texpectErr error\n\t}{\n\t\t{\n\t\t\tname:   \"ok, true\",\n\t\t\twhen:   \"true\",\n\t\t\texpect: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, false\",\n\t\t\twhen:   \"false\",\n\t\t\texpect: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, 1\",\n\t\t\twhen:   \"1\",\n\t\t\texpect: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, 0\",\n\t\t\twhen:   \"0\",\n\t\t\texpect: false,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tv, err := ParseValue[bool](tc.when)\n\t\t\tif tc.expectErr != nil {\n\t\t\t\tassert.ErrorIs(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestParseValue_float32(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\twhen      string\n\t\texpect    float32\n\t\texpectErr string\n\t}{\n\t\t{\n\t\t\tname:   \"ok, 123.345\",\n\t\t\twhen:   \"123.345\",\n\t\t\texpect: 123.345,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, 0\",\n\t\t\twhen:   \"0\",\n\t\t\texpect: 0,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, Inf\",\n\t\t\twhen:   \"+Inf\",\n\t\t\texpect: float32(math.Inf(1)),\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, Inf\",\n\t\t\twhen:   \"-Inf\",\n\t\t\texpect: float32(math.Inf(-1)),\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, NaN\",\n\t\t\twhen:   \"NaN\",\n\t\t\texpect: float32(math.NaN()),\n\t\t},\n\t\t{\n\t\t\tname:      \"ok, invalid value\",\n\t\t\twhen:      \"X\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseFloat: parsing \"X\": invalid syntax`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tv, err := ParseValue[float32](tc.when)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tif math.IsNaN(float64(tc.expect)) {\n\t\t\t\tif !math.IsNaN(float64(v)) {\n\t\t\t\t\tt.Fatal(\"expected NaN but got non NaN\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, tc.expect, v)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseValue_float64(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\twhen      string\n\t\texpect    float64\n\t\texpectErr string\n\t}{\n\t\t{\n\t\t\tname:   \"ok, 123.345\",\n\t\t\twhen:   \"123.345\",\n\t\t\texpect: 123.345,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, 0\",\n\t\t\twhen:   \"0\",\n\t\t\texpect: 0,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, Inf\",\n\t\t\twhen:   \"+Inf\",\n\t\t\texpect: math.Inf(1),\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, Inf\",\n\t\t\twhen:   \"-Inf\",\n\t\t\texpect: math.Inf(-1),\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, NaN\",\n\t\t\twhen:   \"NaN\",\n\t\t\texpect: math.NaN(),\n\t\t},\n\t\t{\n\t\t\tname:      \"ok, invalid value\",\n\t\t\twhen:      \"X\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseFloat: parsing \"X\": invalid syntax`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tv, err := ParseValue[float64](tc.when)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tif math.IsNaN(tc.expect) {\n\t\t\t\tif !math.IsNaN(v) {\n\t\t\t\t\tt.Fatal(\"expected NaN but got non NaN\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, tc.expect, v)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseValue_int(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\twhen      string\n\t\texpect    int\n\t\texpectErr string\n\t}{\n\t\t{\n\t\t\tname:   \"ok, 0\",\n\t\t\twhen:   \"0\",\n\t\t\texpect: 0,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, 1\",\n\t\t\twhen:   \"1\",\n\t\t\texpect: 1,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, -1\",\n\t\t\twhen:   \"-1\",\n\t\t\texpect: -1,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, max int (64bit)\",\n\t\t\twhen:   \"9223372036854775807\",\n\t\t\texpect: 9223372036854775807,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, min int (64bit)\",\n\t\t\twhen:   \"-9223372036854775808\",\n\t\t\texpect: -9223372036854775808,\n\t\t},\n\t\t{\n\t\t\tname:      \"ok, overflow max int (64bit)\",\n\t\t\twhen:      \"9223372036854775808\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseInt: parsing \"9223372036854775808\": value out of range`,\n\t\t},\n\t\t{\n\t\t\tname:      \"ok, underflow min int (64bit)\",\n\t\t\twhen:      \"-9223372036854775809\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseInt: parsing \"-9223372036854775809\": value out of range`,\n\t\t},\n\t\t{\n\t\t\tname:      \"ok, invalid value\",\n\t\t\twhen:      \"X\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseInt: parsing \"X\": invalid syntax`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tv, err := ParseValue[int](tc.when)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestParseValue_uint(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\twhen      string\n\t\texpect    uint\n\t\texpectErr string\n\t}{\n\t\t{\n\t\t\tname:   \"ok, 0\",\n\t\t\twhen:   \"0\",\n\t\t\texpect: 0,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, 1\",\n\t\t\twhen:   \"1\",\n\t\t\texpect: 1,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, max uint (64bit)\",\n\t\t\twhen:   \"18446744073709551615\",\n\t\t\texpect: 18446744073709551615,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, overflow max uint (64bit)\",\n\t\t\twhen:      \"18446744073709551616\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseUint: parsing \"18446744073709551616\": value out of range`,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, negative value\",\n\t\t\twhen:      \"-1\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseUint: parsing \"-1\": invalid syntax`,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, invalid value\",\n\t\t\twhen:      \"X\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseUint: parsing \"X\": invalid syntax`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tv, err := ParseValue[uint](tc.when)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestParseValue_int8(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\twhen      string\n\t\texpect    int8\n\t\texpectErr string\n\t}{\n\t\t{\n\t\t\tname:   \"ok, 0\",\n\t\t\twhen:   \"0\",\n\t\t\texpect: 0,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, 1\",\n\t\t\twhen:   \"1\",\n\t\t\texpect: 1,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, -1\",\n\t\t\twhen:   \"-1\",\n\t\t\texpect: -1,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, max int8\",\n\t\t\twhen:   \"127\",\n\t\t\texpect: 127,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, min int8\",\n\t\t\twhen:   \"-128\",\n\t\t\texpect: -128,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, overflow max int8\",\n\t\t\twhen:      \"128\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseInt: parsing \"128\": value out of range`,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, underflow min int8\",\n\t\t\twhen:      \"-129\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseInt: parsing \"-129\": value out of range`,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, invalid value\",\n\t\t\twhen:      \"X\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseInt: parsing \"X\": invalid syntax`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tv, err := ParseValue[int8](tc.when)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestParseValue_int16(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\twhen      string\n\t\texpect    int16\n\t\texpectErr string\n\t}{\n\t\t{\n\t\t\tname:   \"ok, 0\",\n\t\t\twhen:   \"0\",\n\t\t\texpect: 0,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, 1\",\n\t\t\twhen:   \"1\",\n\t\t\texpect: 1,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, -1\",\n\t\t\twhen:   \"-1\",\n\t\t\texpect: -1,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, max int16\",\n\t\t\twhen:   \"32767\",\n\t\t\texpect: 32767,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, min int16\",\n\t\t\twhen:   \"-32768\",\n\t\t\texpect: -32768,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, overflow max int16\",\n\t\t\twhen:      \"32768\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseInt: parsing \"32768\": value out of range`,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, underflow min int16\",\n\t\t\twhen:      \"-32769\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseInt: parsing \"-32769\": value out of range`,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, invalid value\",\n\t\t\twhen:      \"X\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseInt: parsing \"X\": invalid syntax`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tv, err := ParseValue[int16](tc.when)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestParseValue_int32(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\twhen      string\n\t\texpect    int32\n\t\texpectErr string\n\t}{\n\t\t{\n\t\t\tname:   \"ok, 0\",\n\t\t\twhen:   \"0\",\n\t\t\texpect: 0,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, 1\",\n\t\t\twhen:   \"1\",\n\t\t\texpect: 1,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, -1\",\n\t\t\twhen:   \"-1\",\n\t\t\texpect: -1,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, max int32\",\n\t\t\twhen:   \"2147483647\",\n\t\t\texpect: 2147483647,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, min int32\",\n\t\t\twhen:   \"-2147483648\",\n\t\t\texpect: -2147483648,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, overflow max int32\",\n\t\t\twhen:      \"2147483648\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseInt: parsing \"2147483648\": value out of range`,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, underflow min int32\",\n\t\t\twhen:      \"-2147483649\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseInt: parsing \"-2147483649\": value out of range`,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, invalid value\",\n\t\t\twhen:      \"X\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseInt: parsing \"X\": invalid syntax`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tv, err := ParseValue[int32](tc.when)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestParseValue_int64(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\twhen      string\n\t\texpect    int64\n\t\texpectErr string\n\t}{\n\t\t{\n\t\t\tname:   \"ok, 0\",\n\t\t\twhen:   \"0\",\n\t\t\texpect: 0,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, 1\",\n\t\t\twhen:   \"1\",\n\t\t\texpect: 1,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, -1\",\n\t\t\twhen:   \"-1\",\n\t\t\texpect: -1,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, max int64\",\n\t\t\twhen:   \"9223372036854775807\",\n\t\t\texpect: 9223372036854775807,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, min int64\",\n\t\t\twhen:   \"-9223372036854775808\",\n\t\t\texpect: -9223372036854775808,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, overflow max int64\",\n\t\t\twhen:      \"9223372036854775808\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseInt: parsing \"9223372036854775808\": value out of range`,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, underflow min int64\",\n\t\t\twhen:      \"-9223372036854775809\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseInt: parsing \"-9223372036854775809\": value out of range`,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, invalid value\",\n\t\t\twhen:      \"X\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseInt: parsing \"X\": invalid syntax`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tv, err := ParseValue[int64](tc.when)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestParseValue_uint8(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\twhen      string\n\t\texpect    uint8\n\t\texpectErr string\n\t}{\n\t\t{\n\t\t\tname:   \"ok, 0\",\n\t\t\twhen:   \"0\",\n\t\t\texpect: 0,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, 1\",\n\t\t\twhen:   \"1\",\n\t\t\texpect: 1,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, max uint8\",\n\t\t\twhen:   \"255\",\n\t\t\texpect: 255,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, overflow max uint8\",\n\t\t\twhen:      \"256\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseUint: parsing \"256\": value out of range`,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, negative value\",\n\t\t\twhen:      \"-1\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseUint: parsing \"-1\": invalid syntax`,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, invalid value\",\n\t\t\twhen:      \"X\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseUint: parsing \"X\": invalid syntax`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tv, err := ParseValue[uint8](tc.when)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestParseValue_uint16(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\twhen      string\n\t\texpect    uint16\n\t\texpectErr string\n\t}{\n\t\t{\n\t\t\tname:   \"ok, 0\",\n\t\t\twhen:   \"0\",\n\t\t\texpect: 0,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, 1\",\n\t\t\twhen:   \"1\",\n\t\t\texpect: 1,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, max uint16\",\n\t\t\twhen:   \"65535\",\n\t\t\texpect: 65535,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, overflow max uint16\",\n\t\t\twhen:      \"65536\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseUint: parsing \"65536\": value out of range`,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, negative value\",\n\t\t\twhen:      \"-1\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseUint: parsing \"-1\": invalid syntax`,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, invalid value\",\n\t\t\twhen:      \"X\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseUint: parsing \"X\": invalid syntax`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tv, err := ParseValue[uint16](tc.when)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestParseValue_uint32(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\twhen      string\n\t\texpect    uint32\n\t\texpectErr string\n\t}{\n\t\t{\n\t\t\tname:   \"ok, 0\",\n\t\t\twhen:   \"0\",\n\t\t\texpect: 0,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, 1\",\n\t\t\twhen:   \"1\",\n\t\t\texpect: 1,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, max uint32\",\n\t\t\twhen:   \"4294967295\",\n\t\t\texpect: 4294967295,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, overflow max uint32\",\n\t\t\twhen:      \"4294967296\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseUint: parsing \"4294967296\": value out of range`,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, negative value\",\n\t\t\twhen:      \"-1\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseUint: parsing \"-1\": invalid syntax`,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, invalid value\",\n\t\t\twhen:      \"X\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseUint: parsing \"X\": invalid syntax`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tv, err := ParseValue[uint32](tc.when)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestParseValue_uint64(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\twhen      string\n\t\texpect    uint64\n\t\texpectErr string\n\t}{\n\t\t{\n\t\t\tname:   \"ok, 0\",\n\t\t\twhen:   \"0\",\n\t\t\texpect: 0,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, 1\",\n\t\t\twhen:   \"1\",\n\t\t\texpect: 1,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, max uint64\",\n\t\t\twhen:   \"18446744073709551615\",\n\t\t\texpect: 18446744073709551615,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, overflow max uint64\",\n\t\t\twhen:      \"18446744073709551616\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseUint: parsing \"18446744073709551616\": value out of range`,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, negative value\",\n\t\t\twhen:      \"-1\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseUint: parsing \"-1\": invalid syntax`,\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, invalid value\",\n\t\t\twhen:      \"X\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseUint: parsing \"X\": invalid syntax`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tv, err := ParseValue[uint64](tc.when)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestParseValue_string(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\twhen      string\n\t\texpect    string\n\t\texpectErr string\n\t}{\n\t\t{\n\t\t\tname:   \"ok, my\",\n\t\t\twhen:   \"my\",\n\t\t\texpect: \"my\",\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, empty\",\n\t\t\twhen:   \"\",\n\t\t\texpect: \"\",\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tv, err := ParseValue[string](tc.when)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestParseValue_Duration(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\twhen      string\n\t\texpect    time.Duration\n\t\texpectErr string\n\t}{\n\t\t{\n\t\t\tname:   \"ok, 10h11m01s\",\n\t\t\twhen:   \"10h11m01s\",\n\t\t\texpect: 10*time.Hour + 11*time.Minute + 1*time.Second,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, empty\",\n\t\t\twhen:   \"\",\n\t\t\texpect: 0,\n\t\t},\n\t\t{\n\t\t\tname:      \"ok, invalid\",\n\t\t\twhen:      \"0x0\",\n\t\t\texpect:    0,\n\t\t\texpectErr: `failed to parse value, err: time: unknown unit \"x\" in duration \"0x0\"`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tv, err := ParseValue[time.Duration](tc.when)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestParseValue_Time(t *testing.T) {\n\ttallinn, err := time.LoadLocation(\"Europe/Tallinn\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tberlin, err := time.LoadLocation(\"Europe/Berlin\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tparse := func(t *testing.T, layout string, s string) time.Time {\n\t\tresult, err := time.Parse(layout, s)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treturn result\n\t}\n\n\tparseInLoc := func(t *testing.T, layout string, s string, loc *time.Location) time.Time {\n\t\tresult, err := time.ParseInLocation(layout, s, loc)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treturn result\n\t}\n\n\tvar testCases = []struct {\n\t\tname         string\n\t\twhen         string\n\t\twhenLayout   TimeLayout\n\t\twhenTimeOpts *TimeOpts\n\t\texpect       time.Time\n\t\texpectErr    string\n\t}{\n\t\t{\n\t\t\tname:   \"ok, defaults to RFC3339Nano\",\n\t\t\twhen:   \"2006-01-02T15:04:05.999999999Z\",\n\t\t\texpect: parse(t, time.RFC3339Nano, \"2006-01-02T15:04:05.999999999Z\"),\n\t\t},\n\t\t{\n\t\t\tname: \"ok, custom TimeOpt\",\n\t\t\twhen: \"2006-01-02\",\n\t\t\twhenTimeOpts: &TimeOpts{\n\t\t\t\tLayout:          time.DateOnly,\n\t\t\t\tParseInLocation: tallinn,\n\t\t\t\tToInLocation:    berlin,\n\t\t\t},\n\t\t\texpect: parseInLoc(t, time.DateTime, \"2006-01-01 23:00:00\", berlin),\n\t\t},\n\t\t{\n\t\t\tname:       \"ok, custom layout\",\n\t\t\twhen:       \"2006-01-02\",\n\t\t\twhenLayout: TimeLayout(time.DateOnly),\n\t\t\texpect:     parse(t, time.DateOnly, \"2006-01-02\"),\n\t\t},\n\t\t{\n\t\t\tname:       \"ok, TimeLayoutUnixTime\",\n\t\t\twhen:       \"1766604665\",\n\t\t\twhenLayout: TimeLayoutUnixTime,\n\t\t\texpect:     parse(t, time.RFC3339Nano, \"2025-12-24T19:31:05Z\"),\n\t\t},\n\t\t{\n\t\t\tname:       \"nok, TimeLayoutUnixTime, invalid value\",\n\t\t\twhen:       \"176x6604665\",\n\t\t\twhenLayout: TimeLayoutUnixTime,\n\t\t\texpectErr:  `failed to parse value, err: strconv.ParseInt: parsing \"176x6604665\": invalid syntax`,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok, TimeLayoutUnixTimeMilli\",\n\t\t\twhen:       \"1766604665123\",\n\t\t\twhenLayout: TimeLayoutUnixTimeMilli,\n\t\t\texpect:     parse(t, time.RFC3339Nano, \"2025-12-24T19:31:05.123Z\"),\n\t\t},\n\t\t{\n\t\t\tname:       \"nok, TimeLayoutUnixTimeMilli, invalid value\",\n\t\t\twhen:       \"1x766604665123\",\n\t\t\twhenLayout: TimeLayoutUnixTimeMilli,\n\t\t\texpectErr:  `failed to parse value, err: strconv.ParseInt: parsing \"1x766604665123\": invalid syntax`,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok, TimeLayoutUnixTimeMilli\",\n\t\t\twhen:       \"1766604665999999999\",\n\t\t\twhenLayout: TimeLayoutUnixTimeNano,\n\t\t\texpect:     parse(t, time.RFC3339Nano, \"2025-12-24T19:31:05.999999999Z\"),\n\t\t},\n\t\t{\n\t\t\tname:       \"nok, TimeLayoutUnixTimeMilli, invalid value\",\n\t\t\twhen:       \"1x766604665999999999\",\n\t\t\twhenLayout: TimeLayoutUnixTimeNano,\n\t\t\texpectErr:  `failed to parse value, err: strconv.ParseInt: parsing \"1x766604665999999999\": invalid syntax`,\n\t\t},\n\t\t{\n\t\t\tname:      \"ok, invalid\",\n\t\t\twhen:      \"xx\",\n\t\t\texpect:    time.Time{},\n\t\t\texpectErr: `failed to parse value, err: parsing time \"xx\" as \"2006-01-02T15:04:05.999999999Z07:00\": cannot parse \"xx\" as \"2006\"`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar opts []any\n\t\t\tif tc.whenLayout != \"\" {\n\t\t\t\topts = append(opts, tc.whenLayout)\n\t\t\t}\n\t\t\tif tc.whenTimeOpts != nil {\n\t\t\t\topts = append(opts, *tc.whenTimeOpts)\n\t\t\t}\n\t\t\tv, err := ParseValue[time.Time](tc.when, opts...)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestParseValue_OptionsOnlyForTime(t *testing.T) {\n\t_, err := ParseValue[int](\"test\", TimeLayoutUnixTime)\n\tassert.EqualError(t, err, `failed to parse value, err: options are only supported for time.Time, got *int`)\n}\n\nfunc TestParseValue_BindUnmarshaler(t *testing.T) {\n\texampleTime, _ := time.Parse(time.RFC3339, \"2020-12-23T09:45:31+02:00\")\n\n\tvar testCases = []struct {\n\t\tname      string\n\t\twhen      string\n\t\texpect    Timestamp\n\t\texpectErr string\n\t}{\n\t\t{\n\t\t\tname:   \"ok\",\n\t\t\twhen:   \"2020-12-23T09:45:31+02:00\",\n\t\t\texpect: Timestamp(exampleTime),\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, invalid value\",\n\t\t\twhen:      \"2020-12-23T09:45:3102:00\",\n\t\t\texpect:    Timestamp{},\n\t\t\texpectErr: `failed to parse value, err: parsing time \"2020-12-23T09:45:3102:00\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"02:00\" as \"Z07:00\"`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tv, err := ParseValue[Timestamp](tc.when)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestParseValue_TextUnmarshaler(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\twhen      string\n\t\texpect    TextUnmarshalerType\n\t\texpectErr string\n\t}{\n\t\t{\n\t\t\tname:   \"ok, converts to uppercase\",\n\t\t\twhen:   \"hello\",\n\t\t\texpect: TextUnmarshalerType{Value: \"HELLO\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, empty string\",\n\t\t\twhen:   \"\",\n\t\t\texpect: TextUnmarshalerType{Value: \"\"},\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, invalid value\",\n\t\t\twhen:      \"invalid\",\n\t\t\texpect:    TextUnmarshalerType{},\n\t\t\texpectErr: \"failed to parse value, err: invalid value: invalid\",\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tv, err := ParseValue[TextUnmarshalerType](tc.when)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestParseValue_JSONUnmarshaler(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\twhen      string\n\t\texpect    JSONUnmarshalerType\n\t\texpectErr string\n\t}{\n\t\t{\n\t\t\tname:   \"ok, valid JSON string\",\n\t\t\twhen:   `\"hello\"`,\n\t\t\texpect: JSONUnmarshalerType{Value: \"hello\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, empty JSON string\",\n\t\t\twhen:   `\"\"`,\n\t\t\texpect: JSONUnmarshalerType{Value: \"\"},\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, invalid JSON\",\n\t\t\twhen:      \"not-json\",\n\t\t\texpect:    JSONUnmarshalerType{},\n\t\t\texpectErr: \"failed to parse value, err: invalid character 'o' in literal null (expecting 'u')\",\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, unquoted string\",\n\t\t\twhen:      \"hello\",\n\t\t\texpect:    JSONUnmarshalerType{},\n\t\t\texpectErr: \"failed to parse value, err: invalid character 'h' looking for beginning of value\",\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tv, err := ParseValue[JSONUnmarshalerType](tc.when)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestParseValues_bools(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\twhen      []string\n\t\texpect    []bool\n\t\texpectErr string\n\t}{\n\t\t{\n\t\t\tname:   \"ok\",\n\t\t\twhen:   []string{\"true\", \"0\", \"false\", \"1\"},\n\t\t\texpect: []bool{true, false, false, true},\n\t\t},\n\t\t{\n\t\t\tname:      \"nok\",\n\t\t\twhen:      []string{\"true\", \"10\"},\n\t\t\texpect:    nil,\n\t\t\texpectErr: `failed to parse value, err: strconv.ParseBool: parsing \"10\": invalid syntax`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tv, err := ParseValues[bool](tc.when)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestPathParamOr(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname         string\n\t\tgivenKey     string\n\t\tgivenValue   string\n\t\tdefaultValue int\n\t\texpect       int\n\t\texpectErr    string\n\t}{\n\t\t{\n\t\t\tname:         \"ok, param exists\",\n\t\t\tgivenKey:     \"id\",\n\t\t\tgivenValue:   \"123\",\n\t\t\tdefaultValue: 999,\n\t\t\texpect:       123,\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, param missing - returns default\",\n\t\t\tgivenKey:     \"other\",\n\t\t\tgivenValue:   \"123\",\n\t\t\tdefaultValue: 999,\n\t\t\texpect:       999,\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, param exists but empty - returns default\",\n\t\t\tgivenKey:     \"id\",\n\t\t\tgivenValue:   \"\",\n\t\t\tdefaultValue: 999,\n\t\t\texpect:       999,\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, invalid value\",\n\t\t\tgivenKey:     \"id\",\n\t\t\tgivenValue:   \"invalid\",\n\t\t\tdefaultValue: 999,\n\t\t\texpectErr:    \"code=400, message=path value, err=failed to parse value\",\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := NewContext(nil, nil)\n\t\t\tc.SetPathValues(PathValues{{Name: tc.givenKey, Value: tc.givenValue}})\n\n\t\t\tv, err := PathParamOr[int](c, \"id\", tc.defaultValue)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.ErrorContains(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestQueryParamOr(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname         string\n\t\tgivenURL     string\n\t\tdefaultValue int\n\t\texpect       int\n\t\texpectErr    string\n\t}{\n\t\t{\n\t\t\tname:         \"ok, param exists\",\n\t\t\tgivenURL:     \"/?key=42\",\n\t\t\tdefaultValue: 999,\n\t\t\texpect:       42,\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, param missing - returns default\",\n\t\t\tgivenURL:     \"/?other=42\",\n\t\t\tdefaultValue: 999,\n\t\t\texpect:       999,\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, param exists but empty - returns default\",\n\t\t\tgivenURL:     \"/?key=\",\n\t\t\tdefaultValue: 999,\n\t\t\texpect:       999,\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, invalid value\",\n\t\t\tgivenURL:     \"/?key=invalid\",\n\t\t\tdefaultValue: 999,\n\t\t\texpectErr:    \"code=400, message=query param\",\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.givenURL, nil)\n\t\t\tc := NewContext(req, nil)\n\n\t\t\tv, err := QueryParamOr[int](c, \"key\", tc.defaultValue)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.ErrorContains(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestQueryParamsOr(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname         string\n\t\tgivenURL     string\n\t\tdefaultValue []int\n\t\texpect       []int\n\t\texpectErr    string\n\t}{\n\t\t{\n\t\t\tname:         \"ok, params exist\",\n\t\t\tgivenURL:     \"/?key=1&key=2&key=3\",\n\t\t\tdefaultValue: []int{999},\n\t\t\texpect:       []int{1, 2, 3},\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, params missing - returns default\",\n\t\t\tgivenURL:     \"/?other=1\",\n\t\t\tdefaultValue: []int{7, 8, 9},\n\t\t\texpect:       []int{7, 8, 9},\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, invalid value\",\n\t\t\tgivenURL:     \"/?key=1&key=invalid\",\n\t\t\tdefaultValue: []int{999},\n\t\t\texpectErr:    \"code=400, message=query params\",\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.givenURL, nil)\n\t\t\tc := NewContext(req, nil)\n\n\t\t\tv, err := QueryParamsOr[int](c, \"key\", tc.defaultValue)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.ErrorContains(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestFormValueOr(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname         string\n\t\tgivenURL     string\n\t\tdefaultValue string\n\t\texpect       string\n\t\texpectErr    string\n\t}{\n\t\t{\n\t\t\tname:         \"ok, value exists\",\n\t\t\tgivenURL:     \"/?name=john\",\n\t\t\tdefaultValue: \"default\",\n\t\t\texpect:       \"john\",\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, value missing - returns default\",\n\t\t\tgivenURL:     \"/?other=john\",\n\t\t\tdefaultValue: \"default\",\n\t\t\texpect:       \"default\",\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, value exists but empty - returns default\",\n\t\t\tgivenURL:     \"/?name=\",\n\t\t\tdefaultValue: \"default\",\n\t\t\texpect:       \"default\",\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodPost, tc.givenURL, nil)\n\t\t\tc := NewContext(req, nil)\n\n\t\t\tv, err := FormValueOr[string](c, \"name\", tc.defaultValue)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.ErrorContains(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc TestFormValuesOr(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname         string\n\t\tgivenURL     string\n\t\tdefaultValue []string\n\t\texpect       []string\n\t\texpectErr    string\n\t}{\n\t\t{\n\t\t\tname:         \"ok, values exist\",\n\t\t\tgivenURL:     \"/?tags=go&tags=rust&tags=python\",\n\t\t\tdefaultValue: []string{\"default\"},\n\t\t\texpect:       []string{\"go\", \"rust\", \"python\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, values missing - returns default\",\n\t\t\tgivenURL:     \"/?other=value\",\n\t\t\tdefaultValue: []string{\"a\", \"b\"},\n\t\t\texpect:       []string{\"a\", \"b\"},\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodPost, tc.givenURL, nil)\n\t\t\tc := NewContext(req, nil)\n\n\t\t\tv, err := FormValuesOr[string](c, \"tags\", tc.defaultValue)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.ErrorContains(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "binder_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"io\"\n\t\"math/big\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc createTestContext(URL string, body io.Reader, pathValues map[string]string) *Context {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, URL, body)\n\tif body != nil {\n\t\treq.Header.Set(HeaderContentType, MIMEApplicationJSON)\n\t}\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\tif len(pathValues) > 0 {\n\t\tparams := make(PathValues, 0)\n\t\tfor name, value := range pathValues {\n\t\t\tparams = append(params, PathValue{\n\t\t\t\tName:  name,\n\t\t\t\tValue: value,\n\t\t\t})\n\t\t}\n\t\tc.SetPathValues(params)\n\t}\n\n\treturn c\n}\n\nfunc TestBindingError_Error(t *testing.T) {\n\terr := NewBindingError(\"id\", []string{\"1\", \"nope\"}, \"bind failed\", errors.New(\"internal error\"))\n\tassert.EqualError(t, err, `code=400, message=bind failed, err=internal error, field=id`)\n\n\tbErr := err.(*BindingError)\n\tassert.Equal(t, 400, bErr.Code)\n\tassert.Equal(t, \"bind failed\", bErr.Message)\n\tassert.Equal(t, errors.New(\"internal error\"), bErr.err)\n\n\tassert.Equal(t, \"id\", bErr.Field)\n\tassert.Equal(t, []string{\"1\", \"nope\"}, bErr.Values)\n}\n\nfunc TestBindingError_ErrorJSON(t *testing.T) {\n\terr := NewBindingError(\"id\", []string{\"1\", \"nope\"}, \"bind failed\", errors.New(\"internal error\"))\n\n\tresp, _ := json.Marshal(err)\n\n\tassert.Equal(t, `{\"field\":\"id\",\"message\":\"bind failed\"}`, string(resp))\n}\n\nfunc TestPathValuesBinder(t *testing.T) {\n\tc := createTestContext(\"/api/user/999\", nil, map[string]string{\n\t\t\"id\":    \"1\",\n\t\t\"nr\":    \"2\",\n\t\t\"slice\": \"3\",\n\t})\n\tb := PathValuesBinder(c)\n\n\tid := int64(99)\n\tnr := int64(88)\n\tvar slice = make([]int64, 0)\n\tvar notExisting = make([]int64, 0)\n\terr := b.Int64(\"id\", &id).\n\t\tInt64(\"nr\", &nr).\n\t\tInt64s(\"slice\", &slice).\n\t\tInt64s(\"not_existing\", &notExisting).\n\t\tBindError()\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(1), id)\n\tassert.Equal(t, int64(2), nr)\n\tassert.Equal(t, []int64{3}, slice)      // binding params to slice does not make sense but it should not panic either\n\tassert.Equal(t, []int64{}, notExisting) // binding params to slice does not make sense but it should not panic either\n}\n\nfunc TestQueryParamsBinder_FailFast(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname          string\n\t\twhenURL       string\n\t\texpectError   []string\n\t\tgivenFailFast bool\n\t}{\n\t\t{\n\t\t\tname:          \"ok, FailFast=true stops at first error\",\n\t\t\twhenURL:       \"/api/user/999?nr=en&id=nope\",\n\t\t\tgivenFailFast: true,\n\t\t\texpectError: []string{\n\t\t\t\t`code=400, message=failed to bind field value to int64, err=strconv.ParseInt: parsing \"nope\": invalid syntax, field=id`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"ok, FailFast=false encounters all errors\",\n\t\t\twhenURL:       \"/api/user/999?nr=en&id=nope\",\n\t\t\tgivenFailFast: false,\n\t\t\texpectError: []string{\n\t\t\t\t`code=400, message=failed to bind field value to int64, err=strconv.ParseInt: parsing \"nope\": invalid syntax, field=id`,\n\t\t\t\t`code=400, message=failed to bind field value to int64, err=strconv.ParseInt: parsing \"en\": invalid syntax, field=nr`,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, map[string]string{\"id\": \"999\"})\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tid := int64(99)\n\t\t\tnr := int64(88)\n\t\t\terrs := b.Int64(\"id\", &id).\n\t\t\t\tInt64(\"nr\", &nr).\n\t\t\t\tBindErrors()\n\n\t\t\tassert.Len(t, errs, len(tc.expectError))\n\t\t\tfor _, err := range errs {\n\t\t\t\tassert.Contains(t, tc.expectError, err.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFormFieldBinder(t *testing.T) {\n\te := New()\n\tbody := `texta=foo&slice=5`\n\treq := httptest.NewRequest(http.MethodPost, \"/api/search?id=1&nr=2&slice=3&slice=4\", strings.NewReader(body))\n\treq.Header.Set(HeaderContentLength, strconv.Itoa(len(body)))\n\treq.Header.Set(HeaderContentType, MIMEApplicationForm)\n\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\tb := FormFieldBinder(c)\n\n\tvar texta string\n\tid := int64(99)\n\tnr := int64(88)\n\tvar slice = make([]int64, 0)\n\tvar notExisting = make([]int64, 0)\n\terr := b.\n\t\tInt64s(\"slice\", &slice).\n\t\tInt64(\"id\", &id).\n\t\tInt64(\"nr\", &nr).\n\t\tString(\"texta\", &texta).\n\t\tInt64s(\"notExisting\", &notExisting).\n\t\tBindError()\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"foo\", texta)\n\tassert.Equal(t, int64(1), id)\n\tassert.Equal(t, int64(2), nr)\n\tassert.Equal(t, []int64{5, 3, 4}, slice)\n\tassert.Equal(t, []int64{}, notExisting)\n}\n\nfunc TestValueBinder_errorStopsBinding(t *testing.T) {\n\t// this test documents \"feature\" that binding multiple params can change destination if it was bound before\n\t// failing parameter binding\n\n\tc := createTestContext(\"/api/user/999?id=1&nr=nope\", nil, nil)\n\tb := QueryParamsBinder(c)\n\n\tid := int64(99) // will be changed before nr binding fails\n\tnr := int64(88) // will not be changed\n\terr := b.Int64(\"id\", &id).\n\t\tInt64(\"nr\", &nr).\n\t\tBindError()\n\n\tassert.EqualError(t, err, \"code=400, message=failed to bind field value to int64, err=strconv.ParseInt: parsing \\\"nope\\\": invalid syntax, field=nr\")\n\tassert.Equal(t, int64(1), id)\n\tassert.Equal(t, int64(88), nr)\n}\n\nfunc TestValueBinder_BindError(t *testing.T) {\n\tc := createTestContext(\"/api/user/999?nr=en&id=nope\", nil, nil)\n\tb := QueryParamsBinder(c)\n\n\tid := int64(99)\n\tnr := int64(88)\n\terr := b.Int64(\"id\", &id).\n\t\tInt64(\"nr\", &nr).\n\t\tBindError()\n\n\tassert.EqualError(t, err, \"code=400, message=failed to bind field value to int64, err=strconv.ParseInt: parsing \\\"nope\\\": invalid syntax, field=id\")\n\tassert.Nil(t, b.errors)\n\tassert.Nil(t, b.BindError())\n}\n\nfunc TestValueBinder_GetValues(t *testing.T) {\n\tvar testCases = []struct {\n\t\twhenValuesFunc func(sourceParam string) []string\n\t\tname           string\n\t\texpectError    string\n\t\texpect         []int64\n\t}{\n\t\t{\n\t\t\tname:   \"ok, default implementation\",\n\t\t\texpect: []int64{1, 101},\n\t\t},\n\t\t{\n\t\t\tname: \"ok, values returns nil\",\n\t\t\twhenValuesFunc: func(sourceParam string) []string {\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\texpect: []int64(nil),\n\t\t},\n\t\t{\n\t\t\tname: \"ok, values returns empty slice\",\n\t\t\twhenValuesFunc: func(sourceParam string) []string {\n\t\t\t\treturn []string{}\n\t\t\t},\n\t\t\texpect: []int64(nil),\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(\"/search?nr=en&id=1&id=101\", nil, nil)\n\t\t\tb := QueryParamsBinder(c)\n\t\t\tif tc.whenValuesFunc != nil {\n\t\t\t\tb.ValuesFunc = tc.whenValuesFunc\n\t\t\t}\n\n\t\t\tvar IDs []int64\n\t\t\terr := b.Int64s(\"id\", &IDs).BindError()\n\n\t\t\tassert.Equal(t, tc.expect, IDs)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_CustomFuncWithError(t *testing.T) {\n\tc := createTestContext(\"/search?nr=en&id=1&id=101\", nil, nil)\n\tb := QueryParamsBinder(c)\n\n\tid := int64(99)\n\tgivenCustomFunc := func(values []string) []error {\n\t\tassert.Equal(t, []string{\"1\", \"101\"}, values)\n\n\t\treturn []error{\n\t\t\terrors.New(\"first error\"),\n\t\t\terrors.New(\"second error\"),\n\t\t}\n\t}\n\terr := b.CustomFunc(\"id\", givenCustomFunc).BindError()\n\n\tassert.Equal(t, int64(99), id)\n\tassert.EqualError(t, err, \"first error\")\n}\n\nfunc TestValueBinder_CustomFunc(t *testing.T) {\n\tvar testCases = []struct {\n\t\texpectValue       any\n\t\tname              string\n\t\twhenURL           string\n\t\tgivenFuncErrors   []error\n\t\texpectParamValues []string\n\t\texpectErrors      []string\n\t\tgivenFailFast     bool\n\t}{\n\t\t{\n\t\t\tname:              \"ok, binds value\",\n\t\t\twhenURL:           \"/search?nr=en&id=1&id=100\",\n\t\t\texpectParamValues: []string{\"1\", \"100\"},\n\t\t\texpectValue:       int64(1000),\n\t\t},\n\t\t{\n\t\t\tname:              \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:           \"/search?nr=en\",\n\t\t\texpectParamValues: []string{},\n\t\t\texpectValue:       int64(99),\n\t\t},\n\t\t{\n\t\t\tname:              \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast:     true,\n\t\t\twhenURL:           \"/search?nr=en&id=1&id=100\",\n\t\t\texpectParamValues: []string{\"1\", \"100\"},\n\t\t\texpectValue:       int64(99),\n\t\t\texpectErrors:      []string{\"previous error\"},\n\t\t},\n\t\t{\n\t\t\tname: \"nok, func returns errors\",\n\t\t\tgivenFuncErrors: []error{\n\t\t\t\terrors.New(\"first error\"),\n\t\t\t\terrors.New(\"second error\"),\n\t\t\t},\n\t\t\twhenURL:           \"/search?nr=en&id=1&id=100\",\n\t\t\texpectParamValues: []string{\"1\", \"100\"},\n\t\t\texpectValue:       int64(99),\n\t\t\texpectErrors:      []string{\"first error\", \"second error\"},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tif tc.givenFailFast {\n\t\t\t\tb.errors = []error{errors.New(\"previous error\")}\n\t\t\t}\n\n\t\t\tid := int64(99)\n\t\t\tgivenCustomFunc := func(values []string) []error {\n\t\t\t\tassert.Equal(t, tc.expectParamValues, values)\n\t\t\t\tif tc.givenFuncErrors == nil {\n\t\t\t\t\tid = 1000 // emulated conversion and setting value\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\treturn tc.givenFuncErrors\n\t\t\t}\n\t\t\terrs := b.CustomFunc(\"id\", givenCustomFunc).BindErrors()\n\n\t\t\tassert.Equal(t, tc.expectValue, id)\n\t\t\tif tc.expectErrors != nil {\n\t\t\t\tassert.Len(t, errs, len(tc.expectErrors))\n\t\t\t\tfor _, err := range errs {\n\t\t\t\t\tassert.Contains(t, tc.expectErrors, err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, errs)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_MustCustomFunc(t *testing.T) {\n\tvar testCases = []struct {\n\t\texpectValue       any\n\t\tname              string\n\t\twhenURL           string\n\t\tgivenFuncErrors   []error\n\t\texpectParamValues []string\n\t\texpectErrors      []string\n\t\tgivenFailFast     bool\n\t}{\n\t\t{\n\t\t\tname:              \"ok, binds value\",\n\t\t\twhenURL:           \"/search?nr=en&id=1&id=100\",\n\t\t\texpectParamValues: []string{\"1\", \"100\"},\n\t\t\texpectValue:       int64(1000),\n\t\t},\n\t\t{\n\t\t\tname:              \"nok, params values empty, returns error, value is not changed\",\n\t\t\twhenURL:           \"/search?nr=en\",\n\t\t\texpectParamValues: []string{},\n\t\t\texpectValue:       int64(99),\n\t\t\texpectErrors:      []string{\"code=400, message=required field value is empty, field=id\"},\n\t\t},\n\t\t{\n\t\t\tname:              \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast:     true,\n\t\t\twhenURL:           \"/search?nr=en&id=1&id=100\",\n\t\t\texpectParamValues: []string{\"1\", \"100\"},\n\t\t\texpectValue:       int64(99),\n\t\t\texpectErrors:      []string{\"previous error\"},\n\t\t},\n\t\t{\n\t\t\tname: \"nok, func returns errors\",\n\t\t\tgivenFuncErrors: []error{\n\t\t\t\terrors.New(\"first error\"),\n\t\t\t\terrors.New(\"second error\"),\n\t\t\t},\n\t\t\twhenURL:           \"/search?nr=en&id=1&id=100\",\n\t\t\texpectParamValues: []string{\"1\", \"100\"},\n\t\t\texpectValue:       int64(99),\n\t\t\texpectErrors:      []string{\"first error\", \"second error\"},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tif tc.givenFailFast {\n\t\t\t\tb.errors = []error{errors.New(\"previous error\")}\n\t\t\t}\n\n\t\t\tid := int64(99)\n\t\t\tgivenCustomFunc := func(values []string) []error {\n\t\t\t\tassert.Equal(t, tc.expectParamValues, values)\n\t\t\t\tif tc.givenFuncErrors == nil {\n\t\t\t\t\tid = 1000 // emulated conversion and setting value\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\treturn tc.givenFuncErrors\n\t\t\t}\n\t\t\terrs := b.MustCustomFunc(\"id\", givenCustomFunc).BindErrors()\n\n\t\t\tassert.Equal(t, tc.expectValue, id)\n\t\t\tif tc.expectErrors != nil {\n\t\t\t\tassert.Len(t, errs, len(tc.expectErrors))\n\t\t\t\tfor _, err := range errs {\n\t\t\t\t\tassert.Contains(t, tc.expectErrors, err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, errs)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_String(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenURL         string\n\t\texpectValue     string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value\",\n\t\t\twhenURL:     \"/search?param=en&param=de\",\n\t\t\texpectValue: \"en\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nr=en\",\n\t\t\texpectValue: \"default\",\n\t\t},\n\t\t{\n\t\t\tname:          \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenURL:       \"/search?nr=en&id=1&id=100\",\n\t\t\texpectValue:   \"default\",\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=en&param=de\",\n\t\t\texpectValue: \"en\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nr=en\",\n\t\t\texpectValue: \"default\",\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:          \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenMust:      true,\n\t\t\twhenURL:       \"/search?nr=en&id=1&id=100\",\n\t\t\texpectValue:   \"default\",\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tif tc.givenFailFast {\n\t\t\t\tb.errors = []error{errors.New(\"previous error\")}\n\t\t\t}\n\n\t\t\tdest := \"default\"\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustString(\"param\", &dest).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.String(\"param\", &dest).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_Strings(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenURL         string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\texpectValue     []string\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value\",\n\t\t\twhenURL:     \"/search?param=en&param=de\",\n\t\t\texpectValue: []string{\"en\", \"de\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nr=en\",\n\t\t\texpectValue: []string{\"default\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenURL:       \"/search?nr=en&id=1&id=100\",\n\t\t\texpectValue:   []string{\"default\"},\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=en&param=de\",\n\t\t\texpectValue: []string{\"en\", \"de\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nr=en\",\n\t\t\texpectValue: []string{\"default\"},\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:          \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenMust:      true,\n\t\t\twhenURL:       \"/search?nr=en&id=1&id=100\",\n\t\t\texpectValue:   []string{\"default\"},\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tif tc.givenFailFast {\n\t\t\t\tb.errors = []error{errors.New(\"previous error\")}\n\t\t\t}\n\n\t\t\tdest := []string{\"default\"}\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustStrings(\"param\", &dest).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.Strings(\"param\", &dest).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_Int64_intValue(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenURL         string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\texpectValue     int64\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value\",\n\t\t\twhenURL:     \"/search?param=1&param=100\",\n\t\t\texpectValue: 1,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: 99,\n\t\t},\n\t\t{\n\t\t\tname:          \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   99,\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, conversion fails, value is not changed\",\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: 99,\n\t\t\texpectError: \"code=400, message=failed to bind field value to int64, err=strconv.ParseInt: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=1&param=100\",\n\t\t\texpectValue: 1,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: 99,\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:          \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenMust:      true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   99,\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok (must), conversion fails, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: 99,\n\t\t\texpectError: \"code=400, message=failed to bind field value to int64, err=strconv.ParseInt: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tif tc.givenFailFast {\n\t\t\t\tb.errors = []error{errors.New(\"previous error\")}\n\t\t\t}\n\n\t\t\tdest := int64(99)\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustInt64(\"param\", &dest).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.Int64(\"param\", &dest).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_Int_errorMessage(t *testing.T) {\n\t// int/uint (without byte size) has a little bit different error message so test these separately\n\tc := createTestContext(\"/search?param=nope\", nil, nil)\n\tb := QueryParamsBinder(c).FailFast(false)\n\n\tdestInt := 99\n\tdestUint := uint(98)\n\terrs := b.Int(\"param\", &destInt).Uint(\"param\", &destUint).BindErrors()\n\n\tassert.Equal(t, 99, destInt)\n\tassert.Equal(t, uint(98), destUint)\n\tassert.EqualError(t, errs[0], `code=400, message=failed to bind field value to int, err=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param`)\n\tassert.EqualError(t, errs[1], `code=400, message=failed to bind field value to uint, err=strconv.ParseUint: parsing \"nope\": invalid syntax, field=param`)\n}\n\nfunc TestValueBinder_Uint64_uintValue(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenURL         string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\texpectValue     uint64\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value\",\n\t\t\twhenURL:     \"/search?param=1&param=100\",\n\t\t\texpectValue: 1,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: 99,\n\t\t},\n\t\t{\n\t\t\tname:          \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   99,\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, conversion fails, value is not changed\",\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: 99,\n\t\t\texpectError: \"code=400, message=failed to bind field value to uint64, err=strconv.ParseUint: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=1&param=100\",\n\t\t\texpectValue: 1,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: 99,\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:          \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenMust:      true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   99,\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok (must), conversion fails, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: 99,\n\t\t\texpectError: \"code=400, message=failed to bind field value to uint64, err=strconv.ParseUint: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tif tc.givenFailFast {\n\t\t\t\tb.errors = []error{errors.New(\"previous error\")}\n\t\t\t}\n\n\t\t\tdest := uint64(99)\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustUint64(\"param\", &dest).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.Uint64(\"param\", &dest).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_Int_Types(t *testing.T) {\n\ttype target struct {\n\t\tint64      int64\n\t\tmustInt64  int64\n\t\tuint64     uint64\n\t\tmustUint64 uint64\n\n\t\tint32      int32\n\t\tmustInt32  int32\n\t\tuint32     uint32\n\t\tmustUint32 uint32\n\n\t\tint16      int16\n\t\tmustInt16  int16\n\t\tuint16     uint16\n\t\tmustUint16 uint16\n\n\t\tint8      int8\n\t\tmustInt8  int8\n\t\tuint8     uint8\n\t\tmustUint8 uint8\n\n\t\tbyte     byte\n\t\tmustByte byte\n\n\t\tint      int\n\t\tmustInt  int\n\t\tuint     uint\n\t\tmustUint uint\n\t}\n\ttypes := []string{\n\t\t\"int64=1\",\n\t\t\"mustInt64=2\",\n\t\t\"uint64=3\",\n\t\t\"mustUint64=4\",\n\n\t\t\"int32=5\",\n\t\t\"mustInt32=6\",\n\t\t\"uint32=7\",\n\t\t\"mustUint32=8\",\n\n\t\t\"int16=9\",\n\t\t\"mustInt16=10\",\n\t\t\"uint16=11\",\n\t\t\"mustUint16=12\",\n\n\t\t\"int8=13\",\n\t\t\"mustInt8=14\",\n\t\t\"uint8=15\",\n\t\t\"mustUint8=16\",\n\n\t\t\"byte=17\",\n\t\t\"mustByte=18\",\n\n\t\t\"int=19\",\n\t\t\"mustInt=20\",\n\t\t\"uint=21\",\n\t\t\"mustUint=22\",\n\t}\n\tc := createTestContext(\"/search?\"+strings.Join(types, \"&\"), nil, nil)\n\tb := QueryParamsBinder(c)\n\n\tdest := target{}\n\terr := b.\n\t\tInt64(\"int64\", &dest.int64).\n\t\tMustInt64(\"mustInt64\", &dest.mustInt64).\n\t\tUint64(\"uint64\", &dest.uint64).\n\t\tMustUint64(\"mustUint64\", &dest.mustUint64).\n\t\tInt32(\"int32\", &dest.int32).\n\t\tMustInt32(\"mustInt32\", &dest.mustInt32).\n\t\tUint32(\"uint32\", &dest.uint32).\n\t\tMustUint32(\"mustUint32\", &dest.mustUint32).\n\t\tInt16(\"int16\", &dest.int16).\n\t\tMustInt16(\"mustInt16\", &dest.mustInt16).\n\t\tUint16(\"uint16\", &dest.uint16).\n\t\tMustUint16(\"mustUint16\", &dest.mustUint16).\n\t\tInt8(\"int8\", &dest.int8).\n\t\tMustInt8(\"mustInt8\", &dest.mustInt8).\n\t\tUint8(\"uint8\", &dest.uint8).\n\t\tMustUint8(\"mustUint8\", &dest.mustUint8).\n\t\tByte(\"byte\", &dest.byte).\n\t\tMustByte(\"mustByte\", &dest.mustByte).\n\t\tInt(\"int\", &dest.int).\n\t\tMustInt(\"mustInt\", &dest.mustInt).\n\t\tUint(\"uint\", &dest.uint).\n\t\tMustUint(\"mustUint\", &dest.mustUint).\n\t\tBindError()\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(1), dest.int64)\n\tassert.Equal(t, int64(2), dest.mustInt64)\n\tassert.Equal(t, uint64(3), dest.uint64)\n\tassert.Equal(t, uint64(4), dest.mustUint64)\n\n\tassert.Equal(t, int32(5), dest.int32)\n\tassert.Equal(t, int32(6), dest.mustInt32)\n\tassert.Equal(t, uint32(7), dest.uint32)\n\tassert.Equal(t, uint32(8), dest.mustUint32)\n\n\tassert.Equal(t, int16(9), dest.int16)\n\tassert.Equal(t, int16(10), dest.mustInt16)\n\tassert.Equal(t, uint16(11), dest.uint16)\n\tassert.Equal(t, uint16(12), dest.mustUint16)\n\n\tassert.Equal(t, int8(13), dest.int8)\n\tassert.Equal(t, int8(14), dest.mustInt8)\n\tassert.Equal(t, uint8(15), dest.uint8)\n\tassert.Equal(t, uint8(16), dest.mustUint8)\n\n\tassert.Equal(t, uint8(17), dest.byte)\n\tassert.Equal(t, uint8(18), dest.mustByte)\n\n\tassert.Equal(t, 19, dest.int)\n\tassert.Equal(t, 20, dest.mustInt)\n\tassert.Equal(t, uint(21), dest.uint)\n\tassert.Equal(t, uint(22), dest.mustUint)\n}\n\nfunc TestValueBinder_Int64s_intsValue(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenURL         string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\texpectValue     []int64\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value\",\n\t\t\twhenURL:     \"/search?param=1&param=2&param=1\",\n\t\t\texpectValue: []int64{1, 2, 1},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: []int64{99},\n\t\t},\n\t\t{\n\t\t\tname:          \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   []int64{99},\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, conversion fails, value is not changed\",\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: []int64{99},\n\t\t\texpectError: \"code=400, message=failed to bind field value to int64, err=strconv.ParseInt: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=1&param=2&param=1\",\n\t\t\texpectValue: []int64{1, 2, 1},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: []int64{99},\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:          \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenMust:      true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   []int64{99},\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok (must), conversion fails, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: []int64{99},\n\t\t\texpectError: \"code=400, message=failed to bind field value to int64, err=strconv.ParseInt: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tif tc.givenFailFast {\n\t\t\t\tb.errors = []error{errors.New(\"previous error\")}\n\t\t\t}\n\n\t\t\tdest := []int64{99} // when values are set with bind - contents before bind is gone\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustInt64s(\"param\", &dest).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.Int64s(\"param\", &dest).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_Uint64s_uintsValue(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenURL         string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\texpectValue     []uint64\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value\",\n\t\t\twhenURL:     \"/search?param=1&param=2&param=1\",\n\t\t\texpectValue: []uint64{1, 2, 1},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: []uint64{99},\n\t\t},\n\t\t{\n\t\t\tname:          \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   []uint64{99},\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, conversion fails, value is not changed\",\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: []uint64{99},\n\t\t\texpectError: \"code=400, message=failed to bind field value to uint64, err=strconv.ParseUint: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=1&param=2&param=1\",\n\t\t\texpectValue: []uint64{1, 2, 1},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: []uint64{99},\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:          \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenMust:      true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   []uint64{99},\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok (must), conversion fails, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: []uint64{99},\n\t\t\texpectError: \"code=400, message=failed to bind field value to uint64, err=strconv.ParseUint: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tif tc.givenFailFast {\n\t\t\t\tb.errors = []error{errors.New(\"previous error\")}\n\t\t\t}\n\n\t\t\tdest := []uint64{99} // when values are set with bind - contents before bind is gone\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustUint64s(\"param\", &dest).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.Uint64s(\"param\", &dest).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_Ints_Types(t *testing.T) {\n\ttype target struct {\n\t\tint64      []int64\n\t\tmustInt64  []int64\n\t\tuint64     []uint64\n\t\tmustUint64 []uint64\n\n\t\tint32      []int32\n\t\tmustInt32  []int32\n\t\tuint32     []uint32\n\t\tmustUint32 []uint32\n\n\t\tint16      []int16\n\t\tmustInt16  []int16\n\t\tuint16     []uint16\n\t\tmustUint16 []uint16\n\n\t\tint8      []int8\n\t\tmustInt8  []int8\n\t\tuint8     []uint8\n\t\tmustUint8 []uint8\n\n\t\tint      []int\n\t\tmustInt  []int\n\t\tuint     []uint\n\t\tmustUint []uint\n\t}\n\ttypes := []string{\n\t\t\"int64=1\",\n\t\t\"mustInt64=2\",\n\t\t\"uint64=3\",\n\t\t\"mustUint64=4\",\n\n\t\t\"int32=5\",\n\t\t\"mustInt32=6\",\n\t\t\"uint32=7\",\n\t\t\"mustUint32=8\",\n\n\t\t\"int16=9\",\n\t\t\"mustInt16=10\",\n\t\t\"uint16=11\",\n\t\t\"mustUint16=12\",\n\n\t\t\"int8=13\",\n\t\t\"mustInt8=14\",\n\t\t\"uint8=15\",\n\t\t\"mustUint8=16\",\n\n\t\t\"int=19\",\n\t\t\"mustInt=20\",\n\t\t\"uint=21\",\n\t\t\"mustUint=22\",\n\t}\n\turl := \"/search?\"\n\tfor _, v := range types {\n\t\turl = url + \"&\" + v + \"&\" + v\n\t}\n\tc := createTestContext(url, nil, nil)\n\tb := QueryParamsBinder(c)\n\n\tdest := target{}\n\terr := b.\n\t\tInt64s(\"int64\", &dest.int64).\n\t\tMustInt64s(\"mustInt64\", &dest.mustInt64).\n\t\tUint64s(\"uint64\", &dest.uint64).\n\t\tMustUint64s(\"mustUint64\", &dest.mustUint64).\n\t\tInt32s(\"int32\", &dest.int32).\n\t\tMustInt32s(\"mustInt32\", &dest.mustInt32).\n\t\tUint32s(\"uint32\", &dest.uint32).\n\t\tMustUint32s(\"mustUint32\", &dest.mustUint32).\n\t\tInt16s(\"int16\", &dest.int16).\n\t\tMustInt16s(\"mustInt16\", &dest.mustInt16).\n\t\tUint16s(\"uint16\", &dest.uint16).\n\t\tMustUint16s(\"mustUint16\", &dest.mustUint16).\n\t\tInt8s(\"int8\", &dest.int8).\n\t\tMustInt8s(\"mustInt8\", &dest.mustInt8).\n\t\tUint8s(\"uint8\", &dest.uint8).\n\t\tMustUint8s(\"mustUint8\", &dest.mustUint8).\n\t\tInts(\"int\", &dest.int).\n\t\tMustInts(\"mustInt\", &dest.mustInt).\n\t\tUints(\"uint\", &dest.uint).\n\t\tMustUints(\"mustUint\", &dest.mustUint).\n\t\tBindError()\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, []int64{1, 1}, dest.int64)\n\tassert.Equal(t, []int64{2, 2}, dest.mustInt64)\n\tassert.Equal(t, []uint64{3, 3}, dest.uint64)\n\tassert.Equal(t, []uint64{4, 4}, dest.mustUint64)\n\n\tassert.Equal(t, []int32{5, 5}, dest.int32)\n\tassert.Equal(t, []int32{6, 6}, dest.mustInt32)\n\tassert.Equal(t, []uint32{7, 7}, dest.uint32)\n\tassert.Equal(t, []uint32{8, 8}, dest.mustUint32)\n\n\tassert.Equal(t, []int16{9, 9}, dest.int16)\n\tassert.Equal(t, []int16{10, 10}, dest.mustInt16)\n\tassert.Equal(t, []uint16{11, 11}, dest.uint16)\n\tassert.Equal(t, []uint16{12, 12}, dest.mustUint16)\n\n\tassert.Equal(t, []int8{13, 13}, dest.int8)\n\tassert.Equal(t, []int8{14, 14}, dest.mustInt8)\n\tassert.Equal(t, []uint8{15, 15}, dest.uint8)\n\tassert.Equal(t, []uint8{16, 16}, dest.mustUint8)\n\n\tassert.Equal(t, []int{19, 19}, dest.int)\n\tassert.Equal(t, []int{20, 20}, dest.mustInt)\n\tassert.Equal(t, []uint{21, 21}, dest.uint)\n\tassert.Equal(t, []uint{22, 22}, dest.mustUint)\n}\n\nfunc TestValueBinder_Ints_Types_FailFast(t *testing.T) {\n\t// FailFast() should stop parsing and return early\n\terrTmpl := \"code=400, message=failed to bind field value to %v, err=strconv.Parse%v: parsing \\\"nope\\\": invalid syntax, field=param\"\n\tc := createTestContext(\"/search?param=1&param=nope&param=2\", nil, nil)\n\n\tvar dest64 []int64\n\terr := QueryParamsBinder(c).FailFast(true).Int64s(\"param\", &dest64).BindError()\n\tassert.Equal(t, []int64(nil), dest64)\n\tassert.EqualError(t, err, fmt.Sprintf(errTmpl, \"int64\", \"Int\"))\n\n\tvar dest32 []int32\n\terr = QueryParamsBinder(c).FailFast(true).Int32s(\"param\", &dest32).BindError()\n\tassert.Equal(t, []int32(nil), dest32)\n\tassert.EqualError(t, err, fmt.Sprintf(errTmpl, \"int32\", \"Int\"))\n\n\tvar dest16 []int16\n\terr = QueryParamsBinder(c).FailFast(true).Int16s(\"param\", &dest16).BindError()\n\tassert.Equal(t, []int16(nil), dest16)\n\tassert.EqualError(t, err, fmt.Sprintf(errTmpl, \"int16\", \"Int\"))\n\n\tvar dest8 []int8\n\terr = QueryParamsBinder(c).FailFast(true).Int8s(\"param\", &dest8).BindError()\n\tassert.Equal(t, []int8(nil), dest8)\n\tassert.EqualError(t, err, fmt.Sprintf(errTmpl, \"int8\", \"Int\"))\n\n\tvar dest []int\n\terr = QueryParamsBinder(c).FailFast(true).Ints(\"param\", &dest).BindError()\n\tassert.Equal(t, []int(nil), dest)\n\tassert.EqualError(t, err, fmt.Sprintf(errTmpl, \"int\", \"Int\"))\n\n\tvar destu64 []uint64\n\terr = QueryParamsBinder(c).FailFast(true).Uint64s(\"param\", &destu64).BindError()\n\tassert.Equal(t, []uint64(nil), destu64)\n\tassert.EqualError(t, err, fmt.Sprintf(errTmpl, \"uint64\", \"Uint\"))\n\n\tvar destu32 []uint32\n\terr = QueryParamsBinder(c).FailFast(true).Uint32s(\"param\", &destu32).BindError()\n\tassert.Equal(t, []uint32(nil), destu32)\n\tassert.EqualError(t, err, fmt.Sprintf(errTmpl, \"uint32\", \"Uint\"))\n\n\tvar destu16 []uint16\n\terr = QueryParamsBinder(c).FailFast(true).Uint16s(\"param\", &destu16).BindError()\n\tassert.Equal(t, []uint16(nil), destu16)\n\tassert.EqualError(t, err, fmt.Sprintf(errTmpl, \"uint16\", \"Uint\"))\n\n\tvar destu8 []uint8\n\terr = QueryParamsBinder(c).FailFast(true).Uint8s(\"param\", &destu8).BindError()\n\tassert.Equal(t, []uint8(nil), destu8)\n\tassert.EqualError(t, err, fmt.Sprintf(errTmpl, \"uint8\", \"Uint\"))\n\n\tvar destu []uint\n\terr = QueryParamsBinder(c).FailFast(true).Uints(\"param\", &destu).BindError()\n\tassert.Equal(t, []uint(nil), destu)\n\tassert.EqualError(t, err, fmt.Sprintf(errTmpl, \"uint\", \"Uint\"))\n}\n\nfunc TestValueBinder_Bool(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenURL         string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t\texpectValue     bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value\",\n\t\t\twhenURL:     \"/search?param=true&param=1\",\n\t\t\texpectValue: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: false,\n\t\t},\n\t\t{\n\t\t\tname:          \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   false,\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, conversion fails, value is not changed\",\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: false,\n\t\t\texpectError: \"code=400, message=failed to bind field value to bool, err=strconv.ParseBool: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=1&param=100\",\n\t\t\texpectValue: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: false,\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:          \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenMust:      true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   false,\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok (must), conversion fails, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: false,\n\t\t\texpectError: \"code=400, message=failed to bind field value to bool, err=strconv.ParseBool: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tif tc.givenFailFast {\n\t\t\t\tb.errors = []error{errors.New(\"previous error\")}\n\t\t\t}\n\n\t\t\tdest := false\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustBool(\"param\", &dest).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.Bool(\"param\", &dest).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_Bools(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenURL         string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\texpectValue     []bool\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value\",\n\t\t\twhenURL:     \"/search?param=true&param=false&param=1&param=0\",\n\t\t\texpectValue: []bool{true, false, true, false},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: []bool(nil),\n\t\t},\n\t\t{\n\t\t\tname:            \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast:   true,\n\t\t\tgivenBindErrors: []error{errors.New(\"previous error\")},\n\t\t\twhenURL:         \"/search?param=1&param=100\",\n\t\t\texpectValue:     []bool(nil),\n\t\t\texpectError:     \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, conversion fails, value is not changed\",\n\t\t\twhenURL:     \"/search?param=true&param=nope&param=100\",\n\t\t\texpectValue: []bool(nil),\n\t\t\texpectError: \"code=400, message=failed to bind field value to bool, err=strconv.ParseBool: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:          \"nok, conversion fails fast, value is not changed\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenURL:       \"/search?param=true&param=nope&param=100\",\n\t\t\texpectValue:   []bool(nil),\n\t\t\texpectError:   \"code=400, message=failed to bind field value to bool, err=strconv.ParseBool: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=true&param=false&param=1&param=0\",\n\t\t\texpectValue: []bool{true, false, true, false},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: []bool(nil),\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:            \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenFailFast:   true,\n\t\t\tgivenBindErrors: []error{errors.New(\"previous error\")},\n\t\t\twhenMust:        true,\n\t\t\twhenURL:         \"/search?param=1&param=100\",\n\t\t\texpectValue:     []bool(nil),\n\t\t\texpectError:     \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok (must), conversion fails, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: []bool(nil),\n\t\t\texpectError: \"code=400, message=failed to bind field value to bool, err=strconv.ParseBool: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tb.errors = tc.givenBindErrors\n\n\t\t\tvar dest []bool\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustBools(\"param\", &dest).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.Bools(\"param\", &dest).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_Float64(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenURL         string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\texpectValue     float64\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value\",\n\t\t\twhenURL:     \"/search?param=4.3&param=1\",\n\t\t\texpectValue: 4.3,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: 1.123,\n\t\t},\n\t\t{\n\t\t\tname:          \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   1.123,\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, conversion fails, value is not changed\",\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: 1.123,\n\t\t\texpectError: \"code=400, message=failed to bind field value to float64, err=strconv.ParseFloat: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=4.3&param=100\",\n\t\t\texpectValue: 4.3,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: 1.123,\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:          \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenMust:      true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   1.123,\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok (must), conversion fails, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: 1.123,\n\t\t\texpectError: \"code=400, message=failed to bind field value to float64, err=strconv.ParseFloat: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tif tc.givenFailFast {\n\t\t\t\tb.errors = []error{errors.New(\"previous error\")}\n\t\t\t}\n\n\t\t\tdest := 1.123\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustFloat64(\"param\", &dest).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.Float64(\"param\", &dest).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_Float64s(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenURL         string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\texpectValue     []float64\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value\",\n\t\t\twhenURL:     \"/search?param=4.3&param=0\",\n\t\t\texpectValue: []float64{4.3, 0},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: []float64(nil),\n\t\t},\n\t\t{\n\t\t\tname:            \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast:   true,\n\t\t\tgivenBindErrors: []error{errors.New(\"previous error\")},\n\t\t\twhenURL:         \"/search?param=1&param=100\",\n\t\t\texpectValue:     []float64(nil),\n\t\t\texpectError:     \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, conversion fails, value is not changed\",\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: []float64(nil),\n\t\t\texpectError: \"code=400, message=failed to bind field value to float64, err=strconv.ParseFloat: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:          \"nok, conversion fails fast, value is not changed\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenURL:       \"/search?param=0&param=nope&param=100\",\n\t\t\texpectValue:   []float64(nil),\n\t\t\texpectError:   \"code=400, message=failed to bind field value to float64, err=strconv.ParseFloat: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=4.3&param=0\",\n\t\t\texpectValue: []float64{4.3, 0},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: []float64(nil),\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:            \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenFailFast:   true,\n\t\t\tgivenBindErrors: []error{errors.New(\"previous error\")},\n\t\t\twhenMust:        true,\n\t\t\twhenURL:         \"/search?param=1&param=100\",\n\t\t\texpectValue:     []float64(nil),\n\t\t\texpectError:     \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok (must), conversion fails, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: []float64(nil),\n\t\t\texpectError: \"code=400, message=failed to bind field value to float64, err=strconv.ParseFloat: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tb.errors = tc.givenBindErrors\n\n\t\t\tvar dest []float64\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustFloat64s(\"param\", &dest).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.Float64s(\"param\", &dest).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_Float32(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenURL         string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\texpectValue     float32\n\t\tgivenNoFailFast bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value\",\n\t\t\twhenURL:     \"/search?param=4.3&param=1\",\n\t\t\texpectValue: 4.3,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: 1.123,\n\t\t},\n\t\t{\n\t\t\tname:            \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenNoFailFast: true,\n\t\t\twhenURL:         \"/search?param=1&param=100\",\n\t\t\texpectValue:     1.123,\n\t\t\texpectError:     \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, conversion fails, value is not changed\",\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: 1.123,\n\t\t\texpectError: \"code=400, message=failed to bind field value to float32, err=strconv.ParseFloat: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=4.3&param=100\",\n\t\t\texpectValue: 4.3,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: 1.123,\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:            \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenNoFailFast: true,\n\t\t\twhenMust:        true,\n\t\t\twhenURL:         \"/search?param=1&param=100\",\n\t\t\texpectValue:     1.123,\n\t\t\texpectError:     \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok (must), conversion fails, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: 1.123,\n\t\t\texpectError: \"code=400, message=failed to bind field value to float32, err=strconv.ParseFloat: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenNoFailFast)\n\t\t\tif tc.givenNoFailFast {\n\t\t\t\tb.errors = []error{errors.New(\"previous error\")}\n\t\t\t}\n\n\t\t\tdest := float32(1.123)\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustFloat32(\"param\", &dest).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.Float32(\"param\", &dest).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_Float32s(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenURL         string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\texpectValue     []float32\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value\",\n\t\t\twhenURL:     \"/search?param=4.3&param=0\",\n\t\t\texpectValue: []float32{4.3, 0},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: []float32(nil),\n\t\t},\n\t\t{\n\t\t\tname:            \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast:   true,\n\t\t\tgivenBindErrors: []error{errors.New(\"previous error\")},\n\t\t\twhenURL:         \"/search?param=1&param=100\",\n\t\t\texpectValue:     []float32(nil),\n\t\t\texpectError:     \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, conversion fails, value is not changed\",\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: []float32(nil),\n\t\t\texpectError: \"code=400, message=failed to bind field value to float32, err=strconv.ParseFloat: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:          \"nok, conversion fails fast, value is not changed\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenURL:       \"/search?param=0&param=nope&param=100\",\n\t\t\texpectValue:   []float32(nil),\n\t\t\texpectError:   \"code=400, message=failed to bind field value to float32, err=strconv.ParseFloat: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=4.3&param=0\",\n\t\t\texpectValue: []float32{4.3, 0},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: []float32(nil),\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:            \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenFailFast:   true,\n\t\t\tgivenBindErrors: []error{errors.New(\"previous error\")},\n\t\t\twhenMust:        true,\n\t\t\twhenURL:         \"/search?param=1&param=100\",\n\t\t\texpectValue:     []float32(nil),\n\t\t\texpectError:     \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok (must), conversion fails, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: []float32(nil),\n\t\t\texpectError: \"code=400, message=failed to bind field value to float32, err=strconv.ParseFloat: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tb.errors = tc.givenBindErrors\n\n\t\t\tvar dest []float32\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustFloat32s(\"param\", &dest).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.Float32s(\"param\", &dest).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_Time(t *testing.T) {\n\texampleTime, _ := time.Parse(time.RFC3339, \"2020-12-23T09:45:31+02:00\")\n\tvar testCases = []struct {\n\t\texpectValue     time.Time\n\t\tname            string\n\t\twhenURL         string\n\t\twhenLayout      string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value\",\n\t\t\twhenURL:     \"/search?param=2020-12-23T09:45:31%2B02:00&param=2000-01-02T09:45:31%2B00:00\",\n\t\t\twhenLayout:  time.RFC3339,\n\t\t\texpectValue: exampleTime,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: time.Time{},\n\t\t},\n\t\t{\n\t\t\tname:          \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   time.Time{},\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=2020-12-23T09:45:31%2B02:00&param=2000-01-02T09:45:31%2B00:00\",\n\t\t\twhenLayout:  time.RFC3339,\n\t\t\texpectValue: exampleTime,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: time.Time{},\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:          \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenMust:      true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   time.Time{},\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tif tc.givenFailFast {\n\t\t\t\tb.errors = []error{errors.New(\"previous error\")}\n\t\t\t}\n\n\t\t\tdest := time.Time{}\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustTime(\"param\", &dest, tc.whenLayout).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.Time(\"param\", &dest, tc.whenLayout).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_Times(t *testing.T) {\n\texampleTime, _ := time.Parse(time.RFC3339, \"2020-12-23T09:45:31+02:00\")\n\texampleTime2, _ := time.Parse(time.RFC3339, \"2000-01-02T09:45:31+00:00\")\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenURL         string\n\t\twhenLayout      string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\texpectValue     []time.Time\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value\",\n\t\t\twhenURL:     \"/search?param=2020-12-23T09:45:31%2B02:00&param=2000-01-02T09:45:31%2B00:00\",\n\t\t\twhenLayout:  time.RFC3339,\n\t\t\texpectValue: []time.Time{exampleTime, exampleTime2},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: []time.Time(nil),\n\t\t},\n\t\t{\n\t\t\tname:            \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast:   true,\n\t\t\tgivenBindErrors: []error{errors.New(\"previous error\")},\n\t\t\twhenURL:         \"/search?param=1&param=100\",\n\t\t\texpectValue:     []time.Time(nil),\n\t\t\texpectError:     \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=2020-12-23T09:45:31%2B02:00&param=2000-01-02T09:45:31%2B00:00\",\n\t\t\twhenLayout:  time.RFC3339,\n\t\t\texpectValue: []time.Time{exampleTime, exampleTime2},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: []time.Time(nil),\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:            \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenFailFast:   true,\n\t\t\tgivenBindErrors: []error{errors.New(\"previous error\")},\n\t\t\twhenMust:        true,\n\t\t\twhenURL:         \"/search?param=1&param=100\",\n\t\t\texpectValue:     []time.Time(nil),\n\t\t\texpectError:     \"previous error\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tb.errors = tc.givenBindErrors\n\n\t\t\tlayout := time.RFC3339\n\t\t\tif tc.whenLayout != \"\" {\n\t\t\t\tlayout = tc.whenLayout\n\t\t\t}\n\n\t\t\tvar dest []time.Time\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustTimes(\"param\", &dest, layout).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.Times(\"param\", &dest, layout).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_Duration(t *testing.T) {\n\texample := 42 * time.Second\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenURL         string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\texpectValue     time.Duration\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value\",\n\t\t\twhenURL:     \"/search?param=42s&param=1ms\",\n\t\t\texpectValue: example,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: 0,\n\t\t},\n\t\t{\n\t\t\tname:          \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   0,\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=42s&param=1ms\",\n\t\t\texpectValue: example,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: 0,\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:          \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenMust:      true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   0,\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tif tc.givenFailFast {\n\t\t\t\tb.errors = []error{errors.New(\"previous error\")}\n\t\t\t}\n\n\t\t\tvar dest time.Duration\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustDuration(\"param\", &dest).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.Duration(\"param\", &dest).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_Durations(t *testing.T) {\n\texampleDuration := 42 * time.Second\n\texampleDuration2 := 1 * time.Millisecond\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenURL         string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\texpectValue     []time.Duration\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value\",\n\t\t\twhenURL:     \"/search?param=42s&param=1ms\",\n\t\t\texpectValue: []time.Duration{exampleDuration, exampleDuration2},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: []time.Duration(nil),\n\t\t},\n\t\t{\n\t\t\tname:            \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast:   true,\n\t\t\tgivenBindErrors: []error{errors.New(\"previous error\")},\n\t\t\twhenURL:         \"/search?param=1&param=100\",\n\t\t\texpectValue:     []time.Duration(nil),\n\t\t\texpectError:     \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=42s&param=1ms\",\n\t\t\texpectValue: []time.Duration{exampleDuration, exampleDuration2},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: []time.Duration(nil),\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:            \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenFailFast:   true,\n\t\t\tgivenBindErrors: []error{errors.New(\"previous error\")},\n\t\t\twhenMust:        true,\n\t\t\twhenURL:         \"/search?param=1&param=100\",\n\t\t\texpectValue:     []time.Duration(nil),\n\t\t\texpectError:     \"previous error\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tb.errors = tc.givenBindErrors\n\n\t\t\tvar dest []time.Duration\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustDurations(\"param\", &dest).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.Durations(\"param\", &dest).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_BindUnmarshaler(t *testing.T) {\n\texampleTime, _ := time.Parse(time.RFC3339, \"2020-12-23T09:45:31+02:00\")\n\n\tvar testCases = []struct {\n\t\texpectValue     Timestamp\n\t\tname            string\n\t\twhenURL         string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value\",\n\t\t\twhenURL:     \"/search?param=2020-12-23T09:45:31%2B02:00&param=2000-01-02T09:45:31%2B00:00\",\n\t\t\texpectValue: Timestamp(exampleTime),\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: Timestamp{},\n\t\t},\n\t\t{\n\t\t\tname:          \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   Timestamp{},\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, conversion fails, value is not changed\",\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: Timestamp{},\n\t\t\texpectError: \"code=400, message=failed to bind field value to BindUnmarshaler interface, err=parsing time \\\"nope\\\" as \\\"2006-01-02T15:04:05Z07:00\\\": cannot parse \\\"nope\\\" as \\\"2006\\\", field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=2020-12-23T09:45:31%2B02:00&param=2000-01-02T09:45:31%2B00:00\",\n\t\t\texpectValue: Timestamp(exampleTime),\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: Timestamp{},\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:          \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenMust:      true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   Timestamp{},\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok (must), conversion fails, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: Timestamp{},\n\t\t\texpectError: \"code=400, message=failed to bind field value to BindUnmarshaler interface, err=parsing time \\\"nope\\\" as \\\"2006-01-02T15:04:05Z07:00\\\": cannot parse \\\"nope\\\" as \\\"2006\\\", field=param\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tif tc.givenFailFast {\n\t\t\t\tb.errors = []error{errors.New(\"previous error\")}\n\t\t\t}\n\n\t\t\tvar dest Timestamp\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustBindUnmarshaler(\"param\", &dest).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.BindUnmarshaler(\"param\", &dest).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_JSONUnmarshaler(t *testing.T) {\n\texample := big.NewInt(999)\n\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenURL         string\n\t\texpectError     string\n\t\texpectValue     big.Int\n\t\tgivenBindErrors []error\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value\",\n\t\t\twhenURL:     \"/search?param=999&param=998\",\n\t\t\texpectValue: *example,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: big.Int{},\n\t\t},\n\t\t{\n\t\t\tname:          \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   big.Int{},\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, conversion fails, value is not changed\",\n\t\t\twhenURL:     \"/search?param=nope&param=xxx\",\n\t\t\texpectValue: big.Int{},\n\t\t\texpectError: \"code=400, message=failed to bind field value to json.Unmarshaler interface, err=math/big: cannot unmarshal \\\"nope\\\" into a *big.Int, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=999&param=998\",\n\t\t\texpectValue: *example,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: big.Int{},\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:          \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenMust:      true,\n\t\t\twhenURL:       \"/search?param=1&param=xxx\",\n\t\t\texpectValue:   big.Int{},\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok (must), conversion fails, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=nope&param=xxx\",\n\t\t\texpectValue: big.Int{},\n\t\t\texpectError: \"code=400, message=failed to bind field value to json.Unmarshaler interface, err=math/big: cannot unmarshal \\\"nope\\\" into a *big.Int, field=param\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tif tc.givenFailFast {\n\t\t\t\tb.errors = []error{errors.New(\"previous error\")}\n\t\t\t}\n\n\t\t\tvar dest big.Int\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustJSONUnmarshaler(\"param\", &dest).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.JSONUnmarshaler(\"param\", &dest).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_TextUnmarshaler(t *testing.T) {\n\texample := big.NewInt(999)\n\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenURL         string\n\t\texpectError     string\n\t\texpectValue     big.Int\n\t\tgivenBindErrors []error\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value\",\n\t\t\twhenURL:     \"/search?param=999&param=998\",\n\t\t\texpectValue: *example,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: big.Int{},\n\t\t},\n\t\t{\n\t\t\tname:          \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   big.Int{},\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, conversion fails, value is not changed\",\n\t\t\twhenURL:     \"/search?param=nope&param=xxx\",\n\t\t\texpectValue: big.Int{},\n\t\t\texpectError: \"code=400, message=failed to bind field value to encoding.TextUnmarshaler interface, err=math/big: cannot unmarshal \\\"nope\\\" into a *big.Int, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=999&param=998\",\n\t\t\texpectValue: *example,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: big.Int{},\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:          \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenMust:      true,\n\t\t\twhenURL:       \"/search?param=1&param=xxx\",\n\t\t\texpectValue:   big.Int{},\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok (must), conversion fails, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=nope&param=xxx\",\n\t\t\texpectValue: big.Int{},\n\t\t\texpectError: \"code=400, message=failed to bind field value to encoding.TextUnmarshaler interface, err=math/big: cannot unmarshal \\\"nope\\\" into a *big.Int, field=param\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tif tc.givenFailFast {\n\t\t\t\tb.errors = []error{errors.New(\"previous error\")}\n\t\t\t}\n\n\t\t\tvar dest big.Int\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustTextUnmarshaler(\"param\", &dest).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.TextUnmarshaler(\"param\", &dest).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_BindWithDelimiter_types(t *testing.T) {\n\tvar testCases = []struct {\n\t\texpect  any\n\t\tname    string\n\t\twhenURL string\n\t}{\n\t\t{\n\t\t\tname:   \"ok, strings\",\n\t\t\texpect: []string{\"1\", \"2\", \"1\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, int64\",\n\t\t\texpect: []int64{1, 2, 1},\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, int32\",\n\t\t\texpect: []int32{1, 2, 1},\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, int16\",\n\t\t\texpect: []int16{1, 2, 1},\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, int8\",\n\t\t\texpect: []int8{1, 2, 1},\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, int\",\n\t\t\texpect: []int{1, 2, 1},\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, uint64\",\n\t\t\texpect: []uint64{1, 2, 1},\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, uint32\",\n\t\t\texpect: []uint32{1, 2, 1},\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, uint16\",\n\t\t\texpect: []uint16{1, 2, 1},\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, uint8\",\n\t\t\texpect: []uint8{1, 2, 1},\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, uint\",\n\t\t\texpect: []uint{1, 2, 1},\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, float64\",\n\t\t\texpect: []float64{1, 2, 1},\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, float32\",\n\t\t\texpect: []float32{1, 2, 1},\n\t\t},\n\t\t{\n\t\t\tname:    \"ok, bool\",\n\t\t\twhenURL: \"/search?param=1,false&param=true\",\n\t\t\texpect:  []bool{true, false, true},\n\t\t},\n\t\t{\n\t\t\tname:    \"ok, Duration\",\n\t\t\twhenURL: \"/search?param=1s,42s&param=1ms\",\n\t\t\texpect:  []time.Duration{1 * time.Second, 42 * time.Second, 1 * time.Millisecond},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tURL := \"/search?param=1,2&param=1\"\n\t\t\tif tc.whenURL != \"\" {\n\t\t\t\tURL = tc.whenURL\n\t\t\t}\n\t\t\tc := createTestContext(URL, nil, nil)\n\t\t\tb := QueryParamsBinder(c)\n\n\t\t\tswitch tc.expect.(type) {\n\t\t\tcase []string:\n\t\t\t\tvar dest []string\n\t\t\t\tassert.NoError(t, b.BindWithDelimiter(\"param\", &dest, \",\").BindError())\n\t\t\t\tassert.Equal(t, tc.expect, dest)\n\t\t\tcase []int64:\n\t\t\t\tvar dest []int64\n\t\t\t\tassert.NoError(t, b.BindWithDelimiter(\"param\", &dest, \",\").BindError())\n\t\t\t\tassert.Equal(t, tc.expect, dest)\n\t\t\tcase []int32:\n\t\t\t\tvar dest []int32\n\t\t\t\tassert.NoError(t, b.BindWithDelimiter(\"param\", &dest, \",\").BindError())\n\t\t\t\tassert.Equal(t, tc.expect, dest)\n\t\t\tcase []int16:\n\t\t\t\tvar dest []int16\n\t\t\t\tassert.NoError(t, b.BindWithDelimiter(\"param\", &dest, \",\").BindError())\n\t\t\t\tassert.Equal(t, tc.expect, dest)\n\t\t\tcase []int8:\n\t\t\t\tvar dest []int8\n\t\t\t\tassert.NoError(t, b.BindWithDelimiter(\"param\", &dest, \",\").BindError())\n\t\t\t\tassert.Equal(t, tc.expect, dest)\n\t\t\tcase []int:\n\t\t\t\tvar dest []int\n\t\t\t\tassert.NoError(t, b.BindWithDelimiter(\"param\", &dest, \",\").BindError())\n\t\t\t\tassert.Equal(t, tc.expect, dest)\n\t\t\tcase []uint64:\n\t\t\t\tvar dest []uint64\n\t\t\t\tassert.NoError(t, b.BindWithDelimiter(\"param\", &dest, \",\").BindError())\n\t\t\t\tassert.Equal(t, tc.expect, dest)\n\t\t\tcase []uint32:\n\t\t\t\tvar dest []uint32\n\t\t\t\tassert.NoError(t, b.BindWithDelimiter(\"param\", &dest, \",\").BindError())\n\t\t\t\tassert.Equal(t, tc.expect, dest)\n\t\t\tcase []uint16:\n\t\t\t\tvar dest []uint16\n\t\t\t\tassert.NoError(t, b.BindWithDelimiter(\"param\", &dest, \",\").BindError())\n\t\t\t\tassert.Equal(t, tc.expect, dest)\n\t\t\tcase []uint8:\n\t\t\t\tvar dest []uint8\n\t\t\t\tassert.NoError(t, b.BindWithDelimiter(\"param\", &dest, \",\").BindError())\n\t\t\t\tassert.Equal(t, tc.expect, dest)\n\t\t\tcase []uint:\n\t\t\t\tvar dest []uint\n\t\t\t\tassert.NoError(t, b.BindWithDelimiter(\"param\", &dest, \",\").BindError())\n\t\t\t\tassert.Equal(t, tc.expect, dest)\n\t\t\tcase []float64:\n\t\t\t\tvar dest []float64\n\t\t\t\tassert.NoError(t, b.BindWithDelimiter(\"param\", &dest, \",\").BindError())\n\t\t\t\tassert.Equal(t, tc.expect, dest)\n\t\t\tcase []float32:\n\t\t\t\tvar dest []float32\n\t\t\t\tassert.NoError(t, b.BindWithDelimiter(\"param\", &dest, \",\").BindError())\n\t\t\t\tassert.Equal(t, tc.expect, dest)\n\t\t\tcase []bool:\n\t\t\t\tvar dest []bool\n\t\t\t\tassert.NoError(t, b.BindWithDelimiter(\"param\", &dest, \",\").BindError())\n\t\t\t\tassert.Equal(t, tc.expect, dest)\n\t\t\tcase []time.Duration:\n\t\t\t\tvar dest []time.Duration\n\t\t\t\tassert.NoError(t, b.BindWithDelimiter(\"param\", &dest, \",\").BindError())\n\t\t\t\tassert.Equal(t, tc.expect, dest)\n\t\t\tdefault:\n\t\t\t\tassert.Fail(t, \"invalid type\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_BindWithDelimiter(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenURL         string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\texpectValue     []int64\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value\",\n\t\t\twhenURL:     \"/search?param=1,2&param=1\",\n\t\t\texpectValue: []int64{1, 2, 1},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: []int64(nil),\n\t\t},\n\t\t{\n\t\t\tname:          \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   []int64(nil),\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, conversion fails, value is not changed\",\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: []int64(nil),\n\t\t\texpectError: \"code=400, message=failed to bind field value to int64, err=strconv.ParseInt: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=1,2&param=1\",\n\t\t\texpectValue: []int64{1, 2, 1},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: []int64(nil),\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:          \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenMust:      true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   []int64(nil),\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok (must), conversion fails, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: []int64(nil),\n\t\t\texpectError: \"code=400, message=failed to bind field value to int64, err=strconv.ParseInt: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tif tc.givenFailFast {\n\t\t\t\tb.errors = []error{errors.New(\"previous error\")}\n\t\t\t}\n\n\t\t\tvar dest []int64\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustBindWithDelimiter(\"param\", &dest, \",\").BindError()\n\t\t\t} else {\n\t\t\t\terr = b.BindWithDelimiter(\"param\", &dest, \",\").BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBindWithDelimiter_invalidType(t *testing.T) {\n\tc := createTestContext(\"/search?param=1&param=100\", nil, nil)\n\tb := QueryParamsBinder(c)\n\n\tvar dest []BindUnmarshaler\n\terr := b.BindWithDelimiter(\"param\", &dest, \",\").BindError()\n\tassert.Equal(t, []BindUnmarshaler(nil), dest)\n\tassert.EqualError(t, err, \"code=400, message=unsupported bind type, field=param\")\n}\n\nfunc TestValueBinder_UnixTime(t *testing.T) {\n\texampleTime, _ := time.Parse(time.RFC3339, \"2020-12-28T18:36:43+00:00\") // => 1609180603\n\tvar testCases = []struct {\n\t\texpectValue     time.Time\n\t\tname            string\n\t\twhenURL         string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value, unix time in seconds\",\n\t\t\twhenURL:     \"/search?param=1609180603&param=1609180604\",\n\t\t\texpectValue: exampleTime,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, binds value, unix time over int32 value\",\n\t\t\twhenURL:     \"/search?param=2147483648&param=1609180604\",\n\t\t\texpectValue: time.Unix(2147483648, 0),\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: time.Time{},\n\t\t},\n\t\t{\n\t\t\tname:          \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   time.Time{},\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, conversion fails, value is not changed\",\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: time.Time{},\n\t\t\texpectError: \"code=400, message=failed to bind field value to Time, err=strconv.ParseInt: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=1609180603&param=1609180604\",\n\t\t\texpectValue: exampleTime,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: time.Time{},\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:          \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenMust:      true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   time.Time{},\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok (must), conversion fails, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: time.Time{},\n\t\t\texpectError: \"code=400, message=failed to bind field value to Time, err=strconv.ParseInt: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tif tc.givenFailFast {\n\t\t\t\tb.errors = []error{errors.New(\"previous error\")}\n\t\t\t}\n\n\t\t\tdest := time.Time{}\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustUnixTime(\"param\", &dest).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.UnixTime(\"param\", &dest).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue.UnixNano(), dest.UnixNano())\n\t\t\tassert.Equal(t, tc.expectValue.In(time.UTC), dest.In(time.UTC))\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_UnixTimeMilli(t *testing.T) {\n\texampleTime, _ := time.Parse(time.RFC3339Nano, \"2022-03-13T15:13:30.140000000+00:00\") // => 1647184410140\n\tvar testCases = []struct {\n\t\texpectValue     time.Time\n\t\tname            string\n\t\twhenURL         string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value, unix time in milliseconds\",\n\t\t\twhenURL:     \"/search?param=1647184410140&param=1647184410199\",\n\t\t\texpectValue: exampleTime,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: time.Time{},\n\t\t},\n\t\t{\n\t\t\tname:          \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   time.Time{},\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, conversion fails, value is not changed\",\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: time.Time{},\n\t\t\texpectError: \"code=400, message=failed to bind field value to Time, err=strconv.ParseInt: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=1647184410140&param=1647184410199\",\n\t\t\texpectValue: exampleTime,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: time.Time{},\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:          \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenMust:      true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   time.Time{},\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok (must), conversion fails, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: time.Time{},\n\t\t\texpectError: \"code=400, message=failed to bind field value to Time, err=strconv.ParseInt: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tif tc.givenFailFast {\n\t\t\t\tb.errors = []error{errors.New(\"previous error\")}\n\t\t\t}\n\n\t\t\tdest := time.Time{}\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustUnixTimeMilli(\"param\", &dest).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.UnixTimeMilli(\"param\", &dest).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue.UnixNano(), dest.UnixNano())\n\t\t\tassert.Equal(t, tc.expectValue.In(time.UTC), dest.In(time.UTC))\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_UnixTimeNano(t *testing.T) {\n\texampleTime, _ := time.Parse(time.RFC3339, \"2020-12-28T18:36:43.000000000+00:00\")         // => 1609180603\n\texampleTimeNano, _ := time.Parse(time.RFC3339Nano, \"2020-12-28T18:36:43.123456789+00:00\") // => 1609180603123456789\n\texampleTimeNanoBelowSec, _ := time.Parse(time.RFC3339Nano, \"1970-01-01T00:00:00.999999999+00:00\")\n\tvar testCases = []struct {\n\t\texpectValue     time.Time\n\t\tname            string\n\t\twhenURL         string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"ok, binds value, unix time in nano seconds (sec precision)\",\n\t\t\twhenURL:     \"/search?param=1609180603000000000&param=1609180604\",\n\t\t\texpectValue: exampleTime,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, binds value, unix time in nano seconds\",\n\t\t\twhenURL:     \"/search?param=1609180603123456789&param=1609180604\",\n\t\t\texpectValue: exampleTimeNano,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, binds value, unix time in nano seconds (below 1 sec)\",\n\t\t\twhenURL:     \"/search?param=999999999&param=1609180604\",\n\t\t\texpectValue: exampleTimeNanoBelowSec,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, params values empty, value is not changed\",\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: time.Time{},\n\t\t},\n\t\t{\n\t\t\tname:          \"nok, previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   time.Time{},\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, conversion fails, value is not changed\",\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: time.Time{},\n\t\t\texpectError: \"code=400, message=failed to bind field value to Time, err=strconv.ParseInt: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), binds value\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=1609180603000000000&param=1609180604\",\n\t\t\texpectValue: exampleTime,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok (must), params values empty, returns error, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?nope=1\",\n\t\t\texpectValue: time.Time{},\n\t\t\texpectError: \"code=400, message=required field value is empty, field=param\",\n\t\t},\n\t\t{\n\t\t\tname:          \"nok (must), previous errors fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenMust:      true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   time.Time{},\n\t\t\texpectError:   \"previous error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok (must), conversion fails, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: time.Time{},\n\t\t\texpectError: \"code=400, message=failed to bind field value to Time, err=strconv.ParseInt: parsing \\\"nope\\\": invalid syntax, field=param\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tif tc.givenFailFast {\n\t\t\t\tb.errors = []error{errors.New(\"previous error\")}\n\t\t\t}\n\n\t\t\tdest := time.Time{}\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustUnixTimeNano(\"param\", &dest).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.UnixTimeNano(\"param\", &dest).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue.UnixNano(), dest.UnixNano())\n\t\t\tassert.Equal(t, tc.expectValue.In(time.UTC), dest.In(time.UTC))\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc BenchmarkDefaultBinder_BindInt64_single(b *testing.B) {\n\ttype Opts struct {\n\t\tParam int64 `query:\"param\"`\n\t}\n\tc := createTestContext(\"/search?param=1&param=100\", nil, nil)\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tbinder := new(DefaultBinder)\n\tfor i := 0; i < b.N; i++ {\n\t\tvar dest Opts\n\t\t_ = binder.Bind(c, &dest)\n\t}\n}\n\nfunc BenchmarkValueBinder_BindInt64_single(b *testing.B) {\n\tc := createTestContext(\"/search?param=1&param=100\", nil, nil)\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\ttype Opts struct {\n\t\tParam int64\n\t}\n\tbinder := QueryParamsBinder(c)\n\tfor i := 0; i < b.N; i++ {\n\t\tvar dest Opts\n\t\t_ = binder.Int64(\"param\", &dest.Param).BindError()\n\t}\n}\n\nfunc BenchmarkRawFunc_Int64_single(b *testing.B) {\n\tc := createTestContext(\"/search?param=1&param=100\", nil, nil)\n\n\trawFunc := func(input string, defaultValue int64) (int64, bool) {\n\t\tif input == \"\" {\n\t\t\treturn defaultValue, true\n\t\t}\n\t\tn, err := strconv.Atoi(input)\n\t\tif err != nil {\n\t\t\treturn 0, false\n\t\t}\n\t\treturn int64(n), true\n\t}\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\ttype Opts struct {\n\t\tParam int64\n\t}\n\tfor i := 0; i < b.N; i++ {\n\t\tvar dest Opts\n\t\tif n, ok := rawFunc(c.QueryParam(\"param\"), 1); ok {\n\t\t\tdest.Param = n\n\t\t}\n\t}\n}\n\nfunc BenchmarkDefaultBinder_BindInt64_10_fields(b *testing.B) {\n\ttype Opts struct {\n\t\tString  string   `query:\"string\"`\n\t\tStrings []string `query:\"strings\"`\n\t\tInt64   int64    `query:\"int64\"`\n\t\tUint64  uint64   `query:\"uint64\"`\n\t\tInt32   int32    `query:\"int32\"`\n\t\tUint32  uint32   `query:\"uint32\"`\n\t\tInt16   int16    `query:\"int16\"`\n\t\tUint16  uint16   `query:\"uint16\"`\n\t\tInt8    int8     `query:\"int8\"`\n\t\tUint8   uint8    `query:\"uint8\"`\n\t}\n\tc := createTestContext(\"/search?int64=1&int32=2&int16=3&int8=4&string=test&uint64=5&uint32=6&uint16=7&uint8=8&strings=first&strings=second\", nil, nil)\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tbinder := new(DefaultBinder)\n\tfor i := 0; i < b.N; i++ {\n\t\tvar dest Opts\n\t\t_ = binder.Bind(c, &dest)\n\t\tif dest.Int64 != 1 {\n\t\t\tb.Fatalf(\"int64!=1\")\n\t\t}\n\t}\n}\n\nfunc BenchmarkValueBinder_BindInt64_10_fields(b *testing.B) {\n\ttype Opts struct {\n\t\tString  string   `query:\"string\"`\n\t\tStrings []string `query:\"strings\"`\n\t\tInt64   int64    `query:\"int64\"`\n\t\tUint64  uint64   `query:\"uint64\"`\n\t\tInt32   int32    `query:\"int32\"`\n\t\tUint32  uint32   `query:\"uint32\"`\n\t\tInt16   int16    `query:\"int16\"`\n\t\tUint16  uint16   `query:\"uint16\"`\n\t\tInt8    int8     `query:\"int8\"`\n\t\tUint8   uint8    `query:\"uint8\"`\n\t}\n\tc := createTestContext(\"/search?int64=1&int32=2&int16=3&int8=4&string=test&uint64=5&uint32=6&uint16=7&uint8=8&strings=first&strings=second\", nil, nil)\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tbinder := QueryParamsBinder(c)\n\tfor i := 0; i < b.N; i++ {\n\t\tvar dest Opts\n\t\t_ = binder.\n\t\t\tInt64(\"int64\", &dest.Int64).\n\t\t\tInt32(\"int32\", &dest.Int32).\n\t\t\tInt16(\"int16\", &dest.Int16).\n\t\t\tInt8(\"int8\", &dest.Int8).\n\t\t\tString(\"string\", &dest.String).\n\t\t\tUint64(\"int64\", &dest.Uint64).\n\t\t\tUint32(\"int32\", &dest.Uint32).\n\t\t\tUint16(\"int16\", &dest.Uint16).\n\t\t\tUint8(\"int8\", &dest.Uint8).\n\t\t\tStrings(\"strings\", &dest.Strings).\n\t\t\tBindError()\n\t\tif dest.Int64 != 1 {\n\t\t\tb.Fatalf(\"int64!=1\")\n\t\t}\n\t}\n}\n\nfunc TestValueBinder_TimeError(t *testing.T) {\n\tvar testCases = []struct {\n\t\texpectValue     time.Time\n\t\tname            string\n\t\twhenURL         string\n\t\twhenLayout      string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"nok, conversion fails, value is not changed\",\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: time.Time{},\n\t\t\texpectError: \"code=400, message=failed to bind field value to Time, err=parsing time \\\"nope\\\": extra text: \\\"nope\\\", field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok (must), conversion fails, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: time.Time{},\n\t\t\texpectError: \"code=400, message=failed to bind field value to Time, err=parsing time \\\"nope\\\": extra text: \\\"nope\\\", field=param\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tif tc.givenFailFast {\n\t\t\t\tb.errors = []error{errors.New(\"previous error\")}\n\t\t\t}\n\n\t\t\tdest := time.Time{}\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustTime(\"param\", &dest, tc.whenLayout).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.Time(\"param\", &dest, tc.whenLayout).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_TimesError(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenURL         string\n\t\twhenLayout      string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\texpectValue     []time.Time\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:          \"nok, fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   []time.Time(nil),\n\t\t\texpectError:   \"code=400, message=failed to bind field value to Time, err=parsing time \\\"1\\\" as \\\"2006-01-02T15:04:05Z07:00\\\": cannot parse \\\"1\\\" as \\\"2006\\\", field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, conversion fails, value is not changed\",\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: []time.Time(nil),\n\t\t\texpectError: \"code=400, message=failed to bind field value to Time, err=parsing time \\\"nope\\\" as \\\"2006-01-02T15:04:05Z07:00\\\": cannot parse \\\"nope\\\" as \\\"2006\\\", field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok (must), conversion fails, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: []time.Time(nil),\n\t\t\texpectError: \"code=400, message=failed to bind field value to Time, err=parsing time \\\"nope\\\" as \\\"2006-01-02T15:04:05Z07:00\\\": cannot parse \\\"nope\\\" as \\\"2006\\\", field=param\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tb.errors = tc.givenBindErrors\n\n\t\t\tlayout := time.RFC3339\n\t\t\tif tc.whenLayout != \"\" {\n\t\t\t\tlayout = tc.whenLayout\n\t\t\t}\n\n\t\t\tvar dest []time.Time\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustTimes(\"param\", &dest, layout).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.Times(\"param\", &dest, layout).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_DurationError(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenURL         string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\texpectValue     time.Duration\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:        \"nok, conversion fails, value is not changed\",\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: 0,\n\t\t\texpectError: \"code=400, message=failed to bind field value to Duration, err=time: invalid duration \\\"nope\\\", field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok (must), conversion fails, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: 0,\n\t\t\texpectError: \"code=400, message=failed to bind field value to Duration, err=time: invalid duration \\\"nope\\\", field=param\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tif tc.givenFailFast {\n\t\t\t\tb.errors = []error{errors.New(\"previous error\")}\n\t\t\t}\n\n\t\t\tvar dest time.Duration\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustDuration(\"param\", &dest).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.Duration(\"param\", &dest).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValueBinder_DurationsError(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenURL         string\n\t\texpectError     string\n\t\tgivenBindErrors []error\n\t\texpectValue     []time.Duration\n\t\tgivenFailFast   bool\n\t\twhenMust        bool\n\t}{\n\t\t{\n\t\t\tname:          \"nok, fail fast without binding value\",\n\t\t\tgivenFailFast: true,\n\t\t\twhenURL:       \"/search?param=1&param=100\",\n\t\t\texpectValue:   []time.Duration(nil),\n\t\t\texpectError:   \"code=400, message=failed to bind field value to Duration, err=time: missing unit in duration \\\"1\\\", field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, conversion fails, value is not changed\",\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: []time.Duration(nil),\n\t\t\texpectError: \"code=400, message=failed to bind field value to Duration, err=time: invalid duration \\\"nope\\\", field=param\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok (must), conversion fails, value is not changed\",\n\t\t\twhenMust:    true,\n\t\t\twhenURL:     \"/search?param=nope&param=100\",\n\t\t\texpectValue: []time.Duration(nil),\n\t\t\texpectError: \"code=400, message=failed to bind field value to Duration, err=time: invalid duration \\\"nope\\\", field=param\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := createTestContext(tc.whenURL, nil, nil)\n\t\t\tb := QueryParamsBinder(c).FailFast(tc.givenFailFast)\n\t\t\tb.errors = tc.givenBindErrors\n\n\t\t\tvar dest []time.Duration\n\t\t\tvar err error\n\t\t\tif tc.whenMust {\n\t\t\t\terr = b.MustDurations(\"param\", &dest).BindError()\n\t\t\t} else {\n\t\t\t\terr = b.Durations(\"param\", &dest).BindError()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectValue, dest)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "codecov.yml",
    "content": "coverage:\n  status:\n    project:\n      default:\n        threshold: 1%\n    patch:\n      default:\n        threshold: 1%\n\ncomment:\n  require_changes: true"
  },
  {
    "path": "context.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"bytes\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"mime/multipart\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n)\n\nconst (\n\t// ContextKeyHeaderAllow is set by Router for getting value for `Allow` header in later stages of handler call chain.\n\t// Allow header is mandatory for status 405 (method not found) and useful for OPTIONS method requests.\n\t// It is added to context only when Router does not find matching method handler for request.\n\tContextKeyHeaderAllow = \"echo_header_allow\"\n)\n\nconst (\n\t// defaultMemory is default value for memory limit that is used when\n\t// parsing multipart forms (See (*http.Request).ParseMultipartForm)\n\tdefaultMemory int64 = 32 << 20 // 32 MB\n\tindexPage           = \"index.html\"\n)\n\n// Context represents the context of the current HTTP request. It holds request and\n// response objects, path, path parameters, data and registered handler.\ntype Context struct {\n\trequest     *http.Request\n\torgResponse *Response\n\tresponse    http.ResponseWriter\n\tquery       url.Values\n\n\t// formParseMaxMemory is used for http.Request.ParseMultipartForm\n\tformParseMaxMemory int64\n\n\troute      *RouteInfo\n\tpathValues *PathValues\n\n\tstore  map[string]any\n\techo   *Echo\n\tlogger *slog.Logger\n\n\tpath string\n\tlock sync.RWMutex\n}\n\n// NewContext returns a new Context instance.\n//\n// Note: request,response and e can be left to nil as Echo.ServeHTTP will call c.Reset(req,resp) anyway\n// these arguments are useful when creating context for tests and cases like that.\nfunc NewContext(r *http.Request, w http.ResponseWriter, opts ...any) *Context {\n\tvar e *Echo\n\tfor _, opt := range opts {\n\t\tswitch v := opt.(type) {\n\t\tcase *Echo:\n\t\t\te = v\n\t\t}\n\t}\n\treturn newContext(r, w, e)\n}\n\nfunc newContext(r *http.Request, w http.ResponseWriter, e *Echo) *Context {\n\tc := &Context{\n\t\tpathValues: nil,\n\t\tstore:      make(map[string]any),\n\t\techo:       e,\n\t\tlogger:     nil,\n\t}\n\tvar logger *slog.Logger\n\tparamLen := int32(0)\n\tformParseMaxMemory := defaultMemory\n\tif e != nil {\n\t\tparamLen = e.contextPathParamAllocSize.Load()\n\t\tlogger = e.Logger\n\t\tformParseMaxMemory = e.formParseMaxMemory\n\t}\n\tif logger == nil {\n\t\tlogger = slog.Default()\n\t}\n\tc.logger = logger\n\tp := make(PathValues, 0, paramLen)\n\tc.pathValues = &p\n\n\tc.SetRequest(r)\n\tc.orgResponse = NewResponse(w, logger)\n\tc.response = c.orgResponse\n\tc.formParseMaxMemory = formParseMaxMemory\n\treturn c\n}\n\n// Reset resets the context after request completes. It must be called along\n// with `Echo#AcquireContext()` and `Echo#ReleaseContext()`.\n// See `Echo#ServeHTTP()`\nfunc (c *Context) Reset(r *http.Request, w http.ResponseWriter) {\n\tc.request = r\n\tc.orgResponse.reset(w)\n\tc.response = c.orgResponse\n\tc.query = nil\n\tc.store = nil\n\tc.logger = c.echo.Logger\n\n\tc.route = nil\n\tc.path = \"\"\n\t// NOTE: empty by setting length to 0. PathValues has to have capacity of c.echo.contextPathParamAllocSize at all times\n\t*c.pathValues = (*c.pathValues)[:0]\n}\n\nfunc (c *Context) writeContentType(value string) {\n\theader := c.response.Header()\n\tif header.Get(HeaderContentType) == \"\" {\n\t\theader.Set(HeaderContentType, value)\n\t}\n}\n\n// Request returns `*http.Request`.\nfunc (c *Context) Request() *http.Request {\n\treturn c.request\n}\n\n// SetRequest sets `*http.Request`.\nfunc (c *Context) SetRequest(r *http.Request) {\n\tc.request = r\n}\n\n// Response returns `*Response`.\nfunc (c *Context) Response() http.ResponseWriter {\n\treturn c.response\n}\n\n// SetResponse sets `*http.ResponseWriter`. Some context methods and/or middleware require that given ResponseWriter implements following\n// method `Unwrap() http.ResponseWriter` which eventually should return *echo.Response instance.\nfunc (c *Context) SetResponse(r http.ResponseWriter) {\n\tc.response = r\n}\n\n// IsTLS returns true if HTTP connection is TLS otherwise false.\nfunc (c *Context) IsTLS() bool {\n\treturn c.request.TLS != nil\n}\n\n// IsWebSocket returns true if HTTP connection is WebSocket otherwise false.\nfunc (c *Context) IsWebSocket() bool {\n\tupgrade := c.request.Header.Get(HeaderUpgrade)\n\tconnection := c.request.Header.Get(HeaderConnection)\n\treturn strings.EqualFold(upgrade, \"websocket\") && strings.Contains(strings.ToLower(connection), \"upgrade\")\n}\n\n// Scheme returns the HTTP protocol scheme, `http` or `https`.\nfunc (c *Context) Scheme() string {\n\t// Can't use `r.Request.URL.Scheme`\n\t// See: https://groups.google.com/forum/#!topic/golang-nuts/pMUkBlQBDF0\n\tif c.IsTLS() {\n\t\treturn \"https\"\n\t}\n\tif scheme := c.request.Header.Get(HeaderXForwardedProto); scheme != \"\" {\n\t\treturn scheme\n\t}\n\tif scheme := c.request.Header.Get(HeaderXForwardedProtocol); scheme != \"\" {\n\t\treturn scheme\n\t}\n\tif ssl := c.request.Header.Get(HeaderXForwardedSsl); ssl == \"on\" {\n\t\treturn \"https\"\n\t}\n\tif scheme := c.request.Header.Get(HeaderXUrlScheme); scheme != \"\" {\n\t\treturn scheme\n\t}\n\treturn \"http\"\n}\n\n// RealIP returns the client's network address based on `X-Forwarded-For`\n// or `X-Real-IP` request header.\n// The behavior can be configured using `Echo#IPExtractor`.\nfunc (c *Context) RealIP() string {\n\tif c.echo != nil && c.echo.IPExtractor != nil {\n\t\treturn c.echo.IPExtractor(c.request)\n\t}\n\t// Fall back to legacy behavior\n\tif ip := c.request.Header.Get(HeaderXForwardedFor); ip != \"\" {\n\t\ti := strings.IndexAny(ip, \",\")\n\t\tif i > 0 {\n\t\t\txffip := strings.TrimSpace(ip[:i])\n\t\t\txffip = strings.TrimPrefix(xffip, \"[\")\n\t\t\txffip = strings.TrimSuffix(xffip, \"]\")\n\t\t\treturn xffip\n\t\t}\n\t\treturn ip\n\t}\n\tif ip := c.request.Header.Get(HeaderXRealIP); ip != \"\" {\n\t\tip = strings.TrimPrefix(ip, \"[\")\n\t\tip = strings.TrimSuffix(ip, \"]\")\n\t\treturn ip\n\t}\n\tra, _, _ := net.SplitHostPort(c.request.RemoteAddr)\n\treturn ra\n}\n\n// Path returns the registered path for the handler.\nfunc (c *Context) Path() string {\n\treturn c.path\n}\n\n// SetPath sets the registered path for the handler.\nfunc (c *Context) SetPath(p string) {\n\tc.path = p\n}\n\n// RouteInfo returns current request route information. Method, Path, Name and params if they exist for matched route.\n//\n// RouteInfo returns generic \"empty\" struct for these cases:\n// * Context is accessed before Routing is done. For example inside Pre middlewares (`e.Pre()`)\n// * Router did not find matching route - 404 (route not found)\n// * Router did not find matching route with same method - 405 (method not allowed)\nfunc (c *Context) RouteInfo() RouteInfo {\n\tif c.route != nil {\n\t\treturn c.route.Clone()\n\t}\n\treturn RouteInfo{}\n}\n\n// Param returns path parameter by name.\nfunc (c *Context) Param(name string) string {\n\treturn c.pathValues.GetOr(name, \"\")\n}\n\n// ParamOr returns the path parameter or default value for the provided name.\n//\n// Notes for DefaultRouter implementation:\n// Path parameter could be empty for cases like that:\n// * route `/release-:version/bin` and request URL is `/release-/bin`\n// * route `/api/:version/image.jpg` and request URL is `/api//image.jpg`\n// but not when path parameter is last part of route path\n// * route `/download/file.:ext` will not match request `/download/file.`\nfunc (c *Context) ParamOr(name, defaultValue string) string {\n\treturn c.pathValues.GetOr(name, defaultValue)\n}\n\n// PathValues returns path parameter values.\nfunc (c *Context) PathValues() PathValues {\n\treturn *c.pathValues\n}\n\n// SetPathValues sets path parameters for current request.\nfunc (c *Context) SetPathValues(pathValues PathValues) {\n\tif pathValues == nil {\n\t\tpanic(\"context SetPathValues called with nil PathValues\")\n\t}\n\tc.setPathValues(&pathValues)\n}\n\n// InitializeRoute sets the route related variables of this request to the context.\nfunc (c *Context) InitializeRoute(ri *RouteInfo, pathValues *PathValues) {\n\tc.route = ri\n\tc.path = ri.Path\n\tc.setPathValues(pathValues)\n}\n\nfunc (c *Context) setPathValues(pv *PathValues) {\n\t// Router accesses c.pathValues by index and may resize it to full capacity during routing\n\t// for that to work without going out-of-bounds we must make sure that c.pathValues slice is not replaced with smaller\n\t// slice than Router can set when routing Route with maximum amount of parameters.\n\tpathValues := c.pathValues\n\tif cap(*c.pathValues) < len(*pv) {\n\t\t// normally we should not end up here. pathValues is normally sized to Echo.contextPathParamAllocSize which should not\n\t\t// be smaller than anything router knows as maximum path parameter count to be.\n\t\ttmp := make(PathValues, len(*pv))\n\t\tc.pathValues = &tmp\n\t\tpathValues = c.pathValues\n\t} else if len(*c.pathValues) != len(*pv) {\n\t\t*pathValues = (*pathValues)[0:len(*pv)] // resize slice to given params length for copy to work\n\t}\n\tcopy(*pathValues, *pv)\n}\n\n// QueryParam returns the query param for the provided name.\nfunc (c *Context) QueryParam(name string) string {\n\tif c.query == nil {\n\t\tc.query = c.request.URL.Query()\n\t}\n\treturn c.query.Get(name)\n}\n\n// QueryParamOr returns the query param or default value for the provided name.\n// Note: QueryParamOr does not distinguish if query had no value by that name or value was empty string\n// This means URLs `/test?search=` and `/test` would both return `1` for `c.QueryParamOr(\"search\", \"1\")`\nfunc (c *Context) QueryParamOr(name, defaultValue string) string {\n\tvalue := c.QueryParam(name)\n\tif value == \"\" {\n\t\tvalue = defaultValue\n\t}\n\treturn value\n}\n\n// QueryParams returns the query parameters as `url.Values`.\nfunc (c *Context) QueryParams() url.Values {\n\tif c.query == nil {\n\t\tc.query = c.request.URL.Query()\n\t}\n\treturn c.query\n}\n\n// QueryString returns the URL query string.\nfunc (c *Context) QueryString() string {\n\treturn c.request.URL.RawQuery\n}\n\n// FormValue returns the form field value for the provided name.\nfunc (c *Context) FormValue(name string) string {\n\treturn c.request.FormValue(name)\n}\n\n// FormValueOr returns the form field value or default value for the provided name.\n// Note: FormValueOr does not distinguish if form had no value by that name or value was empty string\nfunc (c *Context) FormValueOr(name, defaultValue string) string {\n\tvalue := c.FormValue(name)\n\tif value == \"\" {\n\t\tvalue = defaultValue\n\t}\n\treturn value\n}\n\n// FormValues returns the form field values as `url.Values`.\nfunc (c *Context) FormValues() (url.Values, error) {\n\tif strings.HasPrefix(c.request.Header.Get(HeaderContentType), MIMEMultipartForm) {\n\t\tif err := c.request.ParseMultipartForm(c.formParseMaxMemory); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\tif err := c.request.ParseForm(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn c.request.Form, nil\n}\n\n// FormFile returns the multipart form file for the provided name.\nfunc (c *Context) FormFile(name string) (*multipart.FileHeader, error) {\n\tf, fh, err := c.request.FormFile(name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t_ = f.Close()\n\treturn fh, nil\n}\n\n// MultipartForm returns the multipart form.\nfunc (c *Context) MultipartForm() (*multipart.Form, error) {\n\terr := c.request.ParseMultipartForm(c.formParseMaxMemory)\n\treturn c.request.MultipartForm, err\n}\n\n// Cookie returns the named cookie provided in the request.\nfunc (c *Context) Cookie(name string) (*http.Cookie, error) {\n\treturn c.request.Cookie(name)\n}\n\n// SetCookie adds a `Set-Cookie` header in HTTP response.\nfunc (c *Context) SetCookie(cookie *http.Cookie) {\n\thttp.SetCookie(c.Response(), cookie)\n}\n\n// Cookies returns the HTTP cookies sent with the request.\nfunc (c *Context) Cookies() []*http.Cookie {\n\treturn c.request.Cookies()\n}\n\n// Get retrieves data from the context.\n// Method returns any(nil) when key does not exist which is different from typed nil (eg. []byte(nil)).\nfunc (c *Context) Get(key string) any {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.store[key]\n}\n\n// Set saves data in the context.\nfunc (c *Context) Set(key string, val any) {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\n\tif c.store == nil {\n\t\tc.store = make(map[string]any)\n\t}\n\tc.store[key] = val\n}\n\n// Bind binds path params, query params and the request body into provided type `i`. The default binder\n// binds body based on Content-Type header.\nfunc (c *Context) Bind(i any) error {\n\treturn c.echo.Binder.Bind(c, i)\n}\n\n// Validate validates provided `i`. It is usually called after `Context#Bind()`.\n// Validator must be registered using `Echo#Validator`.\nfunc (c *Context) Validate(i any) error {\n\tif c.echo.Validator == nil {\n\t\treturn ErrValidatorNotRegistered\n\t}\n\treturn c.echo.Validator.Validate(i)\n}\n\n// Render renders a template with data and sends a text/html response with status\n// code. Renderer must be registered using `Echo.Renderer`.\nfunc (c *Context) Render(code int, name string, data any) (err error) {\n\tif c.echo.Renderer == nil {\n\t\treturn ErrRendererNotRegistered\n\t}\n\t// as Renderer.Render can fail, and in that case we need to delay sending status code to the client until\n\t// (global) error handler decides the correct status code for the error to be sent to the client, so we need to write\n\t//  the rendered template to the buffer first.\n\t//\n\t// html.Template.ExecuteTemplate() documentations writes:\n\t// > If an error occurs executing the template or writing its output,\n\t// > execution stops, but partial results may already have been written to\n\t// > the output writer.\n\n\tbuf := new(bytes.Buffer)\n\tif err = c.echo.Renderer.Render(c, buf, name, data); err != nil {\n\t\treturn\n\t}\n\treturn c.HTMLBlob(code, buf.Bytes())\n}\n\n// HTML sends an HTTP response with status code.\nfunc (c *Context) HTML(code int, html string) (err error) {\n\treturn c.HTMLBlob(code, []byte(html))\n}\n\n// HTMLBlob sends an HTTP blob response with status code.\nfunc (c *Context) HTMLBlob(code int, b []byte) (err error) {\n\treturn c.Blob(code, MIMETextHTMLCharsetUTF8, b)\n}\n\n// String sends a string response with status code.\nfunc (c *Context) String(code int, s string) (err error) {\n\treturn c.Blob(code, MIMETextPlainCharsetUTF8, []byte(s))\n}\n\nfunc (c *Context) jsonPBlob(code int, callback string, i any) (err error) {\n\tc.writeContentType(MIMEApplicationJavaScriptCharsetUTF8)\n\tc.response.WriteHeader(code)\n\tif _, err = c.response.Write([]byte(callback + \"(\")); err != nil {\n\t\treturn\n\t}\n\tif err = c.echo.JSONSerializer.Serialize(c, i, \"\"); err != nil {\n\t\treturn\n\t}\n\tif _, err = c.response.Write([]byte(\");\")); err != nil {\n\t\treturn\n\t}\n\treturn\n}\n\nfunc (c *Context) json(code int, i any, indent string) error {\n\tc.writeContentType(MIMEApplicationJSON)\n\n\t// as JSONSerializer.Serialize can fail, and in that case we need to delay sending status code to the client until\n\t// (global) error handler decides correct status code for the error to be sent to the client.\n\t// For that we need to use writer that can store the proposed status code until the first Write is called.\n\tif r, err := UnwrapResponse(c.response); err == nil {\n\t\tr.Status = code\n\t} else {\n\t\tresp := c.Response()\n\t\tc.SetResponse(&delayedStatusWriter{ResponseWriter: resp, status: code})\n\t\tdefer c.SetResponse(resp)\n\t}\n\n\treturn c.echo.JSONSerializer.Serialize(c, i, indent)\n}\n\n// JSON sends a JSON response with status code.\nfunc (c *Context) JSON(code int, i any) (err error) {\n\treturn c.json(code, i, \"\")\n}\n\n// JSONPretty sends a pretty-print JSON with status code.\nfunc (c *Context) JSONPretty(code int, i any, indent string) (err error) {\n\treturn c.json(code, i, indent)\n}\n\n// JSONBlob sends a JSON blob response with status code.\nfunc (c *Context) JSONBlob(code int, b []byte) (err error) {\n\treturn c.Blob(code, MIMEApplicationJSON, b)\n}\n\n// JSONP sends a JSONP response with status code. It uses `callback` to construct\n// the JSONP payload.\nfunc (c *Context) JSONP(code int, callback string, i any) (err error) {\n\treturn c.jsonPBlob(code, callback, i)\n}\n\n// JSONPBlob sends a JSONP blob response with status code. It uses `callback`\n// to construct the JSONP payload.\nfunc (c *Context) JSONPBlob(code int, callback string, b []byte) (err error) {\n\tc.writeContentType(MIMEApplicationJavaScriptCharsetUTF8)\n\tc.response.WriteHeader(code)\n\tif _, err = c.response.Write([]byte(callback + \"(\")); err != nil {\n\t\treturn\n\t}\n\tif _, err = c.response.Write(b); err != nil {\n\t\treturn\n\t}\n\t_, err = c.response.Write([]byte(\");\"))\n\treturn\n}\n\nfunc (c *Context) xml(code int, i any, indent string) (err error) {\n\tc.writeContentType(MIMEApplicationXMLCharsetUTF8)\n\tc.response.WriteHeader(code)\n\tenc := xml.NewEncoder(c.response)\n\tif indent != \"\" {\n\t\tenc.Indent(\"\", indent)\n\t}\n\tif _, err = c.response.Write([]byte(xml.Header)); err != nil {\n\t\treturn\n\t}\n\treturn enc.Encode(i)\n}\n\n// XML sends an XML response with status code.\nfunc (c *Context) XML(code int, i any) (err error) {\n\treturn c.xml(code, i, \"\")\n}\n\n// XMLPretty sends a pretty-print XML with status code.\nfunc (c *Context) XMLPretty(code int, i any, indent string) (err error) {\n\treturn c.xml(code, i, indent)\n}\n\n// XMLBlob sends an XML blob response with status code.\nfunc (c *Context) XMLBlob(code int, b []byte) (err error) {\n\tc.writeContentType(MIMEApplicationXMLCharsetUTF8)\n\tc.response.WriteHeader(code)\n\tif _, err = c.response.Write([]byte(xml.Header)); err != nil {\n\t\treturn\n\t}\n\t_, err = c.response.Write(b)\n\treturn\n}\n\n// Blob sends a blob response with status code and content type.\nfunc (c *Context) Blob(code int, contentType string, b []byte) (err error) {\n\tc.writeContentType(contentType)\n\tc.response.WriteHeader(code)\n\t_, err = c.response.Write(b)\n\treturn\n}\n\n// Stream sends a streaming response with status code and content type.\nfunc (c *Context) Stream(code int, contentType string, r io.Reader) (err error) {\n\tc.writeContentType(contentType)\n\tc.response.WriteHeader(code)\n\t_, err = io.Copy(c.response, r)\n\treturn\n}\n\n// File sends a response with the content of the file.\nfunc (c *Context) File(file string) error {\n\treturn fsFile(c, file, c.echo.Filesystem)\n}\n\n// FileFS serves file from given file system.\n//\n// When dealing with `embed.FS` use `fs := echo.MustSubFS(fs, \"rootDirectory\") to create sub fs which uses necessary\n// prefix for directory path. This is necessary as `//go:embed assets/images` embeds files with paths\n// including `assets/images` as their prefix.\nfunc (c *Context) FileFS(file string, filesystem fs.FS) error {\n\treturn fsFile(c, file, filesystem)\n}\n\nfunc fsFile(c *Context, file string, filesystem fs.FS) error {\n\tfile = path.Clean(file) // `os.Open` and `os.DirFs.Open()` behave differently, later does not like ``, `.`, `..` at all, but we allowed those now need to clean\n\tf, err := filesystem.Open(file)\n\tif err != nil {\n\t\treturn ErrNotFound\n\t}\n\tdefer f.Close()\n\n\tfi, _ := f.Stat()\n\tif fi.IsDir() {\n\t\tfile = filepath.ToSlash(filepath.Join(file, indexPage)) // ToSlash is necessary for Windows. fs.Open and os.Open are different in that aspect.\n\t\tf, err = filesystem.Open(file)\n\t\tif err != nil {\n\t\t\treturn ErrNotFound\n\t\t}\n\t\tdefer f.Close()\n\t\tif fi, err = f.Stat(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tff, ok := f.(io.ReadSeeker)\n\tif !ok {\n\t\treturn errors.New(\"file does not implement io.ReadSeeker\")\n\t}\n\thttp.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), ff)\n\treturn nil\n}\n\n// Attachment sends a response as attachment, prompting client to save the file.\nfunc (c *Context) Attachment(file, name string) error {\n\treturn c.contentDisposition(file, name, \"attachment\")\n}\n\n// Inline sends a response as inline, opening the file in the browser.\nfunc (c *Context) Inline(file, name string) error {\n\treturn c.contentDisposition(file, name, \"inline\")\n}\n\nvar quoteEscaper = strings.NewReplacer(\"\\\\\", \"\\\\\\\\\", `\"`, \"\\\\\\\"\")\n\nfunc (c *Context) contentDisposition(file, name, dispositionType string) error {\n\tc.response.Header().Set(HeaderContentDisposition, fmt.Sprintf(`%s; filename=\"%s\"`, dispositionType, quoteEscaper.Replace(name)))\n\treturn c.File(file)\n}\n\n// NoContent sends a response with no body and a status code.\nfunc (c *Context) NoContent(code int) error {\n\tc.response.WriteHeader(code)\n\treturn nil\n}\n\n// Redirect redirects the request to a provided URL with status code.\nfunc (c *Context) Redirect(code int, url string) error {\n\tif code < 300 || code > 308 {\n\t\treturn ErrInvalidRedirectCode\n\t}\n\tc.response.Header().Set(HeaderLocation, url)\n\tc.response.WriteHeader(code)\n\treturn nil\n}\n\n// Logger returns logger in Context\nfunc (c *Context) Logger() *slog.Logger {\n\tif c.logger != nil {\n\t\treturn c.logger\n\t}\n\treturn c.echo.Logger\n}\n\n// SetLogger sets logger in Context\nfunc (c *Context) SetLogger(logger *slog.Logger) {\n\tc.logger = logger\n}\n\n// Echo returns the `Echo` instance.\nfunc (c *Context) Echo() *Echo {\n\treturn c.echo\n}\n"
  },
  {
    "path": "context_generic.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport \"errors\"\n\n// ErrNonExistentKey is error that is returned when key does not exist\nvar ErrNonExistentKey = errors.New(\"non existent key\")\n\n// ErrInvalidKeyType is error that is returned when the value is not castable to expected type.\nvar ErrInvalidKeyType = errors.New(\"invalid key type\")\n\n// ContextGet retrieves a value from the context store or ErrNonExistentKey error the key is missing.\n// Returns ErrInvalidKeyType error if the value is not castable to type T.\nfunc ContextGet[T any](c *Context, key string) (T, error) {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\n\tval, ok := c.store[key]\n\tif !ok {\n\t\tvar zero T\n\t\treturn zero, ErrNonExistentKey\n\t}\n\n\ttyped, ok := val.(T)\n\tif !ok {\n\t\tvar zero T\n\t\treturn zero, ErrInvalidKeyType\n\t}\n\n\treturn typed, nil\n}\n\n// ContextGetOr retrieves a value from the context store or returns a default value when the key\n// is missing. Returns ErrInvalidKeyType error if the value is not castable to type T.\nfunc ContextGetOr[T any](c *Context, key string, defaultValue T) (T, error) {\n\ttyped, err := ContextGet[T](c, key)\n\tif err == ErrNonExistentKey {\n\t\treturn defaultValue, nil\n\t}\n\treturn typed, err\n}\n"
  },
  {
    "path": "context_generic_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestContextGetOK(t *testing.T) {\n\tc := NewContext(nil, nil)\n\n\tc.Set(\"key\", int64(123))\n\n\tv, err := ContextGet[int64](c, \"key\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(123), v)\n}\n\nfunc TestContextGetNonExistentKey(t *testing.T) {\n\tc := NewContext(nil, nil)\n\n\tc.Set(\"key\", int64(123))\n\n\tv, err := ContextGet[int64](c, \"nope\")\n\tassert.ErrorIs(t, err, ErrNonExistentKey)\n\tassert.Equal(t, int64(0), v)\n}\n\nfunc TestContextGetInvalidCast(t *testing.T) {\n\tc := NewContext(nil, nil)\n\n\tc.Set(\"key\", int64(123))\n\n\tv, err := ContextGet[bool](c, \"key\")\n\tassert.ErrorIs(t, err, ErrInvalidKeyType)\n\tassert.Equal(t, false, v)\n}\n\nfunc TestContextGetOrOK(t *testing.T) {\n\tc := NewContext(nil, nil)\n\n\tc.Set(\"key\", int64(123))\n\n\tv, err := ContextGetOr[int64](c, \"key\", 999)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(123), v)\n}\n\nfunc TestContextGetOrNonExistentKey(t *testing.T) {\n\tc := NewContext(nil, nil)\n\n\tc.Set(\"key\", int64(123))\n\n\tv, err := ContextGetOr[int64](c, \"nope\", 999)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(999), v)\n}\n\nfunc TestContextGetOrInvalidCast(t *testing.T) {\n\tc := NewContext(nil, nil)\n\n\tc.Set(\"key\", int64(123))\n\n\tv, err := ContextGetOr[float32](c, \"key\", float32(999))\n\tassert.ErrorIs(t, err, ErrInvalidKeyType)\n\tassert.Equal(t, float32(0), v)\n}\n"
  },
  {
    "path": "context_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"math\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype Template struct {\n\ttemplates *template.Template\n}\n\nvar testUser = user{ID: 1, Name: \"Jon Snow\"}\n\nfunc BenchmarkAllocJSONP(b *testing.B) {\n\te := New()\n\te.Logger = slog.New(slog.DiscardHandler)\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(userJSON))\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\n\tfor i := 0; i < b.N; i++ {\n\t\tc.JSONP(http.StatusOK, \"callback\", testUser)\n\t}\n}\n\nfunc BenchmarkAllocJSON(b *testing.B) {\n\te := New()\n\te.Logger = slog.New(slog.DiscardHandler)\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(userJSON))\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\n\tfor i := 0; i < b.N; i++ {\n\t\tc.JSON(http.StatusOK, testUser)\n\t}\n}\n\nfunc BenchmarkAllocXML(b *testing.B) {\n\te := New()\n\te.Logger = slog.New(slog.DiscardHandler)\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(userJSON))\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\n\tfor i := 0; i < b.N; i++ {\n\t\tc.XML(http.StatusOK, testUser)\n\t}\n}\n\nfunc BenchmarkRealIPForHeaderXForwardFor(b *testing.B) {\n\tc := Context{request: &http.Request{\n\t\tHeader: http.Header{HeaderXForwardedFor: []string{\"127.0.0.1, 127.0.1.1, \"}},\n\t}}\n\tfor i := 0; i < b.N; i++ {\n\t\tc.RealIP()\n\t}\n}\n\nfunc (t *Template) Render(c *Context, w io.Writer, name string, data any) error {\n\treturn t.templates.ExecuteTemplate(w, name, data)\n}\n\nfunc TestContextEcho(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(userJSON))\n\trec := httptest.NewRecorder()\n\n\tc := e.NewContext(req, rec)\n\n\tassert.Equal(t, e, c.Echo())\n}\n\nfunc TestContextRequest(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(userJSON))\n\trec := httptest.NewRecorder()\n\n\tc := e.NewContext(req, rec)\n\n\tassert.NotNil(t, c.Request())\n\tassert.Equal(t, req, c.Request())\n}\n\nfunc TestContextResponse(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(userJSON))\n\trec := httptest.NewRecorder()\n\n\tc := e.NewContext(req, rec)\n\n\tassert.NotNil(t, c.Response())\n}\n\nfunc TestContextRenderTemplate(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(userJSON))\n\trec := httptest.NewRecorder()\n\n\tc := e.NewContext(req, rec)\n\n\ttmpl := &Template{\n\t\ttemplates: template.Must(template.New(\"hello\").Parse(\"Hello, {{.}}!\")),\n\t}\n\tc.Echo().Renderer = tmpl\n\terr := c.Render(http.StatusOK, \"hello\", \"Jon Snow\")\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\tassert.Equal(t, \"Hello, Jon Snow!\", rec.Body.String())\n\t}\n}\n\nfunc TestContextRenderTemplateError(t *testing.T) {\n\t// we test that when template rendering fails, no response is sent to the client yet, so the global error handler can decide what to do\n\te := New()\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(userJSON))\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\ttmpl := &Template{\n\t\ttemplates: template.Must(template.New(\"hello\").Parse(\"Hello, {{.}}!\")),\n\t}\n\tc.Echo().Renderer = tmpl\n\terr := c.Render(http.StatusOK, \"not_existing\", \"Jon Snow\")\n\n\tassert.EqualError(t, err, `template: no template \"not_existing\" associated with template \"hello\"`)\n\tassert.Equal(t, http.StatusOK, rec.Code) // status code must not be sent to the client\n\tassert.Empty(t, rec.Body.String())       // body must not be sent to the client\n}\n\nfunc TestContextRenderErrorsOnNoRenderer(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(userJSON))\n\trec := httptest.NewRecorder()\n\n\tc := e.NewContext(req, rec)\n\n\tc.Echo().Renderer = nil\n\tassert.Error(t, c.Render(http.StatusOK, \"hello\", \"Jon Snow\"))\n}\n\nfunc TestContextStream(t *testing.T) {\n\te := New()\n\trec := httptest.NewRecorder()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\tc := e.NewContext(req, rec)\n\n\tr, w := io.Pipe()\n\tgo func() {\n\t\tdefer w.Close()\n\t\tfor i := 0; i < 3; i++ {\n\t\t\tfmt.Fprintf(w, \"data: index %v\\n\\n\", i)\n\t\t\ttime.Sleep(5 * time.Millisecond)\n\t\t}\n\t}()\n\n\terr := c.Stream(http.StatusOK, \"text/event-stream\", r)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\tassert.Equal(t, \"text/event-stream\", rec.Header().Get(HeaderContentType))\n\t\tassert.Equal(t, \"data: index 0\\n\\ndata: index 1\\n\\ndata: index 2\\n\\n\", rec.Body.String())\n\t}\n}\n\nfunc TestContextHTML(t *testing.T) {\n\trec := httptest.NewRecorder()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\tc := NewContext(req, rec)\n\n\terr := c.HTML(http.StatusOK, \"Hi, Jon Snow\")\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\tassert.Equal(t, MIMETextHTMLCharsetUTF8, rec.Header().Get(HeaderContentType))\n\t\tassert.Equal(t, \"Hi, Jon Snow\", rec.Body.String())\n\t}\n}\n\nfunc TestContextHTMLBlob(t *testing.T) {\n\trec := httptest.NewRecorder()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\tc := NewContext(req, rec)\n\n\terr := c.HTMLBlob(http.StatusOK, []byte(\"Hi, Jon Snow\"))\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\tassert.Equal(t, MIMETextHTMLCharsetUTF8, rec.Header().Get(HeaderContentType))\n\t\tassert.Equal(t, \"Hi, Jon Snow\", rec.Body.String())\n\t}\n}\n\nfunc TestContextJSON(t *testing.T) {\n\te := New()\n\trec := httptest.NewRecorder()\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(userJSON))\n\tc := e.NewContext(req, rec)\n\n\terr := c.JSON(http.StatusOK, user{ID: 1, Name: \"Jon Snow\"})\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\tassert.Equal(t, MIMEApplicationJSON, rec.Header().Get(HeaderContentType))\n\t\tassert.Equal(t, userJSON+\"\\n\", rec.Body.String())\n\t}\n}\n\nfunc TestContextJSONErrorsOut(t *testing.T) {\n\te := New()\n\trec := httptest.NewRecorder()\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(userJSON))\n\tc := e.NewContext(req, rec)\n\n\terr := c.JSON(http.StatusOK, make(chan bool))\n\tassert.EqualError(t, err, \"json: unsupported type: chan bool\")\n\n\tassert.Equal(t, http.StatusOK, rec.Code) // status code must not be sent to the client\n\tassert.Empty(t, rec.Body.String())       // body must not be sent to the client\n}\n\nfunc TestContextJSONWithNotEchoResponse(t *testing.T) {\n\te := New()\n\trec := httptest.NewRecorder()\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(userJSON))\n\tc := e.NewContext(req, rec)\n\n\tc.SetResponse(rec)\n\n\terr := c.JSON(http.StatusCreated, map[string]float64{\"foo\": math.NaN()})\n\tassert.EqualError(t, err, \"json: unsupported value: NaN\")\n\n\tassert.Equal(t, http.StatusOK, rec.Code) // status code must not be sent to the client\n\tassert.Empty(t, rec.Body.String())       // body must not be sent to the client\n}\n\nfunc TestContextJSONPretty(t *testing.T) {\n\te := New()\n\trec := httptest.NewRecorder()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\tc := e.NewContext(req, rec)\n\n\terr := c.JSONPretty(http.StatusOK, user{ID: 1, Name: \"Jon Snow\"}, \"  \")\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\tassert.Equal(t, MIMEApplicationJSON, rec.Header().Get(HeaderContentType))\n\t\tassert.Equal(t, userJSONPretty+\"\\n\", rec.Body.String())\n\t}\n}\n\nfunc TestContextJSONWithEmptyIntent(t *testing.T) {\n\te := New()\n\trec := httptest.NewRecorder()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\tc := e.NewContext(req, rec)\n\n\tu := user{ID: 1, Name: \"Jon Snow\"}\n\temptyIndent := \"\"\n\tbuf := new(bytes.Buffer)\n\n\tenc := json.NewEncoder(buf)\n\tenc.SetIndent(emptyIndent, emptyIndent)\n\t_ = enc.Encode(u)\n\terr := c.JSONPretty(http.StatusOK, user{ID: 1, Name: \"Jon Snow\"}, emptyIndent)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\tassert.Equal(t, MIMEApplicationJSON, rec.Header().Get(HeaderContentType))\n\t\tassert.Equal(t, buf.String(), rec.Body.String())\n\t}\n}\n\nfunc TestContextJSONP(t *testing.T) {\n\te := New()\n\trec := httptest.NewRecorder()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\tc := e.NewContext(req, rec)\n\n\tcallback := \"callback\"\n\terr := c.JSONP(http.StatusOK, callback, user{ID: 1, Name: \"Jon Snow\"})\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\tassert.Equal(t, MIMEApplicationJavaScriptCharsetUTF8, rec.Header().Get(HeaderContentType))\n\t\tassert.Equal(t, callback+\"(\"+userJSON+\"\\n);\", rec.Body.String())\n\t}\n}\n\nfunc TestContextJSONBlob(t *testing.T) {\n\te := New()\n\trec := httptest.NewRecorder()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\tc := e.NewContext(req, rec)\n\n\tdata, err := json.Marshal(user{ID: 1, Name: \"Jon Snow\"})\n\tassert.NoError(t, err)\n\terr = c.JSONBlob(http.StatusOK, data)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\tassert.Equal(t, MIMEApplicationJSON, rec.Header().Get(HeaderContentType))\n\t\tassert.Equal(t, userJSON, rec.Body.String())\n\t}\n}\n\nfunc TestContextJSONPBlob(t *testing.T) {\n\te := New()\n\trec := httptest.NewRecorder()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\tc := e.NewContext(req, rec)\n\n\tcallback := \"callback\"\n\tdata, err := json.Marshal(user{ID: 1, Name: \"Jon Snow\"})\n\tassert.NoError(t, err)\n\terr = c.JSONPBlob(http.StatusOK, callback, data)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\tassert.Equal(t, MIMEApplicationJavaScriptCharsetUTF8, rec.Header().Get(HeaderContentType))\n\t\tassert.Equal(t, callback+\"(\"+userJSON+\");\", rec.Body.String())\n\t}\n}\n\nfunc TestContextXML(t *testing.T) {\n\te := New()\n\trec := httptest.NewRecorder()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\tc := e.NewContext(req, rec)\n\n\terr := c.XML(http.StatusOK, user{ID: 1, Name: \"Jon Snow\"})\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\tassert.Equal(t, MIMEApplicationXMLCharsetUTF8, rec.Header().Get(HeaderContentType))\n\t\tassert.Equal(t, xml.Header+userXML, rec.Body.String())\n\t}\n}\n\nfunc TestContextXMLPretty(t *testing.T) {\n\te := New()\n\trec := httptest.NewRecorder()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\tc := e.NewContext(req, rec)\n\n\terr := c.XMLPretty(http.StatusOK, user{ID: 1, Name: \"Jon Snow\"}, \"  \")\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\tassert.Equal(t, MIMEApplicationXMLCharsetUTF8, rec.Header().Get(HeaderContentType))\n\t\tassert.Equal(t, xml.Header+userXMLPretty, rec.Body.String())\n\t}\n}\n\nfunc TestContextXMLBlob(t *testing.T) {\n\te := New()\n\trec := httptest.NewRecorder()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\tc := e.NewContext(req, rec)\n\n\tdata, err := xml.Marshal(user{ID: 1, Name: \"Jon Snow\"})\n\tassert.NoError(t, err)\n\terr = c.XMLBlob(http.StatusOK, data)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\tassert.Equal(t, MIMEApplicationXMLCharsetUTF8, rec.Header().Get(HeaderContentType))\n\t\tassert.Equal(t, xml.Header+userXML, rec.Body.String())\n\t}\n}\n\nfunc TestContextXMLWithEmptyIntent(t *testing.T) {\n\te := New()\n\trec := httptest.NewRecorder()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\tc := e.NewContext(req, rec)\n\n\tu := user{ID: 1, Name: \"Jon Snow\"}\n\temptyIndent := \"\"\n\tbuf := new(bytes.Buffer)\n\n\tenc := xml.NewEncoder(buf)\n\tenc.Indent(emptyIndent, emptyIndent)\n\t_ = enc.Encode(u)\n\terr := c.XMLPretty(http.StatusOK, user{ID: 1, Name: \"Jon Snow\"}, emptyIndent)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\tassert.Equal(t, MIMEApplicationXMLCharsetUTF8, rec.Header().Get(HeaderContentType))\n\t\tassert.Equal(t, xml.Header+buf.String(), rec.Body.String())\n\t}\n}\n\nfunc TestContext_JSON_CommitsCustomResponseCode(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\terr := c.JSON(http.StatusCreated, user{ID: 1, Name: \"Jon Snow\"})\n\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, http.StatusCreated, rec.Code)\n\t\tassert.Equal(t, MIMEApplicationJSON, rec.Header().Get(HeaderContentType))\n\t\tassert.Equal(t, userJSON+\"\\n\", rec.Body.String())\n\t}\n}\n\nfunc TestContextAttachment(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname         string\n\t\twhenName     string\n\t\texpectHeader string\n\t}{\n\t\t{\n\t\t\tname:         \"ok\",\n\t\t\twhenName:     \"walle.png\",\n\t\t\texpectHeader: `attachment; filename=\"walle.png\"`,\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, escape quotes in malicious filename\",\n\t\t\twhenName:     `malicious.sh\"; \\\"; dummy=.txt`,\n\t\t\texpectHeader: `attachment; filename=\"malicious.sh\\\"; \\\\\\\"; dummy=.txt\"`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\t\t\trec := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\terr := c.Attachment(\"_fixture/images/walle.png\", tc.whenName)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tassert.Equal(t, tc.expectHeader, rec.Header().Get(HeaderContentDisposition))\n\n\t\t\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\t\t\tassert.Equal(t, 219885, rec.Body.Len())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestContextInline(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname         string\n\t\twhenName     string\n\t\texpectHeader string\n\t}{\n\t\t{\n\t\t\tname:         \"ok\",\n\t\t\twhenName:     \"walle.png\",\n\t\t\texpectHeader: `inline; filename=\"walle.png\"`,\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, escape quotes in malicious filename\",\n\t\t\twhenName:     `malicious.sh\"; \\\"; dummy=.txt`,\n\t\t\texpectHeader: `inline; filename=\"malicious.sh\\\"; \\\\\\\"; dummy=.txt\"`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\t\t\trec := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\terr := c.Inline(\"_fixture/images/walle.png\", tc.whenName)\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tassert.Equal(t, tc.expectHeader, rec.Header().Get(HeaderContentDisposition))\n\n\t\t\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\t\t\tassert.Equal(t, 219885, rec.Body.Len())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestContextNoContent(t *testing.T) {\n\te := New()\n\trec := httptest.NewRecorder()\n\treq := httptest.NewRequest(http.MethodGet, \"/?pretty\", nil)\n\tc := e.NewContext(req, rec)\n\n\tc.NoContent(http.StatusOK)\n\tassert.Equal(t, http.StatusOK, rec.Code)\n}\n\nfunc TestContextCookie(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\ttheme := \"theme=light\"\n\tuser := \"user=Jon Snow\"\n\treq.Header.Add(HeaderCookie, theme)\n\treq.Header.Add(HeaderCookie, user)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\t// Read single\n\tcookie, err := c.Cookie(\"theme\")\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, \"theme\", cookie.Name)\n\t\tassert.Equal(t, \"light\", cookie.Value)\n\t}\n\n\t// Read multiple\n\tfor _, cookie := range c.Cookies() {\n\t\tswitch cookie.Name {\n\t\tcase \"theme\":\n\t\t\tassert.Equal(t, \"light\", cookie.Value)\n\t\tcase \"user\":\n\t\t\tassert.Equal(t, \"Jon Snow\", cookie.Value)\n\t\t}\n\t}\n\n\t// Write\n\tcookie = &http.Cookie{\n\t\tName:     \"SSID\",\n\t\tValue:    \"Ap4PGTEq\",\n\t\tDomain:   \"labstack.com\",\n\t\tPath:     \"/\",\n\t\tExpires:  time.Now(),\n\t\tSecure:   true,\n\t\tHttpOnly: true,\n\t}\n\tc.SetCookie(cookie)\n\tassert.Contains(t, rec.Header().Get(HeaderSetCookie), \"SSID\")\n\tassert.Contains(t, rec.Header().Get(HeaderSetCookie), \"Ap4PGTEq\")\n\tassert.Contains(t, rec.Header().Get(HeaderSetCookie), \"labstack.com\")\n\tassert.Contains(t, rec.Header().Get(HeaderSetCookie), \"Secure\")\n\tassert.Contains(t, rec.Header().Get(HeaderSetCookie), \"HttpOnly\")\n}\n\nfunc TestContext_PathValues(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname   string\n\t\tgiven  PathValues\n\t\texpect PathValues\n\t}{\n\t\t{\n\t\t\tname: \"param exists\",\n\t\t\tgiven: PathValues{\n\t\t\t\t{Name: \"uid\", Value: \"101\"},\n\t\t\t\t{Name: \"fid\", Value: \"501\"},\n\t\t\t},\n\t\t\texpect: PathValues{\n\t\t\t\t{Name: \"uid\", Value: \"101\"},\n\t\t\t\t{Name: \"fid\", Value: \"501\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"params is empty\",\n\t\t\tgiven:  PathValues{},\n\t\t\texpect: PathValues{},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\t\tc := e.NewContext(req, nil)\n\n\t\t\tc.SetPathValues(tc.given)\n\n\t\t\tassert.EqualValues(t, tc.expect, c.PathValues())\n\t\t})\n\t}\n}\n\nfunc TestContext_PathParam(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname          string\n\t\tgiven         PathValues\n\t\twhenParamName string\n\t\texpect        string\n\t}{\n\t\t{\n\t\t\tname: \"param exists\",\n\t\t\tgiven: PathValues{\n\t\t\t\t{Name: \"uid\", Value: \"101\"},\n\t\t\t\t{Name: \"fid\", Value: \"501\"},\n\t\t\t},\n\t\t\twhenParamName: \"uid\",\n\t\t\texpect:        \"101\",\n\t\t},\n\t\t{\n\t\t\tname: \"multiple same param values exists - return first\",\n\t\t\tgiven: PathValues{\n\t\t\t\t{Name: \"uid\", Value: \"101\"},\n\t\t\t\t{Name: \"uid\", Value: \"202\"},\n\t\t\t\t{Name: \"fid\", Value: \"501\"},\n\t\t\t},\n\t\t\twhenParamName: \"uid\",\n\t\t\texpect:        \"101\",\n\t\t},\n\t\t{\n\t\t\tname: \"param does not exists\",\n\t\t\tgiven: PathValues{\n\t\t\t\t{Name: \"uid\", Value: \"101\"},\n\t\t\t},\n\t\t\twhenParamName: \"nope\",\n\t\t\texpect:        \"\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\t\tc := e.NewContext(req, nil)\n\n\t\t\tc.SetPathValues(tc.given)\n\n\t\t\tassert.EqualValues(t, tc.expect, c.Param(tc.whenParamName))\n\t\t})\n\t}\n}\n\nfunc TestContext_PathParamDefault(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname             string\n\t\tgiven            PathValues\n\t\twhenParamName    string\n\t\twhenDefaultValue string\n\t\texpect           string\n\t}{\n\t\t{\n\t\t\tname: \"param exists\",\n\t\t\tgiven: PathValues{\n\t\t\t\t{Name: \"uid\", Value: \"101\"},\n\t\t\t\t{Name: \"fid\", Value: \"501\"},\n\t\t\t},\n\t\t\twhenParamName:    \"uid\",\n\t\t\twhenDefaultValue: \"999\",\n\t\t\texpect:           \"101\",\n\t\t},\n\t\t{\n\t\t\tname: \"param exists and is empty\",\n\t\t\tgiven: PathValues{\n\t\t\t\t{Name: \"uid\", Value: \"\"},\n\t\t\t\t{Name: \"fid\", Value: \"501\"},\n\t\t\t},\n\t\t\twhenParamName:    \"uid\",\n\t\t\twhenDefaultValue: \"999\",\n\t\t\texpect:           \"\", // <-- this is different from QueryParamOr behaviour\n\t\t},\n\t\t{\n\t\t\tname: \"param does not exists\",\n\t\t\tgiven: PathValues{\n\t\t\t\t{Name: \"uid\", Value: \"101\"},\n\t\t\t},\n\t\t\twhenParamName:    \"nope\",\n\t\t\twhenDefaultValue: \"999\",\n\t\t\texpect:           \"999\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\t\tc := e.NewContext(req, nil)\n\n\t\t\tc.SetPathValues(tc.given)\n\n\t\t\tassert.EqualValues(t, tc.expect, c.ParamOr(tc.whenParamName, tc.whenDefaultValue))\n\t\t})\n\t}\n}\n\nfunc TestContextGetAndSetPathValuesMutability(t *testing.T) {\n\tt.Run(\"c.PathValues() does not return copy and modifying raw slice mutates value in context\", func(t *testing.T) {\n\t\te := New()\n\t\te.contextPathParamAllocSize.Store(1)\n\n\t\treq := httptest.NewRequest(http.MethodGet, \"/:foo\", nil)\n\t\tc := e.NewContext(req, nil)\n\n\t\tparams := PathValues{{Name: \"foo\", Value: \"101\"}}\n\t\tc.SetPathValues(params)\n\n\t\t// round-trip param values with modification\n\t\tparamVals := c.PathValues()\n\t\tassert.Equal(t, params, c.PathValues())\n\n\t\t// PathValues() does not return copy and modifying raw slice mutates value in context\n\t\tparamVals[0] = PathValue{Name: \"xxx\", Value: \"yyy\"}\n\t\tassert.Equal(t, PathValues{PathValue{Name: \"xxx\", Value: \"yyy\"}}, c.PathValues())\n\t})\n\n\tt.Run(\"calling SetPathValues with bigger size changes capacity in context\", func(t *testing.T) {\n\t\te := New()\n\t\te.contextPathParamAllocSize.Store(1)\n\n\t\treq := httptest.NewRequest(http.MethodGet, \"/:foo\", nil)\n\t\tc := e.NewContext(req, nil)\n\t\t// increase path param capacity in context\n\t\tpathValues := PathValues{\n\t\t\t{Name: \"aaa\", Value: \"bbb\"},\n\t\t\t{Name: \"ccc\", Value: \"ddd\"},\n\t\t}\n\t\tc.SetPathValues(pathValues)\n\t\tassert.Equal(t, pathValues, c.PathValues())\n\n\t\t// shouldn't explode during Reset() afterwards!\n\t\tassert.NotPanics(t, func() {\n\t\t\tc.Reset(nil, nil)\n\t\t})\n\t\tassert.Equal(t, PathValues{}, c.PathValues())\n\t\tassert.Len(t, *c.pathValues, 0)\n\t\tassert.Equal(t, 2, cap(*c.pathValues))\n\t})\n\n\tt.Run(\"calling SetPathValues with smaller size slice does not change capacity in context\", func(t *testing.T) {\n\t\te := New()\n\n\t\treq := httptest.NewRequest(http.MethodGet, \"/:foo\", nil)\n\t\tc := e.NewContext(req, nil)\n\t\tc.pathValues = &PathValues{\n\t\t\t{Name: \"aaa\", Value: \"bbb\"},\n\t\t\t{Name: \"ccc\", Value: \"ddd\"},\n\t\t}\n\n\t\tpathValues := PathValues{\n\t\t\t{Name: \"aaa\", Value: \"bbb\"},\n\t\t}\n\t\t// given pathValues slice is smaller. this should not decrease c.pathValues capacity\n\t\tc.SetPathValues(pathValues)\n\t\tassert.Equal(t, pathValues, c.PathValues())\n\n\t\t// shouldn't explode during Reset() afterwards!\n\t\tassert.NotPanics(t, func() {\n\t\t\tc.Reset(nil, nil)\n\t\t})\n\t\tassert.Equal(t, PathValues{}, c.PathValues())\n\t\tassert.Len(t, *c.pathValues, 0)\n\t\tassert.Equal(t, 2, cap(*c.pathValues))\n\t})\n\n}\n\n// Issue #1655\nfunc TestContext_SetParamNamesShouldNotModifyPathValuesCapacity(t *testing.T) {\n\te := New()\n\tc := e.NewContext(nil, nil)\n\n\tassert.Equal(t, int32(0), e.contextPathParamAllocSize.Load())\n\texpectedTwoParams := PathValues{\n\t\t{Name: \"1\", Value: \"one\"},\n\t\t{Name: \"2\", Value: \"two\"},\n\t}\n\tc.SetPathValues(expectedTwoParams)\n\tassert.Equal(t, int32(0), e.contextPathParamAllocSize.Load())\n\tassert.Equal(t, expectedTwoParams, c.PathValues())\n\n\texpectedThreeParams := PathValues{\n\t\t{Name: \"1\", Value: \"one\"},\n\t\t{Name: \"2\", Value: \"two\"},\n\t\t{Name: \"3\", Value: \"three\"},\n\t}\n\tc.SetPathValues(expectedThreeParams)\n\tassert.Equal(t, int32(0), e.contextPathParamAllocSize.Load())\n\tassert.Equal(t, expectedThreeParams, c.PathValues())\n}\n\nfunc TestContextFormValue(t *testing.T) {\n\tf := make(url.Values)\n\tf.Set(\"name\", \"Jon Snow\")\n\tf.Set(\"email\", \"jon@labstack.com\")\n\n\te := New()\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(f.Encode()))\n\treq.Header.Add(HeaderContentType, MIMEApplicationForm)\n\tc := e.NewContext(req, nil)\n\n\t// FormValue\n\tassert.Equal(t, \"Jon Snow\", c.FormValue(\"name\"))\n\tassert.Equal(t, \"jon@labstack.com\", c.FormValue(\"email\"))\n\n\t// FormValueOr\n\tassert.Equal(t, \"Jon Snow\", c.FormValueOr(\"name\", \"nope\"))\n\tassert.Equal(t, \"default\", c.FormValueOr(\"missing\", \"default\"))\n\n\t// FormValues\n\tvalues, err := c.FormValues()\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, url.Values{\n\t\t\t\"name\":  []string{\"Jon Snow\"},\n\t\t\t\"email\": []string{\"jon@labstack.com\"},\n\t\t}, values)\n\t}\n\n\t// Multipart FormParams error\n\treq = httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(f.Encode()))\n\treq.Header.Add(HeaderContentType, MIMEMultipartForm)\n\tc = e.NewContext(req, nil)\n\tvalues, err = c.FormValues()\n\tassert.Nil(t, values)\n\tassert.Error(t, err)\n}\n\nfunc TestContext_QueryParams(t *testing.T) {\n\tvar testCases = []struct {\n\t\texpect   url.Values\n\t\tname     string\n\t\tgivenURL string\n\t}{\n\t\t{\n\t\t\tname:     \"multiple values in url\",\n\t\t\tgivenURL: \"/?test=1&test=2&email=jon%40labstack.com\",\n\t\t\texpect: url.Values{\n\t\t\t\t\"test\":  []string{\"1\", \"2\"},\n\t\t\t\t\"email\": []string{\"jon@labstack.com\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"single value  in url\",\n\t\t\tgivenURL: \"/?nope=1\",\n\t\t\texpect: url.Values{\n\t\t\t\t\"nope\": []string{\"1\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"no query params in url\",\n\t\t\tgivenURL: \"/?\",\n\t\t\texpect:   url.Values{},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.givenURL, nil)\n\t\t\te := New()\n\t\t\tc := e.NewContext(req, nil)\n\n\t\t\tassert.Equal(t, tc.expect, c.QueryParams())\n\t\t})\n\t}\n}\n\nfunc TestContext_QueryParam(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname          string\n\t\tgivenURL      string\n\t\twhenParamName string\n\t\texpect        string\n\t}{\n\t\t{\n\t\t\tname:          \"value exists in url\",\n\t\t\tgivenURL:      \"/?test=1\",\n\t\t\twhenParamName: \"test\",\n\t\t\texpect:        \"1\",\n\t\t},\n\t\t{\n\t\t\tname:          \"multiple values exists in url\",\n\t\t\tgivenURL:      \"/?test=9&test=8\",\n\t\t\twhenParamName: \"test\",\n\t\t\texpect:        \"9\", // <-- first value in returned\n\t\t},\n\t\t{\n\t\t\tname:          \"value does not exists in url\",\n\t\t\tgivenURL:      \"/?nope=1\",\n\t\t\twhenParamName: \"test\",\n\t\t\texpect:        \"\",\n\t\t},\n\t\t{\n\t\t\tname:          \"value is empty in url\",\n\t\t\tgivenURL:      \"/?test=\",\n\t\t\twhenParamName: \"test\",\n\t\t\texpect:        \"\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.givenURL, nil)\n\t\t\te := New()\n\t\t\tc := e.NewContext(req, nil)\n\n\t\t\tassert.Equal(t, tc.expect, c.QueryParam(tc.whenParamName))\n\t\t})\n\t}\n}\n\nfunc TestContext_QueryParamDefault(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname             string\n\t\tgivenURL         string\n\t\twhenParamName    string\n\t\twhenDefaultValue string\n\t\texpect           string\n\t}{\n\t\t{\n\t\t\tname:             \"value exists in url\",\n\t\t\tgivenURL:         \"/?test=1\",\n\t\t\twhenParamName:    \"test\",\n\t\t\twhenDefaultValue: \"999\",\n\t\t\texpect:           \"1\",\n\t\t},\n\t\t{\n\t\t\tname:             \"value does not exists in url\",\n\t\t\tgivenURL:         \"/?nope=1\",\n\t\t\twhenParamName:    \"test\",\n\t\t\twhenDefaultValue: \"999\",\n\t\t\texpect:           \"999\",\n\t\t},\n\t\t{\n\t\t\tname:             \"value is empty in url\",\n\t\t\tgivenURL:         \"/?test=\",\n\t\t\twhenParamName:    \"test\",\n\t\t\twhenDefaultValue: \"999\",\n\t\t\texpect:           \"999\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.givenURL, nil)\n\t\t\te := New()\n\t\t\tc := e.NewContext(req, nil)\n\n\t\t\tassert.Equal(t, tc.expect, c.QueryParamOr(tc.whenParamName, tc.whenDefaultValue))\n\t\t})\n\t}\n}\n\nfunc TestContextFormFile(t *testing.T) {\n\te := New()\n\tbuf := new(bytes.Buffer)\n\tmr := multipart.NewWriter(buf)\n\tw, err := mr.CreateFormFile(\"file\", \"test\")\n\tif assert.NoError(t, err) {\n\t\tw.Write([]byte(\"test\"))\n\t}\n\tmr.Close()\n\treq := httptest.NewRequest(http.MethodPost, \"/\", buf)\n\treq.Header.Set(HeaderContentType, mr.FormDataContentType())\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tf, err := c.FormFile(\"file\")\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, \"test\", f.Filename)\n\t}\n}\n\nfunc TestContextMultipartForm(t *testing.T) {\n\te := New()\n\tbuf := new(bytes.Buffer)\n\tmw := multipart.NewWriter(buf)\n\tmw.WriteField(\"name\", \"Jon Snow\")\n\tfileContent := \"This is a test file\"\n\tw, err := mw.CreateFormFile(\"file\", \"test.txt\")\n\tif assert.NoError(t, err) {\n\t\tw.Write([]byte(fileContent))\n\t}\n\tmw.Close()\n\treq := httptest.NewRequest(http.MethodPost, \"/\", buf)\n\treq.Header.Set(HeaderContentType, mw.FormDataContentType())\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tf, err := c.MultipartForm()\n\tif assert.NoError(t, err) {\n\t\tassert.NotNil(t, f)\n\n\t\tfiles := f.File[\"file\"]\n\t\tif assert.Len(t, files, 1) {\n\t\t\tfile := files[0]\n\t\t\tassert.Equal(t, \"test.txt\", file.Filename)\n\t\t\tassert.Equal(t, int64(len(fileContent)), file.Size)\n\t\t}\n\t}\n}\n\nfunc TestContextRedirect(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tassert.Equal(t, nil, c.Redirect(http.StatusMovedPermanently, \"http://labstack.github.io/echo\"))\n\tassert.Equal(t, http.StatusMovedPermanently, rec.Code)\n\tassert.Equal(t, \"http://labstack.github.io/echo\", rec.Header().Get(HeaderLocation))\n\tassert.Error(t, c.Redirect(310, \"http://labstack.github.io/echo\"))\n}\n\nfunc TestContextGet(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname    string\n\t\tgiven   any\n\t\twhenKey string\n\t\texpect  any\n\t}{\n\t\t{\n\t\t\tname:    \"ok, value exist\",\n\t\t\tgiven:   \"Jon Snow\",\n\t\t\twhenKey: \"key\",\n\t\t\texpect:  \"Jon Snow\",\n\t\t},\n\t\t{\n\t\t\tname:    \"ok, value does not exist\",\n\t\t\tgiven:   \"Jon Snow\",\n\t\t\twhenKey: \"nope\",\n\t\t\texpect:  nil,\n\t\t},\n\t\t{\n\t\t\tname:    \"ok, value is nil value\",\n\t\t\tgiven:   []byte(nil),\n\t\t\twhenKey: \"key\",\n\t\t\texpect:  []byte(nil),\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar c = new(Context)\n\t\t\tc.Set(\"key\", tc.given)\n\n\t\t\tv := c.Get(tc.whenKey)\n\t\t\tassert.Equal(t, tc.expect, v)\n\t\t})\n\t}\n}\n\nfunc BenchmarkContext_Store(b *testing.B) {\n\te := &Echo{}\n\n\tc := &Context{\n\t\techo: e,\n\t}\n\n\tfor n := 0; n < b.N; n++ {\n\t\tc.Set(\"name\", \"Jon Snow\")\n\t\tif c.Get(\"name\") != \"Jon Snow\" {\n\t\t\tb.Fail()\n\t\t}\n\t}\n}\n\ntype validator struct{}\n\nfunc (*validator) Validate(i any) error {\n\treturn nil\n}\n\nfunc TestContext_Validate(t *testing.T) {\n\te := New()\n\tc := e.NewContext(nil, nil)\n\n\tassert.Error(t, c.Validate(struct{}{}))\n\n\te.Validator = &validator{}\n\tassert.NoError(t, c.Validate(struct{}{}))\n}\n\nfunc TestContext_QueryString(t *testing.T) {\n\te := New()\n\n\tqueryString := \"query=string&var=val\"\n\n\treq := httptest.NewRequest(http.MethodGet, \"/?\"+queryString, nil)\n\tc := e.NewContext(req, nil)\n\n\tassert.Equal(t, queryString, c.QueryString())\n}\n\nfunc TestContext_Request(t *testing.T) {\n\tvar c = new(Context)\n\n\tassert.Nil(t, c.Request())\n\n\treq := httptest.NewRequest(http.MethodGet, \"/path\", nil)\n\tc.SetRequest(req)\n\n\tassert.Equal(t, req, c.Request())\n}\n\nfunc TestContext_Scheme(t *testing.T) {\n\ttests := []struct {\n\t\tc *Context\n\t\ts string\n\t}{\n\t\t{\n\t\t\t&Context{\n\t\t\t\trequest: &http.Request{\n\t\t\t\t\tTLS: &tls.ConnectionState{},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"https\",\n\t\t},\n\t\t{\n\t\t\t&Context{\n\t\t\t\trequest: &http.Request{\n\t\t\t\t\tHeader: http.Header{HeaderXForwardedProto: []string{\"https\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"https\",\n\t\t},\n\t\t{\n\t\t\t&Context{\n\t\t\t\trequest: &http.Request{\n\t\t\t\t\tHeader: http.Header{HeaderXForwardedProtocol: []string{\"http\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"http\",\n\t\t},\n\t\t{\n\t\t\t&Context{\n\t\t\t\trequest: &http.Request{\n\t\t\t\t\tHeader: http.Header{HeaderXForwardedSsl: []string{\"on\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"https\",\n\t\t},\n\t\t{\n\t\t\t&Context{\n\t\t\t\trequest: &http.Request{\n\t\t\t\t\tHeader: http.Header{HeaderXUrlScheme: []string{\"https\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"https\",\n\t\t},\n\t\t{\n\t\t\t&Context{\n\t\t\t\trequest: &http.Request{},\n\t\t\t},\n\t\t\t\"http\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tassert.Equal(t, tt.s, tt.c.Scheme())\n\t}\n}\n\nfunc TestContext_IsWebSocket(t *testing.T) {\n\ttests := []struct {\n\t\tc  *Context\n\t\tws assert.BoolAssertionFunc\n\t}{\n\t\t{\n\t\t\t&Context{\n\t\t\t\trequest: &http.Request{\n\t\t\t\t\tHeader: http.Header{\n\t\t\t\t\t\tHeaderUpgrade:    []string{\"websocket\"},\n\t\t\t\t\t\tHeaderConnection: []string{\"upgrade\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tassert.True,\n\t\t},\n\t\t{\n\t\t\t&Context{\n\t\t\t\trequest: &http.Request{\n\t\t\t\t\tHeader: http.Header{\n\t\t\t\t\t\tHeaderUpgrade:    []string{\"Websocket\"},\n\t\t\t\t\t\tHeaderConnection: []string{\"Upgrade\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tassert.True,\n\t\t},\n\t\t{\n\t\t\t&Context{\n\t\t\t\trequest: &http.Request{},\n\t\t\t},\n\t\t\tassert.False,\n\t\t},\n\t\t{\n\t\t\t&Context{\n\t\t\t\trequest: &http.Request{\n\t\t\t\t\tHeader: http.Header{HeaderUpgrade: []string{\"other\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tassert.False,\n\t\t},\n\t\t{\n\t\t\t&Context{\n\t\t\t\trequest: &http.Request{\n\t\t\t\t\tHeader: http.Header{\n\t\t\t\t\t\tHeaderUpgrade:    []string{\"websocket\"},\n\t\t\t\t\t\tHeaderConnection: []string{\"close\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tassert.False,\n\t\t},\n\t}\n\n\tfor i, tt := range tests {\n\t\tt.Run(fmt.Sprintf(\"test %d\", i+1), func(t *testing.T) {\n\t\t\ttt.ws(t, tt.c.IsWebSocket())\n\t\t})\n\t}\n}\n\nfunc TestContext_Bind(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(userJSON))\n\tc := e.NewContext(req, nil)\n\tu := new(user)\n\n\treq.Header.Add(HeaderContentType, MIMEApplicationJSON)\n\terr := c.Bind(u)\n\tassert.NoError(t, err)\n\tassert.Equal(t, &user{ID: 1, Name: \"Jon Snow\"}, u)\n}\n\nfunc TestContext_RealIP(t *testing.T) {\n\ttests := []struct {\n\t\tc *Context\n\t\ts string\n\t}{\n\t\t{\n\t\t\t&Context{\n\t\t\t\trequest: &http.Request{\n\t\t\t\t\tHeader: http.Header{HeaderXForwardedFor: []string{\"127.0.0.1, 127.0.1.1, \"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"127.0.0.1\",\n\t\t},\n\t\t{\n\t\t\t&Context{\n\t\t\t\trequest: &http.Request{\n\t\t\t\t\tHeader: http.Header{HeaderXForwardedFor: []string{\"127.0.0.1,127.0.1.1\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"127.0.0.1\",\n\t\t},\n\t\t{\n\t\t\t&Context{\n\t\t\t\trequest: &http.Request{\n\t\t\t\t\tHeader: http.Header{HeaderXForwardedFor: []string{\"127.0.0.1\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"127.0.0.1\",\n\t\t},\n\t\t{\n\t\t\t&Context{\n\t\t\t\trequest: &http.Request{\n\t\t\t\t\tHeader: http.Header{HeaderXForwardedFor: []string{\"[2001:db8:85a3:8d3:1319:8a2e:370:7348], 2001:db8::1, \"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"2001:db8:85a3:8d3:1319:8a2e:370:7348\",\n\t\t},\n\t\t{\n\t\t\t&Context{\n\t\t\t\trequest: &http.Request{\n\t\t\t\t\tHeader: http.Header{HeaderXForwardedFor: []string{\"[2001:db8:85a3:8d3:1319:8a2e:370:7348],[2001:db8::1]\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"2001:db8:85a3:8d3:1319:8a2e:370:7348\",\n\t\t},\n\t\t{\n\t\t\t&Context{\n\t\t\t\trequest: &http.Request{\n\t\t\t\t\tHeader: http.Header{HeaderXForwardedFor: []string{\"2001:db8:85a3:8d3:1319:8a2e:370:7348\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"2001:db8:85a3:8d3:1319:8a2e:370:7348\",\n\t\t},\n\t\t{\n\t\t\t&Context{\n\t\t\t\trequest: &http.Request{\n\t\t\t\t\tHeader: http.Header{\n\t\t\t\t\t\t\"X-Real-Ip\": []string{\"192.168.0.1\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"192.168.0.1\",\n\t\t},\n\t\t{\n\t\t\t&Context{\n\t\t\t\trequest: &http.Request{\n\t\t\t\t\tHeader: http.Header{\n\t\t\t\t\t\t\"X-Real-Ip\": []string{\"[2001:db8::1]\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"2001:db8::1\",\n\t\t},\n\n\t\t{\n\t\t\t&Context{\n\t\t\t\trequest: &http.Request{\n\t\t\t\t\tRemoteAddr: \"89.89.89.89:1654\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"89.89.89.89\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tassert.Equal(t, tt.s, tt.c.RealIP())\n\t}\n}\n\nfunc TestContext_File(t *testing.T) {\n\tvar testCases = []struct {\n\t\twhenFS           fs.FS\n\t\tname             string\n\t\twhenFile         string\n\t\texpectError      string\n\t\texpectStartsWith []byte\n\t\texpectStatus     int\n\t}{\n\t\t{\n\t\t\tname:             \"ok, from default file system\",\n\t\t\twhenFile:         \"_fixture/images/walle.png\",\n\t\t\twhenFS:           nil,\n\t\t\texpectStatus:     http.StatusOK,\n\t\t\texpectStartsWith: []byte{0x89, 0x50, 0x4e},\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, from custom file system\",\n\t\t\twhenFile:         \"walle.png\",\n\t\t\twhenFS:           os.DirFS(\"_fixture/images\"),\n\t\t\texpectStatus:     http.StatusOK,\n\t\t\texpectStartsWith: []byte{0x89, 0x50, 0x4e},\n\t\t},\n\t\t{\n\t\t\tname:             \"nok, not existent file\",\n\t\t\twhenFile:         \"not.png\",\n\t\t\twhenFS:           os.DirFS(\"_fixture/images\"),\n\t\t\texpectStatus:     http.StatusOK,\n\t\t\texpectStartsWith: nil,\n\t\t\texpectError:      \"Not Found\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\t\t\tif tc.whenFS != nil {\n\t\t\t\te.Filesystem = tc.whenFS\n\t\t\t}\n\n\t\t\thandler := func(ec *Context) error {\n\t\t\t\treturn ec.File(tc.whenFile)\n\t\t\t}\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/match.png\", nil)\n\t\t\trec := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\terr := handler(c)\n\n\t\t\tassert.Equal(t, tc.expectStatus, rec.Code)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\n\t\t\tbody := rec.Body.Bytes()\n\t\t\tif len(body) > len(tc.expectStartsWith) {\n\t\t\t\tbody = body[:len(tc.expectStartsWith)]\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectStartsWith, body)\n\t\t})\n\t}\n}\n\nfunc TestContext_FileFS(t *testing.T) {\n\tvar testCases = []struct {\n\t\twhenFS           fs.FS\n\t\tname             string\n\t\twhenFile         string\n\t\texpectError      string\n\t\texpectStartsWith []byte\n\t\texpectStatus     int\n\t}{\n\t\t{\n\t\t\tname:             \"ok\",\n\t\t\twhenFile:         \"walle.png\",\n\t\t\twhenFS:           os.DirFS(\"_fixture/images\"),\n\t\t\texpectStatus:     http.StatusOK,\n\t\t\texpectStartsWith: []byte{0x89, 0x50, 0x4e},\n\t\t},\n\t\t{\n\t\t\tname:             \"nok, not existent file\",\n\t\t\twhenFile:         \"not.png\",\n\t\t\twhenFS:           os.DirFS(\"_fixture/images\"),\n\t\t\texpectStatus:     http.StatusOK,\n\t\t\texpectStartsWith: nil,\n\t\t\texpectError:      \"Not Found\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\n\t\t\thandler := func(ec *Context) error {\n\t\t\t\treturn ec.FileFS(tc.whenFile, tc.whenFS)\n\t\t\t}\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/match.png\", nil)\n\t\t\trec := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\terr := handler(c)\n\n\t\t\tassert.Equal(t, tc.expectStatus, rec.Code)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\n\t\t\tbody := rec.Body.Bytes()\n\t\t\tif len(body) > len(tc.expectStartsWith) {\n\t\t\t\tbody = body[:len(tc.expectStartsWith)]\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectStartsWith, body)\n\t\t})\n\t}\n}\n\nfunc TestLogger(t *testing.T) {\n\te := New()\n\tc := e.NewContext(nil, nil)\n\n\tlog1 := c.Logger()\n\tassert.NotNil(t, log1)\n\tassert.Equal(t, e.Logger, log1)\n\n\tcustomLogger := slog.New(slog.NewTextHandler(os.Stdout, nil))\n\tc.SetLogger(customLogger)\n\tassert.Equal(t, customLogger, c.Logger())\n\n\t// Resetting the context returns the initial Echo logger\n\tc.Reset(nil, nil)\n\tassert.Equal(t, e.Logger, c.Logger())\n}\n\nfunc TestRouteInfo(t *testing.T) {\n\te := New()\n\tc := e.NewContext(nil, nil)\n\n\torgRI := RouteInfo{\n\t\tName:       \"root\",\n\t\tMethod:     http.MethodGet,\n\t\tPath:       \"/*\",\n\t\tParameters: []string{\"*\"},\n\t}\n\tc.route = &orgRI\n\tri := c.RouteInfo()\n\tassert.Equal(t, orgRI, ri)\n\n\t// Test mutability when middlewares start to change things\n\n\t// RouteInfo inside context will not be affected when returned instance is changed\n\texpect := orgRI.Clone()\n\tri.Path = \"changed\"\n\tri.Parameters[0] = \"changed\"\n\tassert.Equal(t, expect, c.RouteInfo())\n\n\t// RouteInfo inside context will not be affected when returned instance is changed\n\texpect = c.RouteInfo()\n\torgRI.Name = \"changed\"\n\tassert.NotEqual(t, expect, c.RouteInfo())\n}\n"
  },
  {
    "path": "echo.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\n/*\nPackage echo implements high performance, minimalist Go web framework.\n\nExample:\n\n\tpackage main\n\n\timport (\n\t\t\"log/slog\"\n\t\t\"net/http\"\n\n\t\t\"github.com/labstack/echo/v5\"\n\t\t\"github.com/labstack/echo/v5/middleware\"\n\t)\n\n\t// Handler\n\tfunc hello(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"Hello, World!\")\n\t}\n\n\tfunc main() {\n\t\t// Echo instance\n\t\te := echo.New()\n\n\t\t// Middleware\n\t\te.Use(middleware.RequestLogger())\n\t\te.Use(middleware.Recover())\n\n\t\t// Routes\n\t\te.GET(\"/\", hello)\n\n\t\t// Start server\n\t\tif err := e.Start(\":8080\"); err != nil {\n\t\t\tslog.Error(\"failed to start server\", \"error\", err)\n\t\t}\n\t}\n\nLearn more at https://echo.labstack.com\n*/\npackage echo\n\nimport (\n\tstdContext \"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"syscall\"\n)\n\n// Echo is the top-level framework instance.\n//\n// Goroutine safety: Do not mutate Echo instance fields after server has started. Accessing these\n// fields from handlers/middlewares and changing field values at the same time leads to data-races.\n// Same rule applies to adding new routes after server has been started - Adding a route is not Goroutine safe action.\ntype Echo struct {\n\tserveHTTPFunc func(http.ResponseWriter, *http.Request)\n\n\tBinder           Binder\n\tFilesystem       fs.FS\n\tRenderer         Renderer\n\tValidator        Validator\n\tJSONSerializer   JSONSerializer\n\tIPExtractor      IPExtractor\n\tOnAddRoute       func(route Route) error\n\tHTTPErrorHandler HTTPErrorHandler\n\tLogger           *slog.Logger\n\n\tcontextPool sync.Pool\n\n\trouter Router\n\n\t// premiddleware are middlewares that are called before routing is done\n\tpremiddleware []MiddlewareFunc\n\n\t// middleware are middlewares that are called after routing is done and before handler is called\n\tmiddleware []MiddlewareFunc\n\n\tcontextPathParamAllocSize atomic.Int32\n\n\t// formParseMaxMemory is passed to Context for multipart form parsing (See http.Request.ParseMultipartForm)\n\tformParseMaxMemory int64\n}\n\n// JSONSerializer is the interface that encodes and decodes JSON to and from interfaces.\ntype JSONSerializer interface {\n\tSerialize(c *Context, target any, indent string) error\n\tDeserialize(c *Context, target any) error\n}\n\n// HTTPErrorHandler is a centralized HTTP error handler.\ntype HTTPErrorHandler func(c *Context, err error)\n\n// HandlerFunc defines a function to serve HTTP requests.\ntype HandlerFunc func(c *Context) error\n\n// MiddlewareFunc defines a function to process middleware.\ntype MiddlewareFunc func(next HandlerFunc) HandlerFunc\n\n// MiddlewareConfigurator defines interface for creating middleware handlers with possibility to return configuration errors instead of panicking.\ntype MiddlewareConfigurator interface {\n\tToMiddleware() (MiddlewareFunc, error)\n}\n\n// Validator is the interface that wraps the Validate function.\ntype Validator interface {\n\tValidate(i any) error\n}\n\n// MIME types\nconst (\n\t// MIMEApplicationJSON JavaScript Object Notation (JSON) https://www.rfc-editor.org/rfc/rfc8259\n\tMIMEApplicationJSON = \"application/json\"\n\t// Deprecated: Please use MIMEApplicationJSON instead. JSON should be encoded using UTF-8 by default.\n\t// No \"charset\" parameter is defined for this registration.\n\t// Adding one really has no effect on compliant recipients.\n\t// See RFC 8259, section 8.1. https://datatracker.ietf.org/doc/html/rfc8259#section-8.1n\"\n\tMIMEApplicationJSONCharsetUTF8       = MIMEApplicationJSON + \"; \" + charsetUTF8\n\tMIMEApplicationJavaScript            = \"application/javascript\"\n\tMIMEApplicationJavaScriptCharsetUTF8 = MIMEApplicationJavaScript + \"; \" + charsetUTF8\n\tMIMEApplicationXML                   = \"application/xml\"\n\tMIMEApplicationXMLCharsetUTF8        = MIMEApplicationXML + \"; \" + charsetUTF8\n\tMIMETextXML                          = \"text/xml\"\n\tMIMETextXMLCharsetUTF8               = MIMETextXML + \"; \" + charsetUTF8\n\tMIMEApplicationForm                  = \"application/x-www-form-urlencoded\"\n\tMIMEApplicationProtobuf              = \"application/protobuf\"\n\tMIMEApplicationMsgpack               = \"application/msgpack\"\n\tMIMETextHTML                         = \"text/html\"\n\tMIMETextHTMLCharsetUTF8              = MIMETextHTML + \"; \" + charsetUTF8\n\tMIMETextPlain                        = \"text/plain\"\n\tMIMETextPlainCharsetUTF8             = MIMETextPlain + \"; \" + charsetUTF8\n\tMIMEMultipartForm                    = \"multipart/form-data\"\n\tMIMEOctetStream                      = \"application/octet-stream\"\n)\n\nconst (\n\tcharsetUTF8 = \"charset=UTF-8\"\n\t// PROPFIND Method can be used on collection and property resources.\n\tPROPFIND = \"PROPFIND\"\n\t// REPORT Method can be used to get information about a resource, see rfc 3253\n\tREPORT = \"REPORT\"\n\t// RouteNotFound is special method type for routes handling \"route not found\" (404) cases\n\tRouteNotFound = \"echo_route_not_found\"\n\t// RouteAny is special method type that matches any HTTP method in request. Any has lower\n\t// priority that other methods that have been registered with Router to that path.\n\tRouteAny = \"echo_route_any\"\n)\n\n// Headers\nconst (\n\tHeaderAccept         = \"Accept\"\n\tHeaderAcceptEncoding = \"Accept-Encoding\"\n\t// HeaderAllow is the name of the \"Allow\" header field used to list the set of methods\n\t// advertised as supported by the target resource. Returning an Allow header is mandatory\n\t// for status 405 (method not found) and useful for the OPTIONS method in responses.\n\t// See RFC 7231: https://datatracker.ietf.org/doc/html/rfc7231#section-7.4.1\n\tHeaderAllow               = \"Allow\"\n\tHeaderAuthorization       = \"Authorization\"\n\tHeaderContentDisposition  = \"Content-Disposition\"\n\tHeaderContentEncoding     = \"Content-Encoding\"\n\tHeaderContentLength       = \"Content-Length\"\n\tHeaderContentType         = \"Content-Type\"\n\tHeaderCookie              = \"Cookie\"\n\tHeaderSetCookie           = \"Set-Cookie\"\n\tHeaderIfModifiedSince     = \"If-Modified-Since\"\n\tHeaderLastModified        = \"Last-Modified\"\n\tHeaderLocation            = \"Location\"\n\tHeaderRetryAfter          = \"Retry-After\"\n\tHeaderUpgrade             = \"Upgrade\"\n\tHeaderVary                = \"Vary\"\n\tHeaderWWWAuthenticate     = \"WWW-Authenticate\"\n\tHeaderXForwardedFor       = \"X-Forwarded-For\"\n\tHeaderXForwardedProto     = \"X-Forwarded-Proto\"\n\tHeaderXForwardedProtocol  = \"X-Forwarded-Protocol\"\n\tHeaderXForwardedSsl       = \"X-Forwarded-Ssl\"\n\tHeaderXUrlScheme          = \"X-Url-Scheme\"\n\tHeaderXHTTPMethodOverride = \"X-HTTP-Method-Override\"\n\tHeaderXRealIP             = \"X-Real-Ip\"\n\tHeaderXRequestID          = \"X-Request-Id\"\n\tHeaderXCorrelationID      = \"X-Correlation-Id\"\n\tHeaderXRequestedWith      = \"X-Requested-With\"\n\tHeaderServer              = \"Server\"\n\n\t// HeaderOrigin request header indicates the origin (scheme, hostname, and port) that caused the request.\n\t// See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin\n\tHeaderOrigin       = \"Origin\"\n\tHeaderCacheControl = \"Cache-Control\"\n\tHeaderConnection   = \"Connection\"\n\n\t// Access control\n\tHeaderAccessControlRequestMethod    = \"Access-Control-Request-Method\"\n\tHeaderAccessControlRequestHeaders   = \"Access-Control-Request-Headers\"\n\tHeaderAccessControlAllowOrigin      = \"Access-Control-Allow-Origin\"\n\tHeaderAccessControlAllowMethods     = \"Access-Control-Allow-Methods\"\n\tHeaderAccessControlAllowHeaders     = \"Access-Control-Allow-Headers\"\n\tHeaderAccessControlAllowCredentials = \"Access-Control-Allow-Credentials\"\n\tHeaderAccessControlExposeHeaders    = \"Access-Control-Expose-Headers\"\n\tHeaderAccessControlMaxAge           = \"Access-Control-Max-Age\"\n\n\t// Security\n\tHeaderStrictTransportSecurity         = \"Strict-Transport-Security\"\n\tHeaderXContentTypeOptions             = \"X-Content-Type-Options\"\n\tHeaderXXSSProtection                  = \"X-XSS-Protection\"\n\tHeaderXFrameOptions                   = \"X-Frame-Options\"\n\tHeaderContentSecurityPolicy           = \"Content-Security-Policy\"\n\tHeaderContentSecurityPolicyReportOnly = \"Content-Security-Policy-Report-Only\"\n\tHeaderXCSRFToken                      = \"X-CSRF-Token\" // #nosec G101\n\tHeaderReferrerPolicy                  = \"Referrer-Policy\"\n\n\t// HeaderSecFetchSite fetch metadata request header indicates the relationship between a request initiator's\n\t// origin and the origin of the requested resource.\n\t// See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-Fetch-Site\n\tHeaderSecFetchSite = \"Sec-Fetch-Site\"\n)\n\n// Config is configuration for NewWithConfig function\ntype Config struct {\n\t// Logger is the slog logger instance used for application-wide structured logging.\n\t// If not set, a default TextHandler writing to stdout is created.\n\tLogger *slog.Logger\n\n\t// HTTPErrorHandler is the centralized error handler that processes errors returned\n\t// by handlers and middleware, converting them to appropriate HTTP responses.\n\t// If not set, DefaultHTTPErrorHandler(false) is used.\n\tHTTPErrorHandler HTTPErrorHandler\n\n\t// Router is the HTTP request router responsible for matching URLs to handlers\n\t// using a radix tree-based algorithm.\n\t// If not set, NewRouter(RouterConfig{}) is used.\n\tRouter Router\n\n\t// OnAddRoute is an optional callback hook executed when routes are registered.\n\t// Useful for route validation, logging, or custom route processing.\n\t// If not set, no callback is executed.\n\tOnAddRoute func(route Route) error\n\n\t// Filesystem is the fs.FS implementation used for serving static files.\n\t// Supports os.DirFS, embed.FS, and custom implementations.\n\t// If not set, defaults to current working directory.\n\tFilesystem fs.FS\n\n\t// Binder handles automatic data binding from HTTP requests to Go structs.\n\t// Supports JSON, XML, form data, query parameters, and path parameters.\n\t// If not set, DefaultBinder is used.\n\tBinder Binder\n\n\t// Validator provides optional struct validation after data binding.\n\t// Commonly used with third-party validation libraries.\n\t// If not set, Context.Validate() returns ErrValidatorNotRegistered.\n\tValidator Validator\n\n\t// Renderer provides template rendering for generating HTML responses.\n\t// Requires integration with a template engine like html/template.\n\t// If not set, Context.Render() returns ErrRendererNotRegistered.\n\tRenderer Renderer\n\n\t// JSONSerializer handles JSON encoding and decoding for HTTP requests/responses.\n\t// Can be replaced with faster alternatives like jsoniter or sonic.\n\t// If not set, DefaultJSONSerializer using encoding/json is used.\n\tJSONSerializer JSONSerializer\n\n\t// IPExtractor defines the strategy for extracting the real client IP address\n\t// from requests, particularly important when behind proxies or load balancers.\n\t// Used for rate limiting, access control, and logging.\n\t// If not set, falls back to checking X-Forwarded-For and X-Real-IP headers.\n\tIPExtractor IPExtractor\n\n\t// FormParseMaxMemory is default value for memory limit that is used\n\t// when parsing multipart forms (See (*http.Request).ParseMultipartForm)\n\tFormParseMaxMemory int64\n}\n\n// NewWithConfig creates an instance of Echo with given configuration.\nfunc NewWithConfig(config Config) *Echo {\n\te := New()\n\tif config.Logger != nil {\n\t\te.Logger = config.Logger\n\t}\n\tif config.HTTPErrorHandler != nil {\n\t\te.HTTPErrorHandler = config.HTTPErrorHandler\n\t}\n\tif config.Router != nil {\n\t\te.router = config.Router\n\t}\n\tif config.OnAddRoute != nil {\n\t\te.OnAddRoute = config.OnAddRoute\n\t}\n\tif config.Filesystem != nil {\n\t\te.Filesystem = config.Filesystem\n\t}\n\tif config.Binder != nil {\n\t\te.Binder = config.Binder\n\t}\n\tif config.Validator != nil {\n\t\te.Validator = config.Validator\n\t}\n\tif config.Renderer != nil {\n\t\te.Renderer = config.Renderer\n\t}\n\tif config.JSONSerializer != nil {\n\t\te.JSONSerializer = config.JSONSerializer\n\t}\n\tif config.IPExtractor != nil {\n\t\te.IPExtractor = config.IPExtractor\n\t}\n\tif config.FormParseMaxMemory > 0 {\n\t\te.formParseMaxMemory = config.FormParseMaxMemory\n\t}\n\treturn e\n}\n\n// New creates an instance of Echo.\nfunc New() *Echo {\n\tlogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))\n\te := &Echo{\n\t\tLogger:             logger,\n\t\tFilesystem:         newDefaultFS(),\n\t\tBinder:             &DefaultBinder{},\n\t\tJSONSerializer:     &DefaultJSONSerializer{},\n\t\tformParseMaxMemory: defaultMemory,\n\t}\n\n\te.serveHTTPFunc = e.serveHTTP\n\te.router = NewRouter(RouterConfig{})\n\te.HTTPErrorHandler = DefaultHTTPErrorHandler(false)\n\te.contextPool.New = func() any {\n\t\treturn newContext(nil, nil, e)\n\t}\n\treturn e\n}\n\n// NewContext returns a new Context instance.\n//\n// Note: both request and response can be left to nil as Echo.ServeHTTP will call c.Reset(req,resp) anyway\n// these arguments are useful when creating context for tests and cases like that.\nfunc (e *Echo) NewContext(r *http.Request, w http.ResponseWriter) *Context {\n\treturn newContext(r, w, e)\n}\n\n// Router returns the default router.\nfunc (e *Echo) Router() Router {\n\treturn e.router\n}\n\n// DefaultHTTPErrorHandler creates new default HTTP error handler implementation. It sends a JSON response\n// with status code. `exposeError` parameter decides if returned message will contain also error message or not\n//\n// Note: DefaultHTTPErrorHandler does not log errors. Use middleware for it if errors need to be logged (separately)\n// Note: In case errors happens in middleware call-chain that is returning from handler (which did not return an error).\n// When handler has already sent response (ala c.JSON()) and there is error in middleware that is returning from\n// handler. Then the error that global error handler received will be ignored because we have already \"committed\" the\n// response and status code header has been sent to the client.\nfunc DefaultHTTPErrorHandler(exposeError bool) HTTPErrorHandler {\n\treturn func(c *Context, err error) {\n\t\tif r, _ := UnwrapResponse(c.response); r != nil && r.Committed {\n\t\t\treturn\n\t\t}\n\n\t\tcode := http.StatusInternalServerError\n\t\tvar sc HTTPStatusCoder\n\t\tif errors.As(err, &sc) {\n\t\t\tif tmp := sc.StatusCode(); tmp != 0 {\n\t\t\t\tcode = tmp\n\t\t\t}\n\t\t}\n\n\t\tvar result any\n\t\tswitch m := sc.(type) {\n\t\tcase json.Marshaler: // this type knows how to format itself to JSON\n\t\t\tresult = m\n\t\tcase *HTTPError:\n\t\t\tsText := m.Message\n\t\t\tif sText == \"\" {\n\t\t\t\tsText = http.StatusText(code)\n\t\t\t}\n\t\t\tmsg := map[string]any{\"message\": sText}\n\t\t\tif exposeError {\n\t\t\t\tif wrappedErr := m.Unwrap(); wrappedErr != nil {\n\t\t\t\t\tmsg[\"error\"] = wrappedErr.Error()\n\t\t\t\t}\n\t\t\t}\n\t\t\tresult = msg\n\t\tdefault:\n\t\t\tmsg := map[string]any{\"message\": http.StatusText(code)}\n\t\t\tif exposeError {\n\t\t\t\tmsg[\"error\"] = err.Error()\n\t\t\t}\n\t\t\tresult = msg\n\t\t}\n\n\t\tvar cErr error\n\t\tif c.Request().Method == http.MethodHead { // Issue #608\n\t\t\tcErr = c.NoContent(code)\n\t\t} else {\n\t\t\tcErr = c.JSON(code, result)\n\t\t}\n\t\tif cErr != nil {\n\t\t\tc.Logger().Error(\"echo default error handler failed to send error to client\", \"error\", cErr) // truly rare case. ala client already disconnected\n\t\t}\n\t}\n}\n\n// Pre adds middleware to the chain which is run before router tries to find matching route.\n// Meaning middleware is executed even for 404 (not found) cases.\nfunc (e *Echo) Pre(middleware ...MiddlewareFunc) {\n\te.premiddleware = append(e.premiddleware, middleware...)\n}\n\n// Use adds middleware to the chain which is run after router has found matching route and before route/request handler method is executed.\nfunc (e *Echo) Use(middleware ...MiddlewareFunc) {\n\te.middleware = append(e.middleware, middleware...)\n}\n\n// CONNECT registers a new CONNECT route for a path with matching handler in the\n// router with optional route-level middleware. Panics on error.\nfunc (e *Echo) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {\n\treturn e.Add(http.MethodConnect, path, h, m...)\n}\n\n// DELETE registers a new DELETE route for a path with matching handler in the router\n// with optional route-level middleware. Panics on error.\nfunc (e *Echo) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {\n\treturn e.Add(http.MethodDelete, path, h, m...)\n}\n\n// GET registers a new GET route for a path with matching handler in the router\n// with optional route-level middleware. Panics on error.\nfunc (e *Echo) GET(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {\n\treturn e.Add(http.MethodGet, path, h, m...)\n}\n\n// HEAD registers a new HEAD route for a path with matching handler in the\n// router with optional route-level middleware. Panics on error.\nfunc (e *Echo) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {\n\treturn e.Add(http.MethodHead, path, h, m...)\n}\n\n// OPTIONS registers a new OPTIONS route for a path with matching handler in the\n// router with optional route-level middleware. Panics on error.\nfunc (e *Echo) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {\n\treturn e.Add(http.MethodOptions, path, h, m...)\n}\n\n// PATCH registers a new PATCH route for a path with matching handler in the\n// router with optional route-level middleware. Panics on error.\nfunc (e *Echo) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {\n\treturn e.Add(http.MethodPatch, path, h, m...)\n}\n\n// POST registers a new POST route for a path with matching handler in the\n// router with optional route-level middleware. Panics on error.\nfunc (e *Echo) POST(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {\n\treturn e.Add(http.MethodPost, path, h, m...)\n}\n\n// PUT registers a new PUT route for a path with matching handler in the\n// router with optional route-level middleware. Panics on error.\nfunc (e *Echo) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {\n\treturn e.Add(http.MethodPut, path, h, m...)\n}\n\n// TRACE registers a new TRACE route for a path with matching handler in the\n// router with optional route-level middleware. Panics on error.\nfunc (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {\n\treturn e.Add(http.MethodTrace, path, h, m...)\n}\n\n// RouteNotFound registers a special-case route which is executed when no other route is found (i.e. HTTP 404 cases)\n// for current request URL.\n// Path supports static and named/any parameters just like other http method is defined. Generally path is ended with\n// wildcard/match-any character (`/*`, `/download/*` etc).\n//\n// Example: `e.RouteNotFound(\"/*\", func(c *echo.Context) error { return c.NoContent(http.StatusNotFound) })`\nfunc (e *Echo) RouteNotFound(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {\n\treturn e.Add(RouteNotFound, path, h, m...)\n}\n\n// Any registers a new route for all HTTP methods (supported by Echo) and path with matching handler\n// in the router with optional route-level middleware.\n//\n// Note: this method only adds specific set of supported HTTP methods as handler and is not true\n// \"catch-any-arbitrary-method\" way of matching requests.\nfunc (e *Echo) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) RouteInfo {\n\treturn e.Add(RouteAny, path, handler, middleware...)\n}\n\n// Match registers a new route for multiple HTTP methods and path with matching\n// handler in the router with optional route-level middleware. Panics on error.\nfunc (e *Echo) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) Routes {\n\terrs := make([]error, 0)\n\tris := make(Routes, 0)\n\tfor _, m := range methods {\n\t\tri, err := e.AddRoute(Route{\n\t\t\tMethod:      m,\n\t\t\tPath:        path,\n\t\t\tHandler:     handler,\n\t\t\tMiddlewares: middleware,\n\t\t})\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t\tcontinue\n\t\t}\n\t\tris = append(ris, ri)\n\t}\n\tif len(errs) > 0 {\n\t\tpanic(errs) // this is how `v4` handles errors. `v5` has methods to have panic-free usage\n\t}\n\treturn ris\n}\n\n// Static registers a new route with path prefix to serve static files from the provided root directory.\nfunc (e *Echo) Static(pathPrefix, fsRoot string, middleware ...MiddlewareFunc) RouteInfo {\n\tsubFs := MustSubFS(e.Filesystem, fsRoot)\n\treturn e.Add(\n\t\thttp.MethodGet,\n\t\tpathPrefix+\"*\",\n\t\tStaticDirectoryHandler(subFs, false),\n\t\tmiddleware...,\n\t)\n}\n\n// StaticFS registers a new route with path prefix to serve static files from the provided file system.\n//\n// When dealing with `embed.FS` use `fs := echo.MustSubFS(fs, \"rootDirectory\") to create sub fs which uses necessary\n// prefix for directory path. This is necessary as `//go:embed assets/images` embeds files with paths\n// including `assets/images` as their prefix.\nfunc (e *Echo) StaticFS(pathPrefix string, filesystem fs.FS, middleware ...MiddlewareFunc) RouteInfo {\n\treturn e.Add(\n\t\thttp.MethodGet,\n\t\tpathPrefix+\"*\",\n\t\tStaticDirectoryHandler(filesystem, false),\n\t\tmiddleware...,\n\t)\n}\n\n// StaticDirectoryHandler creates handler function to serve files from provided file system\n// When disablePathUnescaping is set then file name from path is not unescaped and is served as is.\nfunc StaticDirectoryHandler(fileSystem fs.FS, disablePathUnescaping bool) HandlerFunc {\n\treturn func(c *Context) error {\n\t\tp := c.Param(\"*\")\n\t\tif !disablePathUnescaping { // when router is already unescaping we do not want to do is twice\n\t\t\ttmpPath, err := url.PathUnescape(p)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to unescape path variable: %w\", err)\n\t\t\t}\n\t\t\tp = tmpPath\n\t\t}\n\n\t\t// fs.FS.Open() already assumes that file names are relative to FS root path and considers name with prefix `/` as invalid\n\t\tname := filepath.ToSlash(filepath.Clean(strings.TrimPrefix(p, \"/\")))\n\t\tfi, err := fs.Stat(fileSystem, name)\n\t\tif err != nil {\n\t\t\treturn ErrNotFound\n\t\t}\n\n\t\t// If the request is for a directory and does not end with \"/\"\n\t\tp = c.Request().URL.Path // path must not be empty.\n\t\tif fi.IsDir() && len(p) > 0 && p[len(p)-1] != '/' {\n\t\t\t// Redirect to ends with \"/\"\n\t\t\treturn c.Redirect(http.StatusMovedPermanently, sanitizeURI(p+\"/\"))\n\t\t}\n\t\treturn fsFile(c, name, fileSystem)\n\t}\n}\n\n// FileFS registers a new route with path to serve file from the provided file system.\nfunc (e *Echo) FileFS(path, file string, filesystem fs.FS, m ...MiddlewareFunc) RouteInfo {\n\treturn e.GET(path, StaticFileHandler(file, filesystem), m...)\n}\n\n// StaticFileHandler creates handler function to serve file from provided file system\nfunc StaticFileHandler(file string, filesystem fs.FS) HandlerFunc {\n\treturn func(c *Context) error {\n\t\treturn fsFile(c, file, filesystem)\n\t}\n}\n\n// File registers a new route with path to serve a static file with optional route-level middleware. Panics on error.\nfunc (e *Echo) File(path, file string, middleware ...MiddlewareFunc) RouteInfo {\n\thandler := func(c *Context) error {\n\t\treturn c.File(file)\n\t}\n\treturn e.Add(http.MethodGet, path, handler, middleware...)\n}\n\n// AddRoute registers a new Route with default host Router\nfunc (e *Echo) AddRoute(route Route) (RouteInfo, error) {\n\treturn e.add(route)\n}\n\nfunc (e *Echo) add(route Route) (RouteInfo, error) {\n\tif e.OnAddRoute != nil {\n\t\tif err := e.OnAddRoute(route); err != nil {\n\t\t\treturn RouteInfo{}, err\n\t\t}\n\t}\n\n\tri, err := e.router.Add(route)\n\tif err != nil {\n\t\treturn RouteInfo{}, err\n\t}\n\n\tparamsCount := int32(len(ri.Parameters)) // #nosec G115\n\tif paramsCount > e.contextPathParamAllocSize.Load() {\n\t\te.contextPathParamAllocSize.Store(paramsCount)\n\t}\n\treturn ri, nil\n}\n\n// Add registers a new route for an HTTP method and path with matching handler\n// in the router with optional route-level middleware.\nfunc (e *Echo) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) RouteInfo {\n\tri, err := e.add(\n\t\tRoute{\n\t\t\tMethod:      method,\n\t\t\tPath:        path,\n\t\t\tHandler:     handler,\n\t\t\tMiddlewares: middleware,\n\t\t\tName:        \"\",\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err) // this is how `v4` handles errors. `v5` has methods to have panic-free usage\n\t}\n\treturn ri\n}\n\n// Group creates a new router group with prefix and optional group-level middleware.\nfunc (e *Echo) Group(prefix string, m ...MiddlewareFunc) (g *Group) {\n\tg = &Group{prefix: prefix, echo: e}\n\tg.Use(m...)\n\treturn\n}\n\n// PreMiddlewares returns registered pre middlewares. These are middleware to the chain\n// which are run before router tries to find matching route.\n// Use this method to build your own ServeHTTP method.\n//\n// NOTE: returned slice is not a copy. Do not mutate.\nfunc (e *Echo) PreMiddlewares() []MiddlewareFunc {\n\treturn e.premiddleware\n}\n\n// Middlewares returns registered route level middlewares. Does not contain any group level\n// middlewares. Use this method to build your own ServeHTTP method.\n//\n// NOTE: returned slice is not a copy. Do not mutate.\nfunc (e *Echo) Middlewares() []MiddlewareFunc {\n\treturn e.middleware\n}\n\n// AcquireContext returns an empty `Context` instance from the pool.\n// You must return the context by calling `ReleaseContext()`.\nfunc (e *Echo) AcquireContext() *Context {\n\treturn e.contextPool.Get().(*Context)\n}\n\n// ReleaseContext returns the `Context` instance back to the pool.\n// You must call it after `AcquireContext()`.\nfunc (e *Echo) ReleaseContext(c *Context) {\n\te.contextPool.Put(c)\n}\n\n// ServeHTTP implements `http.Handler` interface, which serves HTTP requests.\nfunc (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\te.serveHTTPFunc(w, r)\n}\n\n// serveHTTP implements `http.Handler` interface, which serves HTTP requests.\nfunc (e *Echo) serveHTTP(w http.ResponseWriter, r *http.Request) {\n\tc := e.contextPool.Get().(*Context)\n\tdefer e.contextPool.Put(c)\n\n\tc.Reset(r, w)\n\tvar h HandlerFunc\n\n\tif e.premiddleware == nil {\n\t\th = applyMiddleware(e.router.Route(c), e.middleware...)\n\t} else {\n\t\th = func(cc *Context) error {\n\t\t\th1 := applyMiddleware(e.router.Route(cc), e.middleware...)\n\t\t\treturn h1(cc)\n\t\t}\n\t\th = applyMiddleware(h, e.premiddleware...)\n\t}\n\n\t// Execute chain\n\tif err := h(c); err != nil {\n\t\te.HTTPErrorHandler(c, err)\n\t}\n}\n\n// Start stars HTTP server on given address with Echo as a handler serving requests. The server can be shutdown by\n// sending os.Interrupt signal with `ctrl+c`. Method returns only errors that are not http.ErrServerClosed.\n//\n// Note: this method is created for use in examples/demos and is deliberately simple without providing configuration\n// options.\n//\n// In need of customization use:\n//\n//\tctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n//\tdefer cancel()\n//\tsc := echo.StartConfig{Address: \":8080\"}\n//\tif err := sc.Start(ctx, e); err != nil && !errors.Is(err, http.ErrServerClosed) {\n//\t\tslog.Error(err.Error())\n//\t}\n//\n// // or standard library `http.Server`\n//\n//\ts := http.Server{Addr: \":8080\", Handler: e}\n//\tif err := s.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {\n//\t\tslog.Error(err.Error())\n//\t}\nfunc (e *Echo) Start(address string) error {\n\tsc := StartConfig{Address: address}\n\tctx, cancel := signal.NotifyContext(stdContext.Background(), os.Interrupt, syscall.SIGTERM) // start shutdown process on ctrl+c\n\tdefer cancel()\n\treturn sc.Start(ctx, e)\n}\n\n// WrapHandler wraps `http.Handler` into `echo.HandlerFunc`.\nfunc WrapHandler(h http.Handler) HandlerFunc {\n\treturn func(c *Context) error {\n\t\treq := c.Request()\n\t\treq.Pattern = c.Path()\n\t\tfor _, p := range c.PathValues() {\n\t\t\treq.SetPathValue(p.Name, p.Value)\n\t\t}\n\n\t\th.ServeHTTP(c.Response(), req)\n\t\treturn nil\n\t}\n}\n\n// WrapMiddleware wraps `func(http.Handler) http.Handler` into `echo.MiddlewareFunc`\nfunc WrapMiddleware(m func(http.Handler) http.Handler) MiddlewareFunc {\n\treturn func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) (err error) {\n\t\t\treq := c.Request()\n\t\t\treq.Pattern = c.Path()\n\t\t\tfor _, p := range c.PathValues() {\n\t\t\t\treq.SetPathValue(p.Name, p.Value)\n\t\t\t}\n\n\t\t\tm(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tc.SetRequest(r)\n\t\t\t\tc.SetResponse(NewResponse(w, c.echo.Logger))\n\t\t\t\terr = next(c)\n\t\t\t})).ServeHTTP(c.Response(), req)\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc applyMiddleware(h HandlerFunc, middleware ...MiddlewareFunc) HandlerFunc {\n\tfor i := len(middleware) - 1; i >= 0; i-- {\n\t\th = middleware[i](h)\n\t}\n\treturn h\n}\n\n// defaultFS emulates os.Open behaviour with filesystem opened by `os.DirFs`. Difference between `os.Open` and `fs.Open`\n// is that FS does not allow to open path that start with `..` or `/` etc. For example previously you could have `../images`\n// in your application but `fs := os.DirFS(\"./\")` would not allow you to use `fs.Open(\"../images\")` and this would break\n// all old applications that rely on being able to traverse up from current executable run path.\n// NB: private because you really should use fs.FS implementation instances\ntype defaultFS struct {\n\tfs     fs.FS\n\tprefix string\n}\n\nfunc newDefaultFS() *defaultFS {\n\tdir, _ := os.Getwd()\n\treturn &defaultFS{\n\t\tprefix: dir,\n\t\tfs:     os.DirFS(dir),\n\t}\n}\n\nfunc (fs defaultFS) Open(name string) (fs.File, error) {\n\treturn fs.fs.Open(name)\n}\n\nfunc subFS(currentFs fs.FS, root string) (fs.FS, error) {\n\troot = filepath.ToSlash(filepath.Clean(root)) // note: fs.FS operates only with slashes. `ToSlash` is necessary for Windows\n\tif dFS, ok := currentFs.(*defaultFS); ok {\n\t\t// we need to make exception for `defaultFS` instances as it interprets root prefix differently from fs.FS.\n\t\t// fs.Fs.Open does not like relative paths (\"./\", \"../\") and absolute paths at all but prior echo.Filesystem we\n\t\t// were able to use paths like `./myfile.log`, `/etc/hosts` and these would work fine with `os.Open` but not with fs.Fs\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(dFS.prefix, root)\n\t\t}\n\t\treturn &defaultFS{\n\t\t\tprefix: root,\n\t\t\tfs:     os.DirFS(root),\n\t\t}, nil\n\t}\n\treturn fs.Sub(currentFs, root)\n}\n\n// MustSubFS creates sub FS from current filesystem or panic on failure.\n// Panic happens when `fsRoot` contains invalid path according to `fs.ValidPath` rules.\n//\n// MustSubFS is helpful when dealing with `embed.FS` because for example `//go:embed assets/images` embeds files with\n// paths including `assets/images` as their prefix. In that case use `fs := echo.MustSubFS(fs, \"rootDirectory\") to\n// create sub fs which uses necessary prefix for directory path.\nfunc MustSubFS(currentFs fs.FS, fsRoot string) fs.FS {\n\tsubFs, err := subFS(currentFs, fsRoot)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"can not create sub FS, invalid root given, err: %w\", err))\n\t}\n\treturn subFs\n}\n\nfunc sanitizeURI(uri string) string {\n\t// double slash `\\\\`, `//` or even `\\/` is absolute uri for browsers and by redirecting request to that uri\n\t// we are vulnerable to open redirect attack. so replace all slashes from the beginning with single slash\n\tif len(uri) > 1 && (uri[0] == '\\\\' || uri[0] == '/') && (uri[1] == '\\\\' || uri[1] == '/') {\n\t\turi = \"/\" + strings.TrimLeft(uri, `/\\`)\n\t}\n\treturn uri\n}\n"
  },
  {
    "path": "echo_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"bytes\"\n\tstdContext \"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype user struct {\n\tID   int    `json:\"id\" xml:\"id\" form:\"id\" query:\"id\" param:\"id\" header:\"id\"`\n\tName string `json:\"name\" xml:\"name\" form:\"name\" query:\"name\" param:\"name\" header:\"name\"`\n}\n\nconst (\n\tuserJSON                    = `{\"id\":1,\"name\":\"Jon Snow\"}`\n\tusersJSON                   = `[{\"id\":1,\"name\":\"Jon Snow\"}]`\n\tuserXML                     = `<user><id>1</id><name>Jon Snow</name></user>`\n\tuserForm                    = `id=1&name=Jon Snow`\n\tinvalidContent              = \"invalid content\"\n\tuserJSONInvalidType         = `{\"id\":\"1\",\"name\":\"Jon Snow\"}`\n\tuserXMLConvertNumberError   = `<user><id>Number one</id><name>Jon Snow</name></user>`\n\tuserXMLUnsupportedTypeError = `<user><>Number one</><name>Jon Snow</name></user>`\n)\n\nconst userJSONPretty = `{\n  \"id\": 1,\n  \"name\": \"Jon Snow\"\n}`\n\nconst userXMLPretty = `<user>\n  <id>1</id>\n  <name>Jon Snow</name>\n</user>`\n\nvar dummyQuery = url.Values{\"dummy\": []string{\"useless\"}}\n\nfunc TestEcho(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\t// Router\n\tassert.NotNil(t, e.Router())\n\n\te.HTTPErrorHandler(c, errors.New(\"error\"))\n\n\tassert.Equal(t, http.StatusInternalServerError, rec.Code)\n}\n\nfunc TestNewWithConfig(t *testing.T) {\n\te := NewWithConfig(Config{})\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\n\te.GET(\"/\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"Hello, World!\")\n\t})\n\te.ServeHTTP(rec, req)\n\n\tassert.Equal(t, http.StatusTeapot, rec.Code)\n\tassert.Equal(t, `Hello, World!`, rec.Body.String())\n}\n\nfunc TestEcho_StaticFS(t *testing.T) {\n\tvar testCases = []struct {\n\t\tgivenFs              fs.FS\n\t\tname                 string\n\t\tgivenPrefix          string\n\t\tgivenFsRoot          string\n\t\twhenURL              string\n\t\texpectHeaderLocation string\n\t\texpectBodyStartsWith string\n\t\texpectStatus         int\n\t}{\n\t\t{\n\t\t\tname:                 \"ok\",\n\t\t\tgivenPrefix:          \"/images\",\n\t\t\tgivenFs:              os.DirFS(\"./_fixture/images\"),\n\t\t\twhenURL:              \"/images/walle.png\",\n\t\t\texpectStatus:         http.StatusOK,\n\t\t\texpectBodyStartsWith: string([]byte{0x89, 0x50, 0x4e, 0x47}),\n\t\t},\n\t\t{\n\t\t\tname:                 \"ok, from sub fs\",\n\t\t\tgivenPrefix:          \"/images\",\n\t\t\tgivenFs:              MustSubFS(os.DirFS(\"./_fixture/\"), \"images\"),\n\t\t\twhenURL:              \"/images/walle.png\",\n\t\t\texpectStatus:         http.StatusOK,\n\t\t\texpectBodyStartsWith: string([]byte{0x89, 0x50, 0x4e, 0x47}),\n\t\t},\n\t\t{\n\t\t\tname:                 \"No file\",\n\t\t\tgivenPrefix:          \"/images\",\n\t\t\tgivenFs:              os.DirFS(\"_fixture/scripts\"),\n\t\t\twhenURL:              \"/images/bolt.png\",\n\t\t\texpectStatus:         http.StatusNotFound,\n\t\t\texpectBodyStartsWith: \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Directory\",\n\t\t\tgivenPrefix:          \"/images\",\n\t\t\tgivenFs:              os.DirFS(\"_fixture/images\"),\n\t\t\twhenURL:              \"/images/\",\n\t\t\texpectStatus:         http.StatusNotFound,\n\t\t\texpectBodyStartsWith: \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Directory Redirect\",\n\t\t\tgivenPrefix:          \"/\",\n\t\t\tgivenFs:              os.DirFS(\"_fixture/\"),\n\t\t\twhenURL:              \"/folder\",\n\t\t\texpectStatus:         http.StatusMovedPermanently,\n\t\t\texpectHeaderLocation: \"/folder/\",\n\t\t\texpectBodyStartsWith: \"\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Directory Redirect with non-root path\",\n\t\t\tgivenPrefix:          \"/static\",\n\t\t\tgivenFs:              os.DirFS(\"_fixture\"),\n\t\t\twhenURL:              \"/static\",\n\t\t\texpectStatus:         http.StatusMovedPermanently,\n\t\t\texpectHeaderLocation: \"/static/\",\n\t\t\texpectBodyStartsWith: \"\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Prefixed directory 404 (request URL without slash)\",\n\t\t\tgivenPrefix:          \"/folder/\", // trailing slash will intentionally not match \"/folder\"\n\t\t\tgivenFs:              os.DirFS(\"_fixture\"),\n\t\t\twhenURL:              \"/folder\", // no trailing slash\n\t\t\texpectStatus:         http.StatusNotFound,\n\t\t\texpectBodyStartsWith: \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Prefixed directory redirect (without slash redirect to slash)\",\n\t\t\tgivenPrefix:          \"/folder\", // no trailing slash shall match /folder and /folder/*\n\t\t\tgivenFs:              os.DirFS(\"_fixture\"),\n\t\t\twhenURL:              \"/folder\", // no trailing slash\n\t\t\texpectStatus:         http.StatusMovedPermanently,\n\t\t\texpectHeaderLocation: \"/folder/\",\n\t\t\texpectBodyStartsWith: \"\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Directory with index.html\",\n\t\t\tgivenPrefix:          \"/\",\n\t\t\tgivenFs:              os.DirFS(\"_fixture\"),\n\t\t\twhenURL:              \"/\",\n\t\t\texpectStatus:         http.StatusOK,\n\t\t\texpectBodyStartsWith: \"<!doctype html>\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Prefixed directory with index.html (prefix ending with slash)\",\n\t\t\tgivenPrefix:          \"/assets/\",\n\t\t\tgivenFs:              os.DirFS(\"_fixture\"),\n\t\t\twhenURL:              \"/assets/\",\n\t\t\texpectStatus:         http.StatusOK,\n\t\t\texpectBodyStartsWith: \"<!doctype html>\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Prefixed directory with index.html (prefix ending without slash)\",\n\t\t\tgivenPrefix:          \"/assets\",\n\t\t\tgivenFs:              os.DirFS(\"_fixture\"),\n\t\t\twhenURL:              \"/assets/\",\n\t\t\texpectStatus:         http.StatusOK,\n\t\t\texpectBodyStartsWith: \"<!doctype html>\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Sub-directory with index.html\",\n\t\t\tgivenPrefix:          \"/\",\n\t\t\tgivenFs:              os.DirFS(\"_fixture\"),\n\t\t\twhenURL:              \"/folder/\",\n\t\t\texpectStatus:         http.StatusOK,\n\t\t\texpectBodyStartsWith: \"<!doctype html>\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"do not allow directory traversal (backslash - windows separator)\",\n\t\t\tgivenPrefix:          \"/\",\n\t\t\tgivenFs:              os.DirFS(\"_fixture/\"),\n\t\t\twhenURL:              `/..\\\\middleware/basic_auth.go`,\n\t\t\texpectStatus:         http.StatusNotFound,\n\t\t\texpectBodyStartsWith: \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"do not allow directory traversal (slash - unix separator)\",\n\t\t\tgivenPrefix:          \"/\",\n\t\t\tgivenFs:              os.DirFS(\"_fixture/\"),\n\t\t\twhenURL:              `/../middleware/basic_auth.go`,\n\t\t\texpectStatus:         http.StatusNotFound,\n\t\t\texpectBodyStartsWith: \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"open redirect vulnerability\",\n\t\t\tgivenPrefix:          \"/\",\n\t\t\tgivenFs:              os.DirFS(\"_fixture/\"),\n\t\t\twhenURL:              \"/open.redirect.hackercom%2f..\",\n\t\t\texpectStatus:         http.StatusMovedPermanently,\n\t\t\texpectHeaderLocation: \"/open.redirect.hackercom/../\", // location starting with `//open` would be very bad\n\t\t\texpectBodyStartsWith: \"\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\n\t\t\ttmpFs := tc.givenFs\n\t\t\tif tc.givenFsRoot != \"\" {\n\t\t\t\ttmpFs = MustSubFS(tmpFs, tc.givenFsRoot)\n\t\t\t}\n\t\t\te.StaticFS(tc.givenPrefix, tmpFs)\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)\n\t\t\trec := httptest.NewRecorder()\n\n\t\t\te.ServeHTTP(rec, req)\n\n\t\t\tassert.Equal(t, tc.expectStatus, rec.Code)\n\t\t\tbody := rec.Body.String()\n\t\t\tif tc.expectBodyStartsWith != \"\" {\n\t\t\t\tassert.True(t, strings.HasPrefix(body, tc.expectBodyStartsWith))\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, \"\", body)\n\t\t\t}\n\n\t\t\tif tc.expectHeaderLocation != \"\" {\n\t\t\t\tassert.Equal(t, tc.expectHeaderLocation, rec.Result().Header[\"Location\"][0])\n\t\t\t} else {\n\t\t\t\t_, ok := rec.Result().Header[\"Location\"]\n\t\t\t\tassert.False(t, ok)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEcho_FileFS(t *testing.T) {\n\tvar testCases = []struct {\n\t\twhenFS           fs.FS\n\t\tname             string\n\t\twhenPath         string\n\t\twhenFile         string\n\t\tgivenURL         string\n\t\texpectStartsWith []byte\n\t\texpectCode       int\n\t}{\n\t\t{\n\t\t\tname:             \"ok\",\n\t\t\twhenPath:         \"/walle\",\n\t\t\twhenFS:           os.DirFS(\"_fixture/images\"),\n\t\t\twhenFile:         \"walle.png\",\n\t\t\tgivenURL:         \"/walle\",\n\t\t\texpectCode:       http.StatusOK,\n\t\t\texpectStartsWith: []byte{0x89, 0x50, 0x4e},\n\t\t},\n\t\t{\n\t\t\tname:             \"nok, requesting invalid path\",\n\t\t\twhenPath:         \"/walle\",\n\t\t\twhenFS:           os.DirFS(\"_fixture/images\"),\n\t\t\twhenFile:         \"walle.png\",\n\t\t\tgivenURL:         \"/walle.png\",\n\t\t\texpectCode:       http.StatusNotFound,\n\t\t\texpectStartsWith: []byte(`{\"message\":\"Not Found\"}`),\n\t\t},\n\t\t{\n\t\t\tname:             \"nok, serving not existent file from filesystem\",\n\t\t\twhenPath:         \"/walle\",\n\t\t\twhenFS:           os.DirFS(\"_fixture/images\"),\n\t\t\twhenFile:         \"not-existent.png\",\n\t\t\tgivenURL:         \"/walle\",\n\t\t\texpectCode:       http.StatusNotFound,\n\t\t\texpectStartsWith: []byte(`{\"message\":\"Not Found\"}`),\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\t\t\te.FileFS(tc.whenPath, tc.whenFile, tc.whenFS)\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.givenURL, nil)\n\t\t\trec := httptest.NewRecorder()\n\n\t\t\te.ServeHTTP(rec, req)\n\n\t\t\tassert.Equal(t, tc.expectCode, rec.Code)\n\n\t\t\tbody := rec.Body.Bytes()\n\t\t\tif len(body) > len(tc.expectStartsWith) {\n\t\t\t\tbody = body[:len(tc.expectStartsWith)]\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectStartsWith, body)\n\t\t})\n\t}\n}\n\nfunc TestEcho_StaticPanic(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\tgivenRoot string\n\t}{\n\t\t{\n\t\t\tname:      \"panics for ../\",\n\t\t\tgivenRoot: \"../assets\",\n\t\t},\n\t\t{\n\t\t\tname:      \"panics for /\",\n\t\t\tgivenRoot: \"/assets\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\t\t\te.Filesystem = os.DirFS(\"./\")\n\n\t\t\tassert.Panics(t, func() {\n\t\t\t\te.Static(\"../assets\", tc.givenRoot)\n\t\t\t})\n\t\t})\n\t}\n}\n\nfunc TestEchoStaticRedirectIndex(t *testing.T) {\n\te := New()\n\n\t// HandlerFunc\n\tri := e.Static(\"/static\", \"_fixture\")\n\tassert.Equal(t, http.MethodGet, ri.Method)\n\tassert.Equal(t, \"/static*\", ri.Path)\n\tassert.Equal(t, \"GET:/static*\", ri.Name)\n\tassert.Equal(t, []string{\"*\"}, ri.Parameters)\n\n\tctx, cancel := stdContext.WithTimeout(stdContext.Background(), 200*time.Millisecond)\n\tdefer cancel()\n\taddr, err := startOnRandomPort(ctx, e)\n\tif err != nil {\n\t\tassert.Fail(t, err.Error())\n\t}\n\n\tcode, body, err := doGet(fmt.Sprintf(\"http://%v/static\", addr))\n\tassert.NoError(t, err)\n\tassert.True(t, strings.HasPrefix(body, \"<!doctype html>\"))\n\tassert.Equal(t, http.StatusOK, code)\n}\n\nfunc TestEchoFile(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname             string\n\t\tgivenPath        string\n\t\tgivenFile        string\n\t\twhenPath         string\n\t\texpectStartsWith string\n\t\texpectCode       int\n\t}{\n\t\t{\n\t\t\tname:             \"ok\",\n\t\t\tgivenPath:        \"/walle\",\n\t\t\tgivenFile:        \"_fixture/images/walle.png\",\n\t\t\twhenPath:         \"/walle\",\n\t\t\texpectCode:       http.StatusOK,\n\t\t\texpectStartsWith: string([]byte{0x89, 0x50, 0x4e}),\n\t\t},\n\t\t{\n\t\t\tname:             \"ok with relative path\",\n\t\t\tgivenPath:        \"/\",\n\t\t\tgivenFile:        \"./go.mod\",\n\t\t\twhenPath:         \"/\",\n\t\t\texpectCode:       http.StatusOK,\n\t\t\texpectStartsWith: \"module github.com/labstack/echo/v\",\n\t\t},\n\t\t{\n\t\t\tname:             \"nok file does not exist\",\n\t\t\tgivenPath:        \"/\",\n\t\t\tgivenFile:        \"./this-file-does-not-exist\",\n\t\t\twhenPath:         \"/\",\n\t\t\texpectCode:       http.StatusNotFound,\n\t\t\texpectStartsWith: \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New() // we are using echo.defaultFS instance\n\t\t\te.File(tc.givenPath, tc.givenFile)\n\n\t\t\tc, b := request(http.MethodGet, tc.whenPath, e)\n\t\t\tassert.Equal(t, tc.expectCode, c)\n\n\t\t\tif len(b) > len(tc.expectStartsWith) {\n\t\t\t\tb = b[:len(tc.expectStartsWith)]\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectStartsWith, b)\n\t\t})\n\t}\n}\n\nfunc TestEchoMiddleware(t *testing.T) {\n\te := New()\n\tbuf := new(bytes.Buffer)\n\n\te.Pre(func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\t// before route match is found RouteInfo does not exist\n\t\t\tassert.Equal(t, RouteInfo{}, c.RouteInfo())\n\t\t\tbuf.WriteString(\"-1\")\n\t\t\treturn next(c)\n\t\t}\n\t})\n\n\te.Use(func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\tbuf.WriteString(\"1\")\n\t\t\treturn next(c)\n\t\t}\n\t})\n\n\te.Use(func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\tbuf.WriteString(\"2\")\n\t\t\treturn next(c)\n\t\t}\n\t})\n\n\te.Use(func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\tbuf.WriteString(\"3\")\n\t\t\treturn next(c)\n\t\t}\n\t})\n\n\t// Route\n\te.GET(\"/\", func(c *Context) error {\n\t\treturn c.String(http.StatusOK, \"OK\")\n\t})\n\n\tc, b := request(http.MethodGet, \"/\", e)\n\tassert.Equal(t, \"-1123\", buf.String())\n\tassert.Equal(t, http.StatusOK, c)\n\tassert.Equal(t, \"OK\", b)\n}\n\nfunc TestEchoMiddlewareError(t *testing.T) {\n\te := New()\n\te.Use(func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\treturn errors.New(\"error\")\n\t\t}\n\t})\n\te.GET(\"/\", notFoundHandler)\n\tc, _ := request(http.MethodGet, \"/\", e)\n\tassert.Equal(t, http.StatusInternalServerError, c)\n}\n\nfunc TestEchoHandler(t *testing.T) {\n\te := New()\n\n\t// HandlerFunc\n\te.GET(\"/ok\", func(c *Context) error {\n\t\treturn c.String(http.StatusOK, \"OK\")\n\t})\n\n\tc, b := request(http.MethodGet, \"/ok\", e)\n\tassert.Equal(t, http.StatusOK, c)\n\tassert.Equal(t, \"OK\", b)\n}\n\nfunc TestEchoWrapHandler(t *testing.T) {\n\te := New()\n\n\tvar actualID string\n\tvar actualPattern string\n\te.GET(\"/:id\", WrapHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t\tw.Write([]byte(\"test\"))\n\t\tactualID = r.PathValue(\"id\")\n\t\tactualPattern = r.Pattern\n\t})))\n\n\treq := httptest.NewRequest(http.MethodGet, \"/123\", nil)\n\trec := httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\n\tassert.Equal(t, http.StatusOK, rec.Code)\n\tassert.Equal(t, \"test\", rec.Body.String())\n\tassert.Equal(t, \"123\", actualID)\n\tassert.Equal(t, \"/:id\", actualPattern)\n}\n\nfunc TestEchoWrapMiddleware(t *testing.T) {\n\te := New()\n\n\tvar actualID string\n\tvar actualPattern string\n\te.Use(WrapMiddleware(func(h http.Handler) http.Handler {\n\t\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tactualID = r.PathValue(\"id\")\n\t\t\tactualPattern = r.Pattern\n\t\t\th.ServeHTTP(w, r)\n\t\t})\n\t}))\n\n\te.GET(\"/:id\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/123\", nil)\n\trec := httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\n\tassert.Equal(t, http.StatusTeapot, rec.Code)\n\tassert.Equal(t, \"OK\", rec.Body.String())\n\tassert.Equal(t, \"123\", actualID)\n\tassert.Equal(t, \"/:id\", actualPattern)\n}\n\nfunc TestEchoConnect(t *testing.T) {\n\te := New()\n\n\tri := e.CONNECT(\"/\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\tassert.Equal(t, http.MethodConnect, ri.Method)\n\tassert.Equal(t, \"/\", ri.Path)\n\tassert.Equal(t, http.MethodConnect+\":/\", ri.Name)\n\tassert.Nil(t, ri.Parameters)\n\n\tstatus, body := request(http.MethodConnect, \"/\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, \"OK\", body)\n}\n\nfunc TestEchoDelete(t *testing.T) {\n\te := New()\n\n\tri := e.DELETE(\"/\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\tassert.Equal(t, http.MethodDelete, ri.Method)\n\tassert.Equal(t, \"/\", ri.Path)\n\tassert.Equal(t, http.MethodDelete+\":/\", ri.Name)\n\tassert.Nil(t, ri.Parameters)\n\n\tstatus, body := request(http.MethodDelete, \"/\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, \"OK\", body)\n}\n\nfunc TestEchoGet(t *testing.T) {\n\te := New()\n\n\tri := e.GET(\"/\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\tassert.Equal(t, http.MethodGet, ri.Method)\n\tassert.Equal(t, \"/\", ri.Path)\n\tassert.Equal(t, http.MethodGet+\":/\", ri.Name)\n\tassert.Nil(t, ri.Parameters)\n\n\tstatus, body := request(http.MethodGet, \"/\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, \"OK\", body)\n}\n\nfunc TestEchoHead(t *testing.T) {\n\te := New()\n\n\tri := e.HEAD(\"/\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\tassert.Equal(t, http.MethodHead, ri.Method)\n\tassert.Equal(t, \"/\", ri.Path)\n\tassert.Equal(t, http.MethodHead+\":/\", ri.Name)\n\tassert.Nil(t, ri.Parameters)\n\n\tstatus, body := request(http.MethodHead, \"/\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, \"OK\", body)\n}\n\nfunc TestEchoOptions(t *testing.T) {\n\te := New()\n\n\tri := e.OPTIONS(\"/\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\tassert.Equal(t, http.MethodOptions, ri.Method)\n\tassert.Equal(t, \"/\", ri.Path)\n\tassert.Equal(t, http.MethodOptions+\":/\", ri.Name)\n\tassert.Nil(t, ri.Parameters)\n\n\tstatus, body := request(http.MethodOptions, \"/\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, \"OK\", body)\n}\n\nfunc TestEchoPatch(t *testing.T) {\n\te := New()\n\n\tri := e.PATCH(\"/\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\tassert.Equal(t, http.MethodPatch, ri.Method)\n\tassert.Equal(t, \"/\", ri.Path)\n\tassert.Equal(t, http.MethodPatch+\":/\", ri.Name)\n\tassert.Nil(t, ri.Parameters)\n\n\tstatus, body := request(http.MethodPatch, \"/\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, \"OK\", body)\n}\n\nfunc TestEchoPost(t *testing.T) {\n\te := New()\n\n\tri := e.POST(\"/\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\tassert.Equal(t, http.MethodPost, ri.Method)\n\tassert.Equal(t, \"/\", ri.Path)\n\tassert.Equal(t, http.MethodPost+\":/\", ri.Name)\n\tassert.Nil(t, ri.Parameters)\n\n\tstatus, body := request(http.MethodPost, \"/\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, \"OK\", body)\n}\n\nfunc TestEchoPut(t *testing.T) {\n\te := New()\n\n\tri := e.PUT(\"/\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\tassert.Equal(t, http.MethodPut, ri.Method)\n\tassert.Equal(t, \"/\", ri.Path)\n\tassert.Equal(t, http.MethodPut+\":/\", ri.Name)\n\tassert.Nil(t, ri.Parameters)\n\n\tstatus, body := request(http.MethodPut, \"/\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, \"OK\", body)\n}\n\nfunc TestEchoTrace(t *testing.T) {\n\te := New()\n\n\tri := e.TRACE(\"/\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\tassert.Equal(t, http.MethodTrace, ri.Method)\n\tassert.Equal(t, \"/\", ri.Path)\n\tassert.Equal(t, http.MethodTrace+\":/\", ri.Name)\n\tassert.Nil(t, ri.Parameters)\n\n\tstatus, body := request(http.MethodTrace, \"/\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, \"OK\", body)\n}\n\nfunc TestEcho_Any(t *testing.T) {\n\te := New()\n\n\tri := e.Any(\"/activate\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK from ANY\")\n\t})\n\n\tassert.Equal(t, RouteAny, ri.Method)\n\tassert.Equal(t, \"/activate\", ri.Path)\n\tassert.Equal(t, RouteAny+\":/activate\", ri.Name)\n\tassert.Nil(t, ri.Parameters)\n\n\tstatus, body := request(http.MethodTrace, \"/activate\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, `OK from ANY`, body)\n}\n\nfunc TestEcho_Any_hasLowerPriority(t *testing.T) {\n\te := New()\n\n\te.Any(\"/activate\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"ANY\")\n\t})\n\te.GET(\"/activate\", func(c *Context) error {\n\t\treturn c.String(http.StatusLocked, \"GET\")\n\t})\n\n\tstatus, body := request(http.MethodTrace, \"/activate\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, `ANY`, body)\n\n\tstatus, body = request(http.MethodGet, \"/activate\", e)\n\tassert.Equal(t, http.StatusLocked, status)\n\tassert.Equal(t, `GET`, body)\n}\n\nfunc TestEchoMatch(t *testing.T) { // JFC\n\te := New()\n\tris := e.Match([]string{http.MethodGet, http.MethodPost}, \"/\", func(c *Context) error {\n\t\treturn c.String(http.StatusOK, \"Match\")\n\t})\n\tassert.Len(t, ris, 2)\n}\n\nfunc TestEchoServeHTTPPathEncoding(t *testing.T) {\n\te := New()\n\te.GET(\"/with/slash\", func(c *Context) error {\n\t\treturn c.String(http.StatusOK, \"/with/slash\")\n\t})\n\te.GET(\"/:id\", func(c *Context) error {\n\t\treturn c.String(http.StatusOK, c.Param(\"id\"))\n\t})\n\n\tvar testCases = []struct {\n\t\tname         string\n\t\twhenURL      string\n\t\texpectURL    string\n\t\texpectStatus int\n\t}{\n\t\t{\n\t\t\tname:         \"url with encoding is not decoded for routing\",\n\t\t\twhenURL:      \"/with%2Fslash\",\n\t\t\texpectURL:    \"with%2Fslash\", // `%2F` is not decoded to `/` for routing\n\t\t\texpectStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:         \"url without encoding is used as is\",\n\t\t\twhenURL:      \"/with/slash\",\n\t\t\texpectURL:    \"/with/slash\",\n\t\t\texpectStatus: http.StatusOK,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)\n\t\t\trec := httptest.NewRecorder()\n\n\t\t\te.ServeHTTP(rec, req)\n\n\t\t\tassert.Equal(t, tc.expectStatus, rec.Code)\n\t\t\tassert.Equal(t, tc.expectURL, rec.Body.String())\n\t\t})\n\t}\n}\n\nfunc TestEchoGroup(t *testing.T) {\n\te := New()\n\tbuf := new(bytes.Buffer)\n\te.Use(MiddlewareFunc(func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\tbuf.WriteString(\"0\")\n\t\t\treturn next(c)\n\t\t}\n\t}))\n\th := func(c *Context) error {\n\t\treturn c.NoContent(http.StatusOK)\n\t}\n\n\t//--------\n\t// Routes\n\t//--------\n\n\te.GET(\"/users\", h)\n\n\t// Group\n\tg1 := e.Group(\"/group1\")\n\tg1.Use(func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\tbuf.WriteString(\"1\")\n\t\t\treturn next(c)\n\t\t}\n\t})\n\tg1.GET(\"\", h)\n\n\t// Nested groups with middleware\n\tg2 := e.Group(\"/group2\")\n\tg2.Use(func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\tbuf.WriteString(\"2\")\n\t\t\treturn next(c)\n\t\t}\n\t})\n\tg3 := g2.Group(\"/group3\")\n\tg3.Use(func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\tbuf.WriteString(\"3\")\n\t\t\treturn next(c)\n\t\t}\n\t})\n\tg3.GET(\"\", h)\n\n\trequest(http.MethodGet, \"/users\", e)\n\tassert.Equal(t, \"0\", buf.String())\n\n\tbuf.Reset()\n\trequest(http.MethodGet, \"/group1\", e)\n\tassert.Equal(t, \"01\", buf.String())\n\n\tbuf.Reset()\n\trequest(http.MethodGet, \"/group2/group3\", e)\n\tassert.Equal(t, \"023\", buf.String())\n}\n\nfunc TestEcho_RouteNotFound(t *testing.T) {\n\tvar testCases = []struct {\n\t\texpectRoute any\n\t\tname        string\n\t\twhenURL     string\n\t\texpectCode  int\n\t}{\n\t\t{\n\t\t\tname:        \"404, route to static not found handler /a/c/xx\",\n\t\t\twhenURL:     \"/a/c/xx\",\n\t\t\texpectRoute: \"GET /a/c/xx\",\n\t\t\texpectCode:  http.StatusNotFound,\n\t\t},\n\t\t{\n\t\t\tname:        \"404, route to path param not found handler /a/:file\",\n\t\t\twhenURL:     \"/a/echo.exe\",\n\t\t\texpectRoute: \"GET /a/:file\",\n\t\t\texpectCode:  http.StatusNotFound,\n\t\t},\n\t\t{\n\t\t\tname:        \"404, route to any not found handler /*\",\n\t\t\twhenURL:     \"/b/echo.exe\",\n\t\t\texpectRoute: \"GET /*\",\n\t\t\texpectCode:  http.StatusNotFound,\n\t\t},\n\t\t{\n\t\t\tname:        \"200, route /a/c/df to /a/c/df\",\n\t\t\twhenURL:     \"/a/c/df\",\n\t\t\texpectRoute: \"GET /a/c/df\",\n\t\t\texpectCode:  http.StatusOK,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\n\t\t\tokHandler := func(c *Context) error {\n\t\t\t\treturn c.String(http.StatusOK, c.Request().Method+\" \"+c.Path())\n\t\t\t}\n\t\t\tnotFoundHandler := func(c *Context) error {\n\t\t\t\treturn c.String(http.StatusNotFound, c.Request().Method+\" \"+c.Path())\n\t\t\t}\n\n\t\t\te.GET(\"/\", okHandler)\n\t\t\te.GET(\"/a/c/df\", okHandler)\n\t\t\te.GET(\"/a/b*\", okHandler)\n\t\t\te.PUT(\"/*\", okHandler)\n\n\t\t\te.RouteNotFound(\"/a/c/xx\", notFoundHandler)  // static\n\t\t\te.RouteNotFound(\"/a/:file\", notFoundHandler) // param\n\t\t\te.RouteNotFound(\"/*\", notFoundHandler)       // any\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)\n\t\t\trec := httptest.NewRecorder()\n\n\t\t\te.ServeHTTP(rec, req)\n\n\t\t\tassert.Equal(t, tc.expectCode, rec.Code)\n\t\t\tassert.Equal(t, tc.expectRoute, rec.Body.String())\n\t\t})\n\t}\n}\n\nfunc TestEchoNotFound(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, \"/files\", nil)\n\trec := httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\tassert.Equal(t, http.StatusNotFound, rec.Code)\n}\n\nfunc TestEchoMethodNotAllowed(t *testing.T) {\n\te := New()\n\n\te.GET(\"/\", func(c *Context) error {\n\t\treturn c.String(http.StatusOK, \"Echo!\")\n\t})\n\treq := httptest.NewRequest(http.MethodPost, \"/\", nil)\n\trec := httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\n\tassert.Equal(t, http.StatusMethodNotAllowed, rec.Code)\n\tassert.Equal(t, \"OPTIONS, GET\", rec.Header().Get(HeaderAllow))\n}\n\nfunc TestEcho_OnAddRoute(t *testing.T) {\n\texampleRoute := Route{\n\t\tMethod:      http.MethodGet,\n\t\tPath:        \"/api/files/:id\",\n\t\tHandler:     notFoundHandler,\n\t\tMiddlewares: nil,\n\t\tName:        \"x\",\n\t}\n\n\tvar testCases = []struct {\n\t\twhenRoute   Route\n\t\twhenError   error\n\t\tname        string\n\t\texpectError string\n\t\texpectAdded []string\n\t\texpectLen   int\n\t}{\n\t\t{\n\t\t\tname:        \"ok\",\n\t\t\twhenRoute:   exampleRoute,\n\t\t\twhenError:   nil,\n\t\t\texpectAdded: []string{\"/static\", \"/api/files/:id\"},\n\t\t\texpectError: \"\",\n\t\t\texpectLen:   2,\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, error is returned\",\n\t\t\twhenRoute:   exampleRoute,\n\t\t\twhenError:   errors.New(\"nope\"),\n\t\t\texpectAdded: []string{\"/static\"},\n\t\t\texpectError: \"nope\",\n\t\t\texpectLen:   1,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\n\t\t\te := New()\n\n\t\t\tadded := make([]string, 0)\n\t\t\tcnt := 0\n\t\t\te.OnAddRoute = func(route Route) error {\n\t\t\t\tif cnt > 0 && tc.whenError != nil { // we want to GET /static to succeed for nok tests\n\t\t\t\t\treturn tc.whenError\n\t\t\t\t}\n\t\t\t\tcnt++\n\t\t\t\tadded = append(added, route.Path)\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\te.GET(\"/static\", notFoundHandler)\n\n\t\t\tvar err error\n\t\t\t_, err = e.AddRoute(tc.whenRoute)\n\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\n\t\t\tassert.Len(t, e.Router().Routes(), tc.expectLen)\n\t\t\tassert.Equal(t, tc.expectAdded, added)\n\t\t})\n\t}\n}\n\nfunc TestEchoContext(t *testing.T) {\n\te := New()\n\tc := e.AcquireContext()\n\tassert.IsType(t, new(Context), c)\n\te.ReleaseContext(c)\n}\n\nfunc TestPreMiddlewares(t *testing.T) {\n\te := New()\n\tassert.Equal(t, 0, len(e.PreMiddlewares()))\n\n\te.Pre(func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\treturn next(c)\n\t\t}\n\t})\n\n\tassert.Equal(t, 1, len(e.PreMiddlewares()))\n}\n\nfunc TestMiddlewares(t *testing.T) {\n\te := New()\n\tassert.Equal(t, 0, len(e.Middlewares()))\n\n\te.Use(func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\treturn next(c)\n\t\t}\n\t})\n\n\tassert.Equal(t, 1, len(e.Middlewares()))\n}\n\nfunc TestEcho_Start(t *testing.T) {\n\te := New()\n\te.GET(\"/\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\trndPort, err := net.Listen(\"tcp\", \":0\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer rndPort.Close()\n\terrChan := make(chan error, 1)\n\tgo func() {\n\t\terrChan <- e.Start(rndPort.Addr().String())\n\t}()\n\n\tselect {\n\tcase <-time.After(250 * time.Millisecond):\n\t\tt.Fatal(\"start did not error out\")\n\tcase err := <-errChan:\n\t\texpectContains := \"bind: address already in use\"\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\texpectContains = \"bind: Only one usage of each socket address\"\n\t\t}\n\t\tassert.Contains(t, err.Error(), expectContains)\n\t}\n}\n\nfunc request(method, path string, e *Echo) (int, string) {\n\treq := httptest.NewRequest(method, path, nil)\n\trec := httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\treturn rec.Code, rec.Body.String()\n}\n\ntype customError struct {\n\tCode    int\n\tMessage string\n}\n\nfunc (ce *customError) StatusCode() int {\n\treturn ce.Code\n}\n\nfunc (ce *customError) MarshalJSON() ([]byte, error) {\n\treturn []byte(fmt.Sprintf(`{\"x\":\"%v\"}`, ce.Message)), nil\n}\n\nfunc (ce *customError) Error() string {\n\treturn ce.Message\n}\n\nfunc TestDefaultHTTPErrorHandler(t *testing.T) {\n\tvar testCases = []struct {\n\t\twhenError        error\n\t\tname             string\n\t\twhenMethod       string\n\t\texpectBody       string\n\t\texpectLogged     string\n\t\texpectStatus     int\n\t\tgivenExposeError bool\n\t\tgivenLoggerFunc  bool\n\t}{\n\t\t{\n\t\t\tname:             \"ok, expose error = true, HTTPError, no wrapped err\",\n\t\t\tgivenExposeError: true,\n\t\t\twhenError:        &HTTPError{Code: http.StatusTeapot, Message: \"my_error\"},\n\t\t\texpectStatus:     http.StatusTeapot,\n\t\t\texpectBody:       `{\"message\":\"my_error\"}` + \"\\n\",\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, expose error = true, HTTPError + wrapped error\",\n\t\t\tgivenExposeError: true,\n\t\t\twhenError:        HTTPError{Code: http.StatusTeapot, Message: \"my_error\"}.Wrap(errors.New(\"internal_error\")),\n\t\t\texpectStatus:     http.StatusTeapot,\n\t\t\texpectBody:       `{\"error\":\"internal_error\",\"message\":\"my_error\"}` + \"\\n\",\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, expose error = true, HTTPError + wrapped HTTPError\",\n\t\t\tgivenExposeError: true,\n\t\t\twhenError:        HTTPError{Code: http.StatusTeapot, Message: \"my_error\"}.Wrap(&HTTPError{Code: http.StatusTeapot, Message: \"early_error\"}),\n\t\t\texpectStatus:     http.StatusTeapot,\n\t\t\texpectBody:       `{\"error\":\"code=418, message=early_error\",\"message\":\"my_error\"}` + \"\\n\",\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, expose error = false, HTTPError\",\n\t\t\twhenError:    &HTTPError{Code: http.StatusTeapot, Message: \"my_error\"},\n\t\t\texpectStatus: http.StatusTeapot,\n\t\t\texpectBody:   `{\"message\":\"my_error\"}` + \"\\n\",\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, expose error = false, HTTPError, no message\",\n\t\t\twhenError:    &HTTPError{Code: http.StatusTeapot, Message: \"\"},\n\t\t\texpectStatus: http.StatusTeapot,\n\t\t\texpectBody:   `{\"message\":\"I'm a teapot\"}` + \"\\n\",\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, expose error = false, HTTPError + internal HTTPError\",\n\t\t\twhenError:    HTTPError{Code: http.StatusTooEarly, Message: \"my_error\"}.Wrap(&HTTPError{Code: http.StatusTeapot, Message: \"early_error\"}),\n\t\t\texpectStatus: http.StatusTooEarly,\n\t\t\texpectBody:   `{\"message\":\"my_error\"}` + \"\\n\",\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, expose error = true, Error\",\n\t\t\tgivenExposeError: true,\n\t\t\twhenError:        fmt.Errorf(\"my errors wraps: %w\", errors.New(\"internal_error\")),\n\t\t\texpectStatus:     http.StatusInternalServerError,\n\t\t\texpectBody:       `{\"error\":\"my errors wraps: internal_error\",\"message\":\"Internal Server Error\"}` + \"\\n\",\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, expose error = false, Error\",\n\t\t\twhenError:    fmt.Errorf(\"my errors wraps: %w\", errors.New(\"internal_error\")),\n\t\t\texpectStatus: http.StatusInternalServerError,\n\t\t\texpectBody:   `{\"message\":\"Internal Server Error\"}` + \"\\n\",\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, http.HEAD, expose error = true, Error\",\n\t\t\tgivenExposeError: true,\n\t\t\twhenMethod:       http.MethodHead,\n\t\t\twhenError:        fmt.Errorf(\"my errors wraps: %w\", errors.New(\"internal_error\")),\n\t\t\texpectStatus:     http.StatusInternalServerError,\n\t\t\texpectBody:       ``,\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, custom error implement MarshalJSON + HTTPStatusCoder\",\n\t\t\twhenMethod:   http.MethodGet,\n\t\t\twhenError:    &customError{Code: http.StatusTeapot, Message: \"custom error msg\"},\n\t\t\texpectStatus: http.StatusTeapot,\n\t\t\texpectBody:   `{\"x\":\"custom error msg\"}` + \"\\n\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tbuf := new(bytes.Buffer)\n\t\t\te := New()\n\t\t\te.Logger = slog.New(slog.DiscardHandler)\n\t\t\te.Any(\"/path\", func(c *Context) error {\n\t\t\t\treturn tc.whenError\n\t\t\t})\n\n\t\t\te.HTTPErrorHandler = DefaultHTTPErrorHandler(tc.givenExposeError)\n\n\t\t\tmethod := http.MethodGet\n\t\t\tif tc.whenMethod != \"\" {\n\t\t\t\tmethod = tc.whenMethod\n\t\t\t}\n\t\t\tc, b := request(method, \"/path\", e)\n\n\t\t\tassert.Equal(t, tc.expectStatus, c)\n\t\t\tassert.Equal(t, tc.expectBody, b)\n\t\t\tassert.Equal(t, tc.expectLogged, buf.String())\n\t\t})\n\t}\n}\n\nfunc TestDefaultHTTPErrorHandler_CommitedResponse(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\tresp := httptest.NewRecorder()\n\tc := e.NewContext(req, resp)\n\n\tc.orgResponse.Committed = true\n\terrHandler := DefaultHTTPErrorHandler(false)\n\n\terrHandler(c, errors.New(\"my_error\"))\n\tassert.Equal(t, http.StatusOK, resp.Code)\n}\n\nfunc benchmarkEchoRoutes(b *testing.B, routes []testRoute) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\tu := req.URL\n\tw := httptest.NewRecorder()\n\n\tb.ReportAllocs()\n\n\t// Add routes\n\tfor _, route := range routes {\n\t\te.Add(route.Method, route.Path, func(c *Context) error {\n\t\t\treturn nil\n\t\t})\n\t}\n\n\t// Find routes\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tfor _, route := range routes {\n\t\t\treq.Method = route.Method\n\t\t\tu.Path = route.Path\n\t\t\te.ServeHTTP(w, req)\n\t\t}\n\t}\n}\n\nfunc BenchmarkEchoStaticRoutes(b *testing.B) {\n\tbenchmarkEchoRoutes(b, staticRoutes)\n}\n\nfunc BenchmarkEchoStaticRoutesMisses(b *testing.B) {\n\tbenchmarkEchoRoutes(b, staticRoutes)\n}\n\nfunc BenchmarkEchoGitHubAPI(b *testing.B) {\n\tbenchmarkEchoRoutes(b, gitHubAPI)\n}\n\nfunc BenchmarkEchoGitHubAPIMisses(b *testing.B) {\n\tbenchmarkEchoRoutes(b, gitHubAPI)\n}\n\nfunc BenchmarkEchoParseAPI(b *testing.B) {\n\tbenchmarkEchoRoutes(b, parseAPI)\n}\n"
  },
  {
    "path": "echotest/context.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echotest\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\n// ContextConfig is configuration for creating echo.Context for testing purposes.\ntype ContextConfig struct {\n\t// Request will be used instead of default `httptest.NewRequest(http.MethodGet, \"/\", nil)`\n\tRequest *http.Request\n\n\t// Response will be used instead of default `httptest.NewRecorder()`\n\tResponse *httptest.ResponseRecorder\n\n\t// QueryValues will be set as Request.URL.RawQuery value\n\tQueryValues url.Values\n\n\t// Headers will be set as Request.Header value\n\tHeaders http.Header\n\n\t// PathValues initializes context.PathValues with given value.\n\tPathValues echo.PathValues\n\n\t// RouteInfo initializes context.RouteInfo() with given value\n\tRouteInfo *echo.RouteInfo\n\n\t// FormValues creates form-urlencoded form out of given values. If there is no\n\t// `content-type` header it will be set to `application/x-www-form-urlencoded`\n\t// In case Request was not set the Request.Method is set to `POST`\n\t//\n\t// FormValues, MultipartForm and JSONBody are mutually exclusive.\n\tFormValues url.Values\n\n\t// MultipartForm creates multipart form out of given value. If there is no\n\t// `content-type` header it will be set to `multipart/form-data`\n\t// In case Request was not set the Request.Method is set to `POST`\n\t//\n\t// FormValues, MultipartForm and JSONBody are mutually exclusive.\n\tMultipartForm *MultipartForm\n\n\t// JSONBody creates JSON body out of given bytes. If there is no\n\t// `content-type` header it will be set to `application/json`\n\t// In case Request was not set the Request.Method is set to `POST`\n\t//\n\t// FormValues, MultipartForm and JSONBody are mutually exclusive.\n\tJSONBody []byte\n}\n\n// MultipartForm is used to create multipart form out of given value\ntype MultipartForm struct {\n\tFields map[string]string\n\tFiles  []MultipartFormFile\n}\n\n// MultipartFormFile is used to create file in multipart form out of given value\ntype MultipartFormFile struct {\n\tFieldname string\n\tFilename  string\n\tContent   []byte\n}\n\n// ToContext converts ContextConfig to echo.Context\nfunc (conf ContextConfig) ToContext(t *testing.T) *echo.Context {\n\tc, _ := conf.ToContextRecorder(t)\n\treturn c\n}\n\n// ToContextRecorder converts ContextConfig to echo.Context and httptest.ResponseRecorder\nfunc (conf ContextConfig) ToContextRecorder(t *testing.T) (*echo.Context, *httptest.ResponseRecorder) {\n\tif conf.Response == nil {\n\t\tconf.Response = httptest.NewRecorder()\n\t}\n\tisDefaultRequest := false\n\tif conf.Request == nil {\n\t\tisDefaultRequest = true\n\t\tconf.Request = httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t}\n\n\tif len(conf.QueryValues) > 0 {\n\t\tconf.Request.URL.RawQuery = conf.QueryValues.Encode()\n\t}\n\tif len(conf.Headers) > 0 {\n\t\tconf.Request.Header = conf.Headers\n\t}\n\tif len(conf.FormValues) > 0 {\n\t\tbody := strings.NewReader(url.Values(conf.FormValues).Encode())\n\t\tconf.Request.Body = io.NopCloser(body)\n\t\tconf.Request.ContentLength = int64(body.Len())\n\n\t\tif conf.Request.Header.Get(echo.HeaderContentType) == \"\" {\n\t\t\tconf.Request.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm)\n\t\t}\n\t\tif isDefaultRequest {\n\t\t\tconf.Request.Method = http.MethodPost\n\t\t}\n\t} else if conf.MultipartForm != nil {\n\t\tvar body bytes.Buffer\n\t\tmw := multipart.NewWriter(&body)\n\t\tfor field, value := range conf.MultipartForm.Fields {\n\t\t\tif err := mw.WriteField(field, value); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t}\n\t\tfor _, file := range conf.MultipartForm.Files {\n\t\t\tfw, err := mw.CreateFormFile(file.Fieldname, file.Filename)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif _, err = fw.Write(file.Content); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t}\n\t\tif err := mw.Close(); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tconf.Request.Body = io.NopCloser(&body)\n\t\tconf.Request.ContentLength = int64(body.Len())\n\t\tif conf.Request.Header.Get(echo.HeaderContentType) == \"\" {\n\t\t\tconf.Request.Header.Set(echo.HeaderContentType, mw.FormDataContentType())\n\t\t}\n\t\tif isDefaultRequest {\n\t\t\tconf.Request.Method = http.MethodPost\n\t\t}\n\t} else if conf.JSONBody != nil {\n\t\tbody := bytes.NewReader(conf.JSONBody)\n\t\tconf.Request.Body = io.NopCloser(body)\n\t\tconf.Request.ContentLength = int64(body.Len())\n\n\t\tif conf.Request.Header.Get(echo.HeaderContentType) == \"\" {\n\t\t\tconf.Request.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)\n\t\t}\n\t\tif isDefaultRequest {\n\t\t\tconf.Request.Method = http.MethodPost\n\t\t}\n\t}\n\n\tec := echo.NewContext(conf.Request, conf.Response, echo.New())\n\tif conf.RouteInfo == nil {\n\t\tconf.RouteInfo = &echo.RouteInfo{\n\t\t\tName:       \"\",\n\t\t\tMethod:     conf.Request.Method,\n\t\t\tPath:       \"/test\",\n\t\t\tParameters: []string{},\n\t\t}\n\t\tfor _, p := range conf.PathValues {\n\t\t\tconf.RouteInfo.Parameters = append(conf.RouteInfo.Parameters, p.Name)\n\t\t}\n\t}\n\tec.InitializeRoute(conf.RouteInfo, &conf.PathValues)\n\treturn ec, conf.Response\n}\n\n// ServeWithHandler serves ContextConfig with given handler and returns httptest.ResponseRecorder for response checking\nfunc (conf ContextConfig) ServeWithHandler(t *testing.T, handler echo.HandlerFunc, opts ...any) *httptest.ResponseRecorder {\n\tc, rec := conf.ToContextRecorder(t)\n\n\terrHandler := echo.DefaultHTTPErrorHandler(false)\n\tfor _, opt := range opts {\n\t\tswitch o := opt.(type) {\n\t\tcase echo.HTTPErrorHandler:\n\t\t\terrHandler = o\n\t\t}\n\t}\n\n\terr := handler(c)\n\tif err != nil {\n\t\terrHandler(c, err)\n\t}\n\treturn rec\n}\n"
  },
  {
    "path": "echotest/context_external_test.go",
    "content": "package echotest_test\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/labstack/echo/v5/echotest\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestToContext_JSONBody(t *testing.T) {\n\tc := echotest.ContextConfig{\n\t\tJSONBody: echotest.LoadBytes(t, \"testdata/test.json\"),\n\t}.ToContext(t)\n\n\tpayload := struct {\n\t\tField string `json:\"field\"`\n\t}{}\n\tif err := c.Bind(&payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tassert.Equal(t, \"value\", payload.Field)\n\tassert.Equal(t, http.MethodPost, c.Request().Method)\n\tassert.Equal(t, echo.MIMEApplicationJSON, c.Request().Header.Get(echo.HeaderContentType))\n}\n"
  },
  {
    "path": "echotest/context_test.go",
    "content": "package echotest\n\nimport (\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestServeWithHandler(t *testing.T) {\n\thandler := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, c.QueryParam(\"key\"))\n\t}\n\ttestConf := ContextConfig{\n\t\tQueryValues: url.Values{\"key\": []string{\"value\"}},\n\t}\n\n\tresp := testConf.ServeWithHandler(t, handler)\n\n\tassert.Equal(t, http.StatusOK, resp.Code)\n\tassert.Equal(t, \"value\", resp.Body.String())\n}\n\nfunc TestServeWithHandler_error(t *testing.T) {\n\thandler := func(c *echo.Context) error {\n\t\treturn echo.NewHTTPError(http.StatusBadRequest, \"something went wrong\")\n\t}\n\ttestConf := ContextConfig{\n\t\tQueryValues: url.Values{\"key\": []string{\"value\"}},\n\t}\n\n\tcustomErrHandler := echo.DefaultHTTPErrorHandler(true)\n\n\tresp := testConf.ServeWithHandler(t, handler, customErrHandler)\n\n\tassert.Equal(t, http.StatusBadRequest, resp.Code)\n\tassert.Equal(t, `{\"message\":\"something went wrong\"}`+\"\\n\", resp.Body.String())\n}\n\nfunc TestToContext_QueryValues(t *testing.T) {\n\ttestConf := ContextConfig{\n\t\tQueryValues: url.Values{\"t\": []string{\"2006-01-02\"}},\n\t}\n\tc := testConf.ToContext(t)\n\n\tv, err := echo.QueryParam[string](c, \"t\")\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"2006-01-02\", v)\n}\n\nfunc TestToContext_Headers(t *testing.T) {\n\ttestConf := ContextConfig{\n\t\tHeaders: http.Header{echo.HeaderXRequestID: []string{\"ABC\"}},\n\t}\n\tc := testConf.ToContext(t)\n\n\tid := c.Request().Header.Get(echo.HeaderXRequestID)\n\n\tassert.Equal(t, \"ABC\", id)\n}\n\nfunc TestToContext_PathValues(t *testing.T) {\n\ttestConf := ContextConfig{\n\t\tPathValues: echo.PathValues{{\n\t\t\tName:  \"key\",\n\t\t\tValue: \"value\",\n\t\t}},\n\t}\n\tc := testConf.ToContext(t)\n\n\tkey := c.Param(\"key\")\n\n\tassert.Equal(t, \"value\", key)\n}\n\nfunc TestToContext_RouteInfo(t *testing.T) {\n\ttestConf := ContextConfig{\n\t\tRouteInfo: &echo.RouteInfo{\n\t\t\tName:       \"my_route\",\n\t\t\tMethod:     http.MethodGet,\n\t\t\tPath:       \"/:id\",\n\t\t\tParameters: []string{\"id\"},\n\t\t},\n\t}\n\tc := testConf.ToContext(t)\n\n\tri := c.RouteInfo()\n\n\tassert.Equal(t, echo.RouteInfo{\n\t\tName:       \"my_route\",\n\t\tMethod:     http.MethodGet,\n\t\tPath:       \"/:id\",\n\t\tParameters: []string{\"id\"},\n\t}, ri)\n}\n\nfunc TestToContext_FormValues(t *testing.T) {\n\ttestConf := ContextConfig{\n\t\tFormValues: url.Values{\"key\": []string{\"value\"}},\n\t}\n\tc := testConf.ToContext(t)\n\n\tassert.Equal(t, \"value\", c.FormValue(\"key\"))\n\tassert.Equal(t, http.MethodPost, c.Request().Method)\n\tassert.Equal(t, echo.MIMEApplicationForm, c.Request().Header.Get(echo.HeaderContentType))\n}\n\nfunc TestToContext_MultipartForm(t *testing.T) {\n\ttestConf := ContextConfig{\n\t\tMultipartForm: &MultipartForm{\n\t\t\tFields: map[string]string{\n\t\t\t\t\"key\": \"value\",\n\t\t\t},\n\t\t\tFiles: []MultipartFormFile{\n\t\t\t\t{\n\t\t\t\t\tFieldname: \"file\",\n\t\t\t\t\tFilename:  \"test.json\",\n\t\t\t\t\tContent:   LoadBytes(t, \"testdata/test.json\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tc := testConf.ToContext(t)\n\n\tassert.Equal(t, \"value\", c.FormValue(\"key\"))\n\tassert.Equal(t, http.MethodPost, c.Request().Method)\n\tassert.Equal(t, true, strings.HasPrefix(c.Request().Header.Get(echo.HeaderContentType), \"multipart/form-data; boundary=\"))\n\n\tfv, err := c.FormFile(\"file\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tassert.Equal(t, \"test.json\", fv.Filename)\n\tassert.Equal(t, int64(23), fv.Size)\n}\n\nfunc TestToContext_JSONBody(t *testing.T) {\n\ttestConf := ContextConfig{\n\t\tJSONBody: LoadBytes(t, \"testdata/test.json\"),\n\t}\n\tc := testConf.ToContext(t)\n\n\tpayload := struct {\n\t\tField string `json:\"field\"`\n\t}{}\n\tif err := c.Bind(&payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tassert.Equal(t, \"value\", payload.Field)\n\tassert.Equal(t, http.MethodPost, c.Request().Method)\n\tassert.Equal(t, echo.MIMEApplicationJSON, c.Request().Header.Get(echo.HeaderContentType))\n}\n"
  },
  {
    "path": "echotest/reader.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echotest\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n)\n\ntype loadBytesOpts func([]byte) []byte\n\n// TrimNewlineEnd instructs LoadBytes to remove `\\n` from the end of loaded file.\nfunc TrimNewlineEnd(bytes []byte) []byte {\n\tbLen := len(bytes)\n\tif bLen > 1 && bytes[bLen-1] == '\\n' {\n\t\tbytes = bytes[:bLen-1]\n\t}\n\treturn bytes\n}\n\n// LoadBytes is helper to load file contents relative to current (where test file is) package\n// directory.\nfunc LoadBytes(t *testing.T, name string, opts ...loadBytesOpts) []byte {\n\tbytes := loadBytes(t, name, 2)\n\n\tfor _, f := range opts {\n\t\tbytes = f(bytes)\n\t}\n\n\treturn bytes\n}\n\nfunc loadBytes(t *testing.T, name string, callDepth int) []byte {\n\t_, b, _, _ := runtime.Caller(callDepth)\n\tbasepath := filepath.Dir(b)\n\n\tpath := filepath.Join(basepath, name) // relative path\n\tbytes, err := os.ReadFile(path)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn bytes[:]\n}\n"
  },
  {
    "path": "echotest/reader_external_test.go",
    "content": "package echotest_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v5/echotest\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nconst testJSONContent = `{\n  \"field\": \"value\"\n}`\n\nfunc TestLoadBytesOK(t *testing.T) {\n\tdata := echotest.LoadBytes(t, \"testdata/test.json\")\n\tassert.Equal(t, []byte(testJSONContent+\"\\n\"), data)\n}\n\nfunc TestLoadBytes_custom(t *testing.T) {\n\tdata := echotest.LoadBytes(t, \"testdata/test.json\", func(bytes []byte) []byte {\n\t\treturn []byte(strings.ToUpper(string(bytes)))\n\t})\n\tassert.Equal(t, []byte(strings.ToUpper(testJSONContent)+\"\\n\"), data)\n}\n"
  },
  {
    "path": "echotest/reader_test.go",
    "content": "package echotest\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nconst testJSONContent = `{\n  \"field\": \"value\"\n}`\n\nfunc TestLoadBytesOK(t *testing.T) {\n\tdata := LoadBytes(t, \"testdata/test.json\")\n\tassert.Equal(t, []byte(testJSONContent+\"\\n\"), data)\n}\n\nfunc TestLoadBytesOK_TrimNewlineEnd(t *testing.T) {\n\tdata := LoadBytes(t, \"testdata/test.json\", TrimNewlineEnd)\n\tassert.Equal(t, []byte(testJSONContent), data)\n}\n"
  },
  {
    "path": "echotest/testdata/test.json",
    "content": "{\n  \"field\": \"value\"\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/labstack/echo/v5\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/stretchr/testify v1.11.1\n\tgolang.org/x/net v0.49.0\n\tgolang.org/x/time v0.14.0\n)\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgolang.org/x/text v0.33.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngolang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=\ngolang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=\ngolang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=\ngolang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=\ngolang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=\ngolang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "group.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"io/fs\"\n\t\"net/http\"\n)\n\n// Group is a set of sub-routes for a specified route. It can be used for inner\n// routes that share a common middleware or functionality that should be separate\n// from the parent echo instance while still inheriting from it.\ntype Group struct {\n\techo       *Echo\n\tprefix     string\n\tmiddleware []MiddlewareFunc\n}\n\n// Use implements `Echo#Use()` for sub-routes within the Group.\n// Group middlewares are not executed on request when there is no matching route found.\nfunc (g *Group) Use(middleware ...MiddlewareFunc) {\n\tg.middleware = append(g.middleware, middleware...)\n}\n\n// CONNECT implements `Echo#CONNECT()` for sub-routes within the Group. Panics on error.\nfunc (g *Group) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {\n\treturn g.Add(http.MethodConnect, path, h, m...)\n}\n\n// DELETE implements `Echo#DELETE()` for sub-routes within the Group. Panics on error.\nfunc (g *Group) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {\n\treturn g.Add(http.MethodDelete, path, h, m...)\n}\n\n// GET implements `Echo#GET()` for sub-routes within the Group. Panics on error.\nfunc (g *Group) GET(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {\n\treturn g.Add(http.MethodGet, path, h, m...)\n}\n\n// HEAD implements `Echo#HEAD()` for sub-routes within the Group. Panics on error.\nfunc (g *Group) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {\n\treturn g.Add(http.MethodHead, path, h, m...)\n}\n\n// OPTIONS implements `Echo#OPTIONS()` for sub-routes within the Group. Panics on error.\nfunc (g *Group) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {\n\treturn g.Add(http.MethodOptions, path, h, m...)\n}\n\n// PATCH implements `Echo#PATCH()` for sub-routes within the Group. Panics on error.\nfunc (g *Group) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {\n\treturn g.Add(http.MethodPatch, path, h, m...)\n}\n\n// POST implements `Echo#POST()` for sub-routes within the Group. Panics on error.\nfunc (g *Group) POST(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {\n\treturn g.Add(http.MethodPost, path, h, m...)\n}\n\n// PUT implements `Echo#PUT()` for sub-routes within the Group. Panics on error.\nfunc (g *Group) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {\n\treturn g.Add(http.MethodPut, path, h, m...)\n}\n\n// TRACE implements `Echo#TRACE()` for sub-routes within the Group. Panics on error.\nfunc (g *Group) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {\n\treturn g.Add(http.MethodTrace, path, h, m...)\n}\n\n// Any implements `Echo#Any()` for sub-routes within the Group. Panics on error.\nfunc (g *Group) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) RouteInfo {\n\treturn g.Add(RouteAny, path, handler, middleware...)\n}\n\n// Match implements `Echo#Match()` for sub-routes within the Group. Panics on error.\nfunc (g *Group) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) Routes {\n\terrs := make([]error, 0)\n\tris := make(Routes, 0)\n\tfor _, m := range methods {\n\t\tri, err := g.AddRoute(Route{\n\t\t\tMethod:      m,\n\t\t\tPath:        path,\n\t\t\tHandler:     handler,\n\t\t\tMiddlewares: middleware,\n\t\t})\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t\tcontinue\n\t\t}\n\t\tris = append(ris, ri)\n\t}\n\tif len(errs) > 0 {\n\t\tpanic(errs) // this is how `v4` handles errors. `v5` has methods to have panic-free usage\n\t}\n\treturn ris\n}\n\n// Group creates a new sub-group with prefix and optional sub-group-level middleware.\n// Important! Group middlewares are only executed in case there was exact route match and not\n// for 404 (not found) or 405 (method not allowed) cases. If this kind of behaviour is needed then add\n// a catch-all route `/*` for the group which handler returns always 404\nfunc (g *Group) Group(prefix string, middleware ...MiddlewareFunc) (sg *Group) {\n\tm := make([]MiddlewareFunc, 0, len(g.middleware)+len(middleware))\n\tm = append(m, g.middleware...)\n\tm = append(m, middleware...)\n\tsg = g.echo.Group(g.prefix+prefix, m...)\n\treturn\n}\n\n// Static implements `Echo#Static()` for sub-routes within the Group.\nfunc (g *Group) Static(pathPrefix, fsRoot string, middleware ...MiddlewareFunc) RouteInfo {\n\tsubFs := MustSubFS(g.echo.Filesystem, fsRoot)\n\treturn g.StaticFS(pathPrefix, subFs, middleware...)\n}\n\n// StaticFS implements `Echo#StaticFS()` for sub-routes within the Group.\n//\n// When dealing with `embed.FS` use `fs := echo.MustSubFS(fs, \"rootDirectory\") to create sub fs which uses necessary\n// prefix for directory path. This is necessary as `//go:embed assets/images` embeds files with paths\n// including `assets/images` as their prefix.\nfunc (g *Group) StaticFS(pathPrefix string, filesystem fs.FS, middleware ...MiddlewareFunc) RouteInfo {\n\treturn g.Add(\n\t\thttp.MethodGet,\n\t\tpathPrefix+\"*\",\n\t\tStaticDirectoryHandler(filesystem, false),\n\t\tmiddleware...,\n\t)\n}\n\n// FileFS implements `Echo#FileFS()` for sub-routes within the Group.\nfunc (g *Group) FileFS(path, file string, filesystem fs.FS, m ...MiddlewareFunc) RouteInfo {\n\treturn g.GET(path, StaticFileHandler(file, filesystem), m...)\n}\n\n// File implements `Echo#File()` for sub-routes within the Group. Panics on error.\nfunc (g *Group) File(path, file string, middleware ...MiddlewareFunc) RouteInfo {\n\thandler := func(c *Context) error {\n\t\treturn c.File(file)\n\t}\n\treturn g.Add(http.MethodGet, path, handler, middleware...)\n}\n\n// RouteNotFound implements `Echo#RouteNotFound()` for sub-routes within the Group.\n//\n// Example: `g.RouteNotFound(\"/*\", func(c *echo.Context) error { return c.NoContent(http.StatusNotFound) })`\nfunc (g *Group) RouteNotFound(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {\n\treturn g.Add(RouteNotFound, path, h, m...)\n}\n\n// Add implements `Echo#Add()` for sub-routes within the Group. Panics on error.\nfunc (g *Group) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) RouteInfo {\n\tri, err := g.AddRoute(Route{\n\t\tMethod:      method,\n\t\tPath:        path,\n\t\tHandler:     handler,\n\t\tMiddlewares: middleware,\n\t})\n\tif err != nil {\n\t\tpanic(err) // this is how `v4` handles errors. `v5` has methods to have panic-free usage\n\t}\n\treturn ri\n}\n\n// AddRoute registers a new Routable with Router\nfunc (g *Group) AddRoute(route Route) (RouteInfo, error) {\n\t// Combine middleware into a new slice to avoid accidentally passing the same slice for\n\t// multiple routes, which would lead to later add() calls overwriting the\n\t// middleware from earlier calls.\n\tgroupRoute := route.WithPrefix(g.prefix, append([]MiddlewareFunc{}, g.middleware...))\n\treturn g.echo.add(groupRoute)\n}\n"
  },
  {
    "path": "group_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"io/fs\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGroup_withoutRouteWillNotExecuteMiddleware(t *testing.T) {\n\te := New()\n\n\tcalled := false\n\tmw := func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\tcalled = true\n\t\t\treturn c.NoContent(http.StatusTeapot)\n\t\t}\n\t}\n\t// even though group has middleware it will not be executed when there are no routes under that group\n\t_ = e.Group(\"/group\", mw)\n\n\tstatus, body := request(http.MethodGet, \"/group/nope\", e)\n\tassert.Equal(t, http.StatusNotFound, status)\n\tassert.Equal(t, `{\"message\":\"Not Found\"}`+\"\\n\", body)\n\n\tassert.False(t, called)\n}\n\nfunc TestGroup_withRoutesWillNotExecuteMiddlewareFor404(t *testing.T) {\n\te := New()\n\n\tcalled := false\n\tmw := func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\tcalled = true\n\t\t\treturn c.NoContent(http.StatusTeapot)\n\t\t}\n\t}\n\t// even though group has middleware and routes when we have no match on some route the middlewares for that\n\t// group will not be executed\n\tg := e.Group(\"/group\", mw)\n\tg.GET(\"/yes\", handlerFunc)\n\n\tstatus, body := request(http.MethodGet, \"/group/nope\", e)\n\tassert.Equal(t, http.StatusNotFound, status)\n\tassert.Equal(t, `{\"message\":\"Not Found\"}`+\"\\n\", body)\n\n\tassert.False(t, called)\n}\n\nfunc TestGroup_multiLevelGroup(t *testing.T) {\n\te := New()\n\n\tapi := e.Group(\"/api\")\n\tusers := api.Group(\"/users\")\n\tusers.GET(\"/activate\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\tstatus, body := request(http.MethodGet, \"/api/users/activate\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, `OK`, body)\n}\n\nfunc TestGroupFile(t *testing.T) {\n\te := New()\n\tg := e.Group(\"/group\")\n\tg.File(\"/walle\", \"_fixture/images/walle.png\")\n\texpectedData, err := os.ReadFile(\"_fixture/images/walle.png\")\n\tassert.Nil(t, err)\n\treq := httptest.NewRequest(http.MethodGet, \"/group/walle\", nil)\n\trec := httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\tassert.Equal(t, http.StatusOK, rec.Code)\n\tassert.Equal(t, expectedData, rec.Body.Bytes())\n}\n\nfunc TestGroupRouteMiddleware(t *testing.T) {\n\t// Ensure middleware slices are not re-used\n\te := New()\n\tg := e.Group(\"/group\")\n\th := func(*Context) error { return nil }\n\tm1 := func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\treturn next(c)\n\t\t}\n\t}\n\tm2 := func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\treturn next(c)\n\t\t}\n\t}\n\tm3 := func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\treturn next(c)\n\t\t}\n\t}\n\tm4 := func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\treturn c.NoContent(404)\n\t\t}\n\t}\n\tm5 := func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\treturn c.NoContent(405)\n\t\t}\n\t}\n\tg.Use(m1, m2, m3)\n\tg.GET(\"/404\", h, m4)\n\tg.GET(\"/405\", h, m5)\n\n\tc, _ := request(http.MethodGet, \"/group/404\", e)\n\tassert.Equal(t, 404, c)\n\tc, _ = request(http.MethodGet, \"/group/405\", e)\n\tassert.Equal(t, 405, c)\n}\n\nfunc TestGroupRouteMiddlewareWithMatchAny(t *testing.T) {\n\t// Ensure middleware and match any routes do not conflict\n\te := New()\n\tg := e.Group(\"/group\")\n\tm1 := func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\treturn next(c)\n\t\t}\n\t}\n\tm2 := func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\treturn c.String(http.StatusOK, c.RouteInfo().Path)\n\t\t}\n\t}\n\th := func(c *Context) error {\n\t\treturn c.String(http.StatusOK, c.RouteInfo().Path)\n\t}\n\tg.Use(m1)\n\tg.GET(\"/help\", h, m2)\n\tg.GET(\"/*\", h, m2)\n\tg.GET(\"\", h, m2)\n\te.GET(\"unrelated\", h, m2)\n\te.GET(\"*\", h, m2)\n\n\t_, m := request(http.MethodGet, \"/group/help\", e)\n\tassert.Equal(t, \"/group/help\", m)\n\t_, m = request(http.MethodGet, \"/group/help/other\", e)\n\tassert.Equal(t, \"/group/*\", m)\n\t_, m = request(http.MethodGet, \"/group/404\", e)\n\tassert.Equal(t, \"/group/*\", m)\n\t_, m = request(http.MethodGet, \"/group\", e)\n\tassert.Equal(t, \"/group\", m)\n\t_, m = request(http.MethodGet, \"/other\", e)\n\tassert.Equal(t, \"/*\", m)\n\t_, m = request(http.MethodGet, \"/\", e)\n\tassert.Equal(t, \"/*\", m)\n\n}\n\nfunc TestGroup_CONNECT(t *testing.T) {\n\te := New()\n\n\tusers := e.Group(\"/users\")\n\tri := users.CONNECT(\"/activate\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\tassert.Equal(t, http.MethodConnect, ri.Method)\n\tassert.Equal(t, \"/users/activate\", ri.Path)\n\tassert.Equal(t, http.MethodConnect+\":/users/activate\", ri.Name)\n\tassert.Nil(t, ri.Parameters)\n\n\tstatus, body := request(http.MethodConnect, \"/users/activate\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, `OK`, body)\n}\n\nfunc TestGroup_DELETE(t *testing.T) {\n\te := New()\n\n\tusers := e.Group(\"/users\")\n\tri := users.DELETE(\"/activate\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\tassert.Equal(t, http.MethodDelete, ri.Method)\n\tassert.Equal(t, \"/users/activate\", ri.Path)\n\tassert.Equal(t, http.MethodDelete+\":/users/activate\", ri.Name)\n\tassert.Nil(t, ri.Parameters)\n\n\tstatus, body := request(http.MethodDelete, \"/users/activate\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, `OK`, body)\n}\n\nfunc TestGroup_HEAD(t *testing.T) {\n\te := New()\n\n\tusers := e.Group(\"/users\")\n\tri := users.HEAD(\"/activate\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\tassert.Equal(t, http.MethodHead, ri.Method)\n\tassert.Equal(t, \"/users/activate\", ri.Path)\n\tassert.Equal(t, http.MethodHead+\":/users/activate\", ri.Name)\n\tassert.Nil(t, ri.Parameters)\n\n\tstatus, body := request(http.MethodHead, \"/users/activate\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, `OK`, body)\n}\n\nfunc TestGroup_OPTIONS(t *testing.T) {\n\te := New()\n\n\tusers := e.Group(\"/users\")\n\tri := users.OPTIONS(\"/activate\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\tassert.Equal(t, http.MethodOptions, ri.Method)\n\tassert.Equal(t, \"/users/activate\", ri.Path)\n\tassert.Equal(t, http.MethodOptions+\":/users/activate\", ri.Name)\n\tassert.Nil(t, ri.Parameters)\n\n\tstatus, body := request(http.MethodOptions, \"/users/activate\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, `OK`, body)\n}\n\nfunc TestGroup_PATCH(t *testing.T) {\n\te := New()\n\n\tusers := e.Group(\"/users\")\n\tri := users.PATCH(\"/activate\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\tassert.Equal(t, http.MethodPatch, ri.Method)\n\tassert.Equal(t, \"/users/activate\", ri.Path)\n\tassert.Equal(t, http.MethodPatch+\":/users/activate\", ri.Name)\n\tassert.Nil(t, ri.Parameters)\n\n\tstatus, body := request(http.MethodPatch, \"/users/activate\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, `OK`, body)\n}\n\nfunc TestGroup_POST(t *testing.T) {\n\te := New()\n\n\tusers := e.Group(\"/users\")\n\tri := users.POST(\"/activate\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\tassert.Equal(t, http.MethodPost, ri.Method)\n\tassert.Equal(t, \"/users/activate\", ri.Path)\n\tassert.Equal(t, http.MethodPost+\":/users/activate\", ri.Name)\n\tassert.Nil(t, ri.Parameters)\n\n\tstatus, body := request(http.MethodPost, \"/users/activate\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, `OK`, body)\n}\n\nfunc TestGroup_PUT(t *testing.T) {\n\te := New()\n\n\tusers := e.Group(\"/users\")\n\tri := users.PUT(\"/activate\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\tassert.Equal(t, http.MethodPut, ri.Method)\n\tassert.Equal(t, \"/users/activate\", ri.Path)\n\tassert.Equal(t, http.MethodPut+\":/users/activate\", ri.Name)\n\tassert.Nil(t, ri.Parameters)\n\n\tstatus, body := request(http.MethodPut, \"/users/activate\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, `OK`, body)\n}\n\nfunc TestGroup_TRACE(t *testing.T) {\n\te := New()\n\n\tusers := e.Group(\"/users\")\n\tri := users.TRACE(\"/activate\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\tassert.Equal(t, http.MethodTrace, ri.Method)\n\tassert.Equal(t, \"/users/activate\", ri.Path)\n\tassert.Equal(t, http.MethodTrace+\":/users/activate\", ri.Name)\n\tassert.Nil(t, ri.Parameters)\n\n\tstatus, body := request(http.MethodTrace, \"/users/activate\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, `OK`, body)\n}\n\nfunc TestGroup_RouteNotFound(t *testing.T) {\n\tvar testCases = []struct {\n\t\texpectRoute any\n\t\tname        string\n\t\twhenURL     string\n\t\texpectCode  int\n\t}{\n\t\t{\n\t\t\tname:        \"404, route to static not found handler /group/a/c/xx\",\n\t\t\twhenURL:     \"/group/a/c/xx\",\n\t\t\texpectRoute: \"GET /group/a/c/xx\",\n\t\t\texpectCode:  http.StatusNotFound,\n\t\t},\n\t\t{\n\t\t\tname:        \"404, route to path param not found handler /group/a/:file\",\n\t\t\twhenURL:     \"/group/a/echo.exe\",\n\t\t\texpectRoute: \"GET /group/a/:file\",\n\t\t\texpectCode:  http.StatusNotFound,\n\t\t},\n\t\t{\n\t\t\tname:        \"404, route to any not found handler /group/*\",\n\t\t\twhenURL:     \"/group/b/echo.exe\",\n\t\t\texpectRoute: \"GET /group/*\",\n\t\t\texpectCode:  http.StatusNotFound,\n\t\t},\n\t\t{\n\t\t\tname:        \"200, route /group/a/c/df to /group/a/c/df\",\n\t\t\twhenURL:     \"/group/a/c/df\",\n\t\t\texpectRoute: \"GET /group/a/c/df\",\n\t\t\texpectCode:  http.StatusOK,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\t\t\tg := e.Group(\"/group\")\n\n\t\t\tokHandler := func(c *Context) error {\n\t\t\t\treturn c.String(http.StatusOK, c.Request().Method+\" \"+c.Path())\n\t\t\t}\n\t\t\tnotFoundHandler := func(c *Context) error {\n\t\t\t\treturn c.String(http.StatusNotFound, c.Request().Method+\" \"+c.Path())\n\t\t\t}\n\n\t\t\tg.GET(\"/\", okHandler)\n\t\t\tg.GET(\"/a/c/df\", okHandler)\n\t\t\tg.GET(\"/a/b*\", okHandler)\n\t\t\tg.PUT(\"/*\", okHandler)\n\n\t\t\tg.RouteNotFound(\"/a/c/xx\", notFoundHandler)  // static\n\t\t\tg.RouteNotFound(\"/a/:file\", notFoundHandler) // param\n\t\t\tg.RouteNotFound(\"/*\", notFoundHandler)       // any\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)\n\t\t\trec := httptest.NewRecorder()\n\n\t\t\te.ServeHTTP(rec, req)\n\n\t\t\tassert.Equal(t, tc.expectCode, rec.Code)\n\t\t\tassert.Equal(t, tc.expectRoute, rec.Body.String())\n\t\t})\n\t}\n}\n\nfunc TestGroup_Any(t *testing.T) {\n\te := New()\n\n\tusers := e.Group(\"/users\")\n\tri := users.Any(\"/activate\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK from ANY\")\n\t})\n\n\tassert.Equal(t, RouteAny, ri.Method)\n\tassert.Equal(t, \"/users/activate\", ri.Path)\n\tassert.Equal(t, RouteAny+\":/users/activate\", ri.Name)\n\tassert.Nil(t, ri.Parameters)\n\n\tstatus, body := request(http.MethodTrace, \"/users/activate\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, `OK from ANY`, body)\n}\n\nfunc TestGroup_Match(t *testing.T) {\n\te := New()\n\n\tmyMethods := []string{http.MethodGet, http.MethodPost}\n\tusers := e.Group(\"/users\")\n\tris := users.Match(myMethods, \"/activate\", func(c *Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\tassert.Len(t, ris, 2)\n\n\tfor _, m := range myMethods {\n\t\tstatus, body := request(m, \"/users/activate\", e)\n\t\tassert.Equal(t, http.StatusTeapot, status)\n\t\tassert.Equal(t, `OK`, body)\n\t}\n}\n\nfunc TestGroup_MatchWithErrors(t *testing.T) {\n\te := New()\n\n\tusers := e.Group(\"/users\")\n\tusers.GET(\"/activate\", func(c *Context) error {\n\t\treturn c.String(http.StatusOK, \"OK\")\n\t})\n\tmyMethods := []string{http.MethodGet, http.MethodPost}\n\n\terrs := func() (errs []error) {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tif tmpErr, ok := r.([]error); ok {\n\t\t\t\t\terrs = tmpErr\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tpanic(r)\n\t\t\t}\n\t\t}()\n\n\t\tusers.Match(myMethods, \"/activate\", func(c *Context) error {\n\t\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t\t})\n\t\treturn nil\n\t}()\n\tassert.Len(t, errs, 1)\n\tassert.EqualError(t, errs[0], \"GET /users/activate: adding duplicate route (same method+path) is not allowed\")\n\n\tfor _, m := range myMethods {\n\t\tstatus, body := request(m, \"/users/activate\", e)\n\n\t\texpect := http.StatusTeapot\n\t\tif m == http.MethodGet {\n\t\t\texpect = http.StatusOK\n\t\t}\n\t\tassert.Equal(t, expect, status)\n\t\tassert.Equal(t, `OK`, body)\n\t}\n}\n\nfunc TestGroup_Static(t *testing.T) {\n\te := New()\n\n\tg := e.Group(\"/books\")\n\tri := g.Static(\"/download\", \"_fixture\")\n\tassert.Equal(t, http.MethodGet, ri.Method)\n\tassert.Equal(t, \"/books/download*\", ri.Path)\n\tassert.Equal(t, \"GET:/books/download*\", ri.Name)\n\tassert.Equal(t, []string{\"*\"}, ri.Parameters)\n\n\treq := httptest.NewRequest(http.MethodGet, \"/books/download/index.html\", nil)\n\trec := httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\n\tassert.Equal(t, http.StatusOK, rec.Code)\n\tbody := rec.Body.String()\n\tassert.True(t, strings.HasPrefix(body, \"<!doctype html>\"))\n}\n\nfunc TestGroup_StaticMultiTest(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname                  string\n\t\tgivenPrefix           string\n\t\tgivenRoot             string\n\t\twhenURL               string\n\t\texpectHeaderLocation  string\n\t\texpectBodyStartsWith  string\n\t\texpectBodyNotContains string\n\t\texpectStatus          int\n\t}{\n\t\t{\n\t\t\tname:                 \"ok\",\n\t\t\tgivenPrefix:          \"/images\",\n\t\t\tgivenRoot:            \"_fixture/images\",\n\t\t\twhenURL:              \"/test/images/walle.png\",\n\t\t\texpectStatus:         http.StatusOK,\n\t\t\texpectBodyStartsWith: string([]byte{0x89, 0x50, 0x4e, 0x47}),\n\t\t},\n\t\t{\n\t\t\tname:                 \"ok, without prefix\",\n\t\t\tgivenPrefix:          \"\",\n\t\t\tgivenRoot:            \"_fixture/images\",\n\t\t\twhenURL:              \"/testwalle.png\", // `/test` + `*` creates route `/test*` witch matches `/testwalle.png`\n\t\t\texpectStatus:         http.StatusOK,\n\t\t\texpectBodyStartsWith: string([]byte{0x89, 0x50, 0x4e, 0x47}),\n\t\t},\n\t\t{\n\t\t\tname:                 \"nok, without prefix does not serve dir index\",\n\t\t\tgivenPrefix:          \"\",\n\t\t\tgivenRoot:            \"_fixture/images\",\n\t\t\twhenURL:              \"/test/\", // `/test` + `*` creates route `/test*`\n\t\t\texpectStatus:         http.StatusNotFound,\n\t\t\texpectBodyStartsWith: \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"No file\",\n\t\t\tgivenPrefix:          \"/images\",\n\t\t\tgivenRoot:            \"_fixture/scripts\",\n\t\t\twhenURL:              \"/test/images/bolt.png\",\n\t\t\texpectStatus:         http.StatusNotFound,\n\t\t\texpectBodyStartsWith: \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Directory\",\n\t\t\tgivenPrefix:          \"/images\",\n\t\t\tgivenRoot:            \"_fixture/images\",\n\t\t\twhenURL:              \"/test/images/\",\n\t\t\texpectStatus:         http.StatusNotFound,\n\t\t\texpectBodyStartsWith: \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Directory Redirect\",\n\t\t\tgivenPrefix:          \"/\",\n\t\t\tgivenRoot:            \"_fixture\",\n\t\t\twhenURL:              \"/test/folder\",\n\t\t\texpectStatus:         http.StatusMovedPermanently,\n\t\t\texpectHeaderLocation: \"/test/folder/\",\n\t\t\texpectBodyStartsWith: \"\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Directory Redirect with non-root path\",\n\t\t\tgivenPrefix:          \"/static\",\n\t\t\tgivenRoot:            \"_fixture\",\n\t\t\twhenURL:              \"/test/static\",\n\t\t\texpectStatus:         http.StatusMovedPermanently,\n\t\t\texpectHeaderLocation: \"/test/static/\",\n\t\t\texpectBodyStartsWith: \"\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Prefixed directory 404 (request URL without slash)\",\n\t\t\tgivenPrefix:          \"/folder/\", // trailing slash will intentionally not match \"/folder\"\n\t\t\tgivenRoot:            \"_fixture\",\n\t\t\twhenURL:              \"/test/folder\", // no trailing slash\n\t\t\texpectStatus:         http.StatusNotFound,\n\t\t\texpectBodyStartsWith: \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Prefixed directory redirect (without slash redirect to slash)\",\n\t\t\tgivenPrefix:          \"/folder\", // no trailing slash shall match /folder and /folder/*\n\t\t\tgivenRoot:            \"_fixture\",\n\t\t\twhenURL:              \"/test/folder\", // no trailing slash\n\t\t\texpectStatus:         http.StatusMovedPermanently,\n\t\t\texpectHeaderLocation: \"/test/folder/\",\n\t\t\texpectBodyStartsWith: \"\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Directory with index.html\",\n\t\t\tgivenPrefix:          \"/\",\n\t\t\tgivenRoot:            \"_fixture\",\n\t\t\twhenURL:              \"/test/\",\n\t\t\texpectStatus:         http.StatusOK,\n\t\t\texpectBodyStartsWith: \"<!doctype html>\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Prefixed directory with index.html (prefix ending with slash)\",\n\t\t\tgivenPrefix:          \"/assets/\",\n\t\t\tgivenRoot:            \"_fixture\",\n\t\t\twhenURL:              \"/test/assets/\",\n\t\t\texpectStatus:         http.StatusOK,\n\t\t\texpectBodyStartsWith: \"<!doctype html>\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Prefixed directory with index.html (prefix ending without slash)\",\n\t\t\tgivenPrefix:          \"/assets\",\n\t\t\tgivenRoot:            \"_fixture\",\n\t\t\twhenURL:              \"/test/assets/\",\n\t\t\texpectStatus:         http.StatusOK,\n\t\t\texpectBodyStartsWith: \"<!doctype html>\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Sub-directory with index.html\",\n\t\t\tgivenPrefix:          \"/\",\n\t\t\tgivenRoot:            \"_fixture\",\n\t\t\twhenURL:              \"/test/folder/\",\n\t\t\texpectStatus:         http.StatusOK,\n\t\t\texpectBodyStartsWith: \"<!doctype html>\",\n\t\t},\n\t\t{\n\t\t\tname:                  \"nok, URL encoded path traversal (single encoding, slash - unix separator)\",\n\t\t\tgivenRoot:             \"_fixture/dist/public\",\n\t\t\twhenURL:               \"/%2e%2e%2fprivate.txt\",\n\t\t\texpectStatus:          http.StatusNotFound,\n\t\t\texpectBodyStartsWith:  \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t\texpectBodyNotContains: `private file`,\n\t\t},\n\t\t{\n\t\t\tname:                  \"nok, URL encoded path traversal (single encoding, backslash - windows separator)\",\n\t\t\tgivenRoot:             \"_fixture/dist/public\",\n\t\t\twhenURL:               \"/%2e%2e%5cprivate.txt\",\n\t\t\texpectStatus:          http.StatusNotFound,\n\t\t\texpectBodyStartsWith:  \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t\texpectBodyNotContains: `private file`,\n\t\t},\n\t\t{\n\t\t\tname:                 \"do not allow directory traversal (backslash - windows separator)\",\n\t\t\tgivenPrefix:          \"/\",\n\t\t\tgivenRoot:            \"_fixture/\",\n\t\t\twhenURL:              `/test/..\\\\middleware/basic_auth.go`,\n\t\t\texpectStatus:         http.StatusNotFound,\n\t\t\texpectBodyStartsWith: \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"do not allow directory traversal (slash - unix separator)\",\n\t\t\tgivenPrefix:          \"/\",\n\t\t\tgivenRoot:            \"_fixture/\",\n\t\t\twhenURL:              `/test/../middleware/basic_auth.go`,\n\t\t\texpectStatus:         http.StatusNotFound,\n\t\t\texpectBodyStartsWith: \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\n\t\t\tg := e.Group(\"/test\")\n\t\t\tg.Static(tc.givenPrefix, tc.givenRoot)\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)\n\t\t\trec := httptest.NewRecorder()\n\t\t\te.ServeHTTP(rec, req)\n\n\t\t\tassert.Equal(t, tc.expectStatus, rec.Code)\n\t\t\tbody := rec.Body.String()\n\t\t\tif tc.expectBodyStartsWith != \"\" {\n\t\t\t\tassert.True(t, strings.HasPrefix(body, tc.expectBodyStartsWith))\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, \"\", body)\n\t\t\t}\n\t\t\tif tc.expectBodyNotContains != \"\" {\n\t\t\t\tassert.NotContains(t, body, tc.expectBodyNotContains)\n\t\t\t}\n\n\t\t\tif tc.expectHeaderLocation != \"\" {\n\t\t\t\tassert.Equal(t, tc.expectHeaderLocation, rec.Result().Header[\"Location\"][0])\n\t\t\t} else {\n\t\t\t\t_, ok := rec.Result().Header[\"Location\"]\n\t\t\t\tassert.False(t, ok)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGroup_FileFS(t *testing.T) {\n\tvar testCases = []struct {\n\t\twhenFS           fs.FS\n\t\tname             string\n\t\twhenPath         string\n\t\twhenFile         string\n\t\tgivenURL         string\n\t\texpectStartsWith []byte\n\t\texpectCode       int\n\t}{\n\t\t{\n\t\t\tname:             \"ok\",\n\t\t\twhenPath:         \"/walle\",\n\t\t\twhenFS:           os.DirFS(\"_fixture/images\"),\n\t\t\twhenFile:         \"walle.png\",\n\t\t\tgivenURL:         \"/assets/walle\",\n\t\t\texpectCode:       http.StatusOK,\n\t\t\texpectStartsWith: []byte{0x89, 0x50, 0x4e},\n\t\t},\n\t\t{\n\t\t\tname:             \"nok, requesting invalid path\",\n\t\t\twhenPath:         \"/walle\",\n\t\t\twhenFS:           os.DirFS(\"_fixture/images\"),\n\t\t\twhenFile:         \"walle.png\",\n\t\t\tgivenURL:         \"/assets/walle.png\",\n\t\t\texpectCode:       http.StatusNotFound,\n\t\t\texpectStartsWith: []byte(`{\"message\":\"Not Found\"}`),\n\t\t},\n\t\t{\n\t\t\tname:             \"nok, serving not existent file from filesystem\",\n\t\t\twhenPath:         \"/walle\",\n\t\t\twhenFS:           os.DirFS(\"_fixture/images\"),\n\t\t\twhenFile:         \"not-existent.png\",\n\t\t\tgivenURL:         \"/assets/walle\",\n\t\t\texpectCode:       http.StatusNotFound,\n\t\t\texpectStartsWith: []byte(`{\"message\":\"Not Found\"}`),\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\t\t\tg := e.Group(\"/assets\")\n\t\t\tg.FileFS(tc.whenPath, tc.whenFile, tc.whenFS)\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.givenURL, nil)\n\t\t\trec := httptest.NewRecorder()\n\n\t\t\te.ServeHTTP(rec, req)\n\n\t\t\tassert.Equal(t, tc.expectCode, rec.Code)\n\n\t\t\tbody := rec.Body.Bytes()\n\t\t\tif len(body) > len(tc.expectStartsWith) {\n\t\t\t\tbody = body[:len(tc.expectStartsWith)]\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectStartsWith, body)\n\t\t})\n\t}\n}\n\nfunc TestGroup_StaticPanic(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\tgivenRoot string\n\t}{\n\t\t{\n\t\t\tname:      \"panics for ../\",\n\t\t\tgivenRoot: \"../images\",\n\t\t},\n\t\t{\n\t\t\tname:      \"panics for /\",\n\t\t\tgivenRoot: \"/images\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\t\t\te.Filesystem = os.DirFS(\"./\")\n\n\t\t\tg := e.Group(\"/assets\")\n\n\t\t\tassert.Panics(t, func() {\n\t\t\t\tg.Static(\"/images\", tc.givenRoot)\n\t\t\t})\n\t\t})\n\t}\n}\n\nfunc TestGroup_RouteNotFoundWithMiddleware(t *testing.T) {\n\tvar testCases = []struct {\n\t\texpectBody             any\n\t\tname                   string\n\t\twhenURL                string\n\t\texpectCode             int\n\t\tgivenCustom404         bool\n\t\texpectMiddlewareCalled bool\n\t}{\n\t\t{\n\t\t\tname:                   \"ok, custom 404 handler is called with middleware\",\n\t\t\tgivenCustom404:         true,\n\t\t\twhenURL:                \"/group/test3\",\n\t\t\texpectBody:             \"404 GET /group/*\",\n\t\t\texpectCode:             http.StatusNotFound,\n\t\t\texpectMiddlewareCalled: true, // because RouteNotFound is added after middleware is added\n\t\t},\n\t\t{\n\t\t\tname:                   \"ok, default group 404 handler is not called with middleware\",\n\t\t\tgivenCustom404:         false,\n\t\t\twhenURL:                \"/group/test3\",\n\t\t\texpectBody:             \"404 GET /*\",\n\t\t\texpectCode:             http.StatusNotFound,\n\t\t\texpectMiddlewareCalled: false, // because RouteNotFound is added before middleware is added\n\t\t},\n\t\t{\n\t\t\tname:                   \"ok, (no slash) default group 404 handler is called with middleware\",\n\t\t\tgivenCustom404:         false,\n\t\t\twhenURL:                \"/group\",\n\t\t\texpectBody:             \"404 GET /*\",\n\t\t\texpectCode:             http.StatusNotFound,\n\t\t\texpectMiddlewareCalled: false, // because RouteNotFound is added before middleware is added\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\n\t\t\tokHandler := func(c *Context) error {\n\t\t\t\treturn c.String(http.StatusOK, c.Request().Method+\" \"+c.Path())\n\t\t\t}\n\t\t\tnotFoundHandler := func(c *Context) error {\n\t\t\t\treturn c.String(http.StatusNotFound, \"404 \"+c.Request().Method+\" \"+c.Path())\n\t\t\t}\n\n\t\t\te := New()\n\t\t\te.GET(\"/test1\", okHandler)\n\t\t\te.RouteNotFound(\"/*\", notFoundHandler)\n\n\t\t\tg := e.Group(\"/group\")\n\t\t\tg.GET(\"/test1\", okHandler)\n\n\t\t\tmiddlewareCalled := false\n\t\t\tg.Use(func(next HandlerFunc) HandlerFunc {\n\t\t\t\treturn func(c *Context) error {\n\t\t\t\t\tmiddlewareCalled = true\n\t\t\t\t\treturn next(c)\n\t\t\t\t}\n\t\t\t})\n\t\t\tif tc.givenCustom404 {\n\t\t\t\tg.RouteNotFound(\"/*\", notFoundHandler)\n\t\t\t}\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)\n\t\t\trec := httptest.NewRecorder()\n\n\t\t\te.ServeHTTP(rec, req)\n\n\t\t\tassert.Equal(t, tc.expectMiddlewareCalled, middlewareCalled)\n\t\t\tassert.Equal(t, tc.expectCode, rec.Code)\n\t\t\tassert.Equal(t, tc.expectBody, rec.Body.String())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "httperror.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\n// Following errors can produce HTTP status code by implementing HTTPStatusCoder interface\nvar (\n\tErrBadRequest                  = &httpError{http.StatusBadRequest}            // 400\n\tErrUnauthorized                = &httpError{http.StatusUnauthorized}          // 401\n\tErrForbidden                   = &httpError{http.StatusForbidden}             // 403\n\tErrNotFound                    = &httpError{http.StatusNotFound}              // 404\n\tErrMethodNotAllowed            = &httpError{http.StatusMethodNotAllowed}      // 405\n\tErrRequestTimeout              = &httpError{http.StatusRequestTimeout}        // 408\n\tErrStatusRequestEntityTooLarge = &httpError{http.StatusRequestEntityTooLarge} // 413\n\tErrUnsupportedMediaType        = &httpError{http.StatusUnsupportedMediaType}  // 415\n\tErrTooManyRequests             = &httpError{http.StatusTooManyRequests}       // 429\n\tErrInternalServerError         = &httpError{http.StatusInternalServerError}   // 500\n\tErrBadGateway                  = &httpError{http.StatusBadGateway}            // 502\n\tErrServiceUnavailable          = &httpError{http.StatusServiceUnavailable}    // 503\n)\n\n// Following errors fall into 500 (InternalServerError) category\nvar (\n\tErrValidatorNotRegistered = errors.New(\"validator not registered\")\n\tErrRendererNotRegistered  = errors.New(\"renderer not registered\")\n\tErrInvalidRedirectCode    = errors.New(\"invalid redirect status code\")\n\tErrCookieNotFound         = errors.New(\"cookie not found\")\n\tErrInvalidCertOrKeyType   = errors.New(\"invalid cert or key type, must be string or []byte\")\n\tErrInvalidListenerNetwork = errors.New(\"invalid listener network\")\n)\n\n// HTTPStatusCoder is interface that errors can implement to produce status code for HTTP response\ntype HTTPStatusCoder interface {\n\tStatusCode() int\n}\n\n// StatusCode returns status code from error if it implements HTTPStatusCoder interface.\n// If error does not implement the interface it returns 0.\nfunc StatusCode(err error) int {\n\tvar sc HTTPStatusCoder\n\tif errors.As(err, &sc) {\n\t\treturn sc.StatusCode()\n\t}\n\treturn 0\n}\n\n// ResolveResponseStatus returns the Response and HTTP status code that should be (or has been) sent for rw,\n// given an optional error.\n//\n// This function is useful for middleware and handlers that need to figure out the HTTP status\n// code to return based on the error that occurred or what was set in the response.\n//\n// Precedence rules:\n//  1. If the response has already been committed, the committed status wins (err is ignored).\n//  2. Otherwise, start with 200 OK (net/http default if WriteHeader is never called).\n//  3. If the response has a non-zero suggested status, use it.\n//  4. If err != nil, it overrides the suggested status:\n//     - StatusCode(err) if non-zero\n//     - otherwise 500 Internal Server Error.\nfunc ResolveResponseStatus(rw http.ResponseWriter, err error) (resp *Response, status int) {\n\tresp, _ = UnwrapResponse(rw)\n\n\t// once committed (sent to the client), the wire status is fixed; err cannot change it.\n\tif resp != nil && resp.Committed {\n\t\tif resp.Status == 0 {\n\t\t\t// unlikely path, but fall back to net/http implicit default if handler never calls WriteHeader\n\t\t\treturn resp, http.StatusOK\n\t\t}\n\t\treturn resp, resp.Status\n\t}\n\n\t// net/http implicit default if handler never calls WriteHeader.\n\tstatus = http.StatusOK\n\n\t// suggested status written from middleware/handlers, if present.\n\tif resp != nil && resp.Status != 0 {\n\t\tstatus = resp.Status\n\t}\n\n\t// error overrides suggested status (matches typical Echo error-handler semantics).\n\tif err != nil {\n\t\tif s := StatusCode(err); s != 0 {\n\t\t\tstatus = s\n\t\t} else {\n\t\t\tstatus = http.StatusInternalServerError\n\t\t}\n\t}\n\n\treturn resp, status\n}\n\n// NewHTTPError creates new instance of HTTPError\nfunc NewHTTPError(code int, message string) *HTTPError {\n\treturn &HTTPError{\n\t\tCode:    code,\n\t\tMessage: message,\n\t}\n}\n\n// HTTPError represents an error that occurred while handling a request.\ntype HTTPError struct {\n\t// Code is status code for HTTP response\n\tCode    int    `json:\"-\"`\n\tMessage string `json:\"message\"`\n\terr     error\n}\n\n// StatusCode returns status code for HTTP response\nfunc (he *HTTPError) StatusCode() int {\n\treturn he.Code\n}\n\n// Error makes it compatible with `error` interface.\nfunc (he *HTTPError) Error() string {\n\tmsg := he.Message\n\tif msg == \"\" {\n\t\tmsg = http.StatusText(he.Code)\n\t}\n\tif he.err == nil {\n\t\treturn fmt.Sprintf(\"code=%d, message=%v\", he.Code, msg)\n\t}\n\treturn fmt.Sprintf(\"code=%d, message=%v, err=%v\", he.Code, msg, he.err.Error())\n}\n\n// Wrap eturns new HTTPError with given errors wrapped inside\nfunc (he HTTPError) Wrap(err error) error {\n\treturn &HTTPError{\n\t\tCode:    he.Code,\n\t\tMessage: he.Message,\n\t\terr:     err,\n\t}\n}\n\nfunc (he *HTTPError) Unwrap() error {\n\treturn he.err\n}\n\ntype httpError struct {\n\tcode int\n}\n\nfunc (he httpError) StatusCode() int {\n\treturn he.code\n}\n\nfunc (he httpError) Error() string {\n\treturn http.StatusText(he.code) // does not include status code\n}\n\nfunc (he httpError) Wrap(err error) error {\n\treturn &HTTPError{\n\t\tCode:    he.code,\n\t\tMessage: http.StatusText(he.code),\n\t\terr:     err,\n\t}\n}\n"
  },
  {
    "path": "httperror_external_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\n// run tests as external package to get real feel for API\npackage echo_test\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"github.com/labstack/echo/v5\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n)\n\nfunc ExampleDefaultHTTPErrorHandler() {\n\te := echo.New()\n\te.GET(\"/api/endpoint\", func(c *echo.Context) error {\n\t\treturn &apiError{\n\t\t\tCode: http.StatusBadRequest,\n\t\t\tBody: map[string]any{\"message\": \"custom error\"},\n\t\t}\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/api/endpoint?err=1\", nil)\n\tresp := httptest.NewRecorder()\n\n\te.ServeHTTP(resp, req)\n\n\tfmt.Printf(\"%d %s\", resp.Code, resp.Body.String())\n\n\t// Output: 400 {\"error\":{\"message\":\"custom error\"}}\n}\n\ntype apiError struct {\n\tCode int\n\tBody any\n}\n\nfunc (e *apiError) StatusCode() int {\n\treturn e.Code\n}\n\nfunc (e *apiError) MarshalJSON() ([]byte, error) {\n\ttype body struct {\n\t\tError any `json:\"error\"`\n\t}\n\treturn json.Marshal(body{Error: e.Body})\n}\n\nfunc (e *apiError) Error() string {\n\treturn http.StatusText(e.Code)\n}\n"
  },
  {
    "path": "httperror_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestHTTPError_StatusCode(t *testing.T) {\n\tvar err error = &HTTPError{Code: http.StatusBadRequest, Message: \"my error message\"}\n\n\tcode := 0\n\tvar sc HTTPStatusCoder\n\tif errors.As(err, &sc) {\n\t\tcode = sc.StatusCode()\n\t}\n\tassert.Equal(t, http.StatusBadRequest, code)\n}\n\nfunc TestHTTPError_Error(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname   string\n\t\terror  error\n\t\texpect string\n\t}{\n\t\t{\n\t\t\tname:   \"ok, without message\",\n\t\t\terror:  &HTTPError{Code: http.StatusBadRequest},\n\t\t\texpect: \"code=400, message=Bad Request\",\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, with message\",\n\t\t\terror:  &HTTPError{Code: http.StatusBadRequest, Message: \"my error message\"},\n\t\t\texpect: \"code=400, message=my error message\",\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tc.expect, tc.error.Error())\n\t\t})\n\t}\n}\n\nfunc TestHTTPError_WrapUnwrap(t *testing.T) {\n\terr := &HTTPError{Code: http.StatusBadRequest, Message: \"bad\"}\n\twrapped := err.Wrap(errors.New(\"my_error\")).(*HTTPError)\n\n\terr.Code = http.StatusOK\n\terr.Message = \"changed\"\n\n\tassert.Equal(t, http.StatusBadRequest, wrapped.Code)\n\tassert.Equal(t, \"bad\", wrapped.Message)\n\n\tassert.Equal(t, errors.New(\"my_error\"), wrapped.Unwrap())\n\tassert.Equal(t, \"code=400, message=bad, err=my_error\", wrapped.Error())\n}\n\nfunc TestNewHTTPError(t *testing.T) {\n\terr := NewHTTPError(http.StatusBadRequest, \"bad\")\n\terr2 := &HTTPError{Code: http.StatusBadRequest, Message: \"bad\"}\n\n\tassert.Equal(t, err2, err)\n}\n\nfunc TestStatusCode(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname   string\n\t\terr    error\n\t\texpect int\n\t}{\n\t\t{\n\t\t\tname:   \"ok, HTTPError\",\n\t\t\terr:    &HTTPError{Code: http.StatusNotFound},\n\t\t\texpect: http.StatusNotFound,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, sentinel error\",\n\t\t\terr:    ErrNotFound,\n\t\t\texpect: http.StatusNotFound,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, wrapped HTTPError\",\n\t\t\terr:    fmt.Errorf(\"wrapped: %w\", &HTTPError{Code: http.StatusTeapot}),\n\t\t\texpect: http.StatusTeapot,\n\t\t},\n\t\t{\n\t\t\tname:   \"nok, normal error\",\n\t\t\terr:    errors.New(\"error\"),\n\t\t\texpect: 0,\n\t\t},\n\t\t{\n\t\t\tname:   \"nok, nil\",\n\t\t\terr:    nil,\n\t\t\texpect: 0,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tc.expect, StatusCode(tc.err))\n\t\t})\n\t}\n}\n\nfunc TestResolveResponseStatus(t *testing.T) {\n\tsomeErr := errors.New(\"some error\")\n\n\tvar testCases = []struct {\n\t\tname         string\n\t\twhenResp     http.ResponseWriter\n\t\twhenErr      error\n\t\texpectStatus int\n\t\texpectResp   bool\n\t}{\n\t\t{\n\t\t\tname:         \"nil resp, nil err -> 200\",\n\t\t\twhenResp:     nil,\n\t\t\twhenErr:      nil,\n\t\t\texpectStatus: http.StatusOK,\n\t\t\texpectResp:   false,\n\t\t},\n\t\t{\n\t\t\tname:         \"resp suggested status used when no error\",\n\t\t\twhenResp:     &Response{Status: http.StatusCreated},\n\t\t\twhenErr:      nil,\n\t\t\texpectStatus: http.StatusCreated,\n\t\t\texpectResp:   true,\n\t\t},\n\t\t{\n\t\t\tname:         \"error overrides suggested status with StatusCode(err)\",\n\t\t\twhenResp:     &Response{Status: http.StatusAccepted},\n\t\t\twhenErr:      ErrBadRequest,\n\t\t\texpectStatus: http.StatusBadRequest,\n\t\t\texpectResp:   true,\n\t\t},\n\t\t{\n\t\t\tname:         \"error overrides suggested status with 500 when StatusCode(err)==0\",\n\t\t\twhenResp:     &Response{Status: http.StatusAccepted},\n\t\t\twhenErr:      ErrInternalServerError,\n\t\t\texpectStatus: http.StatusInternalServerError,\n\t\t\texpectResp:   true,\n\t\t},\n\t\t{\n\t\t\tname:         \"nil resp, error -> 500 when StatusCode(err)==0\",\n\t\t\twhenResp:     nil,\n\t\t\twhenErr:      someErr,\n\t\t\texpectStatus: http.StatusInternalServerError,\n\t\t\texpectResp:   false,\n\t\t},\n\t\t{\n\t\t\tname:         \"committed response wins over error\",\n\t\t\twhenResp:     &Response{Committed: true, Status: http.StatusNoContent},\n\t\t\twhenErr:      someErr,\n\t\t\texpectStatus: http.StatusNoContent,\n\t\t\texpectResp:   true,\n\t\t},\n\t\t{\n\t\t\tname:         \"committed response with status 0 falls back to 200 (defensive)\",\n\t\t\twhenResp:     &Response{Committed: true, Status: 0},\n\t\t\twhenErr:      someErr,\n\t\t\texpectStatus: http.StatusOK,\n\t\t\texpectResp:   true,\n\t\t},\n\t\t{\n\t\t\tname:         \"resp with status 0 and no error -> 200\",\n\t\t\twhenResp:     &Response{Status: 0},\n\t\t\twhenErr:      nil,\n\t\t\texpectStatus: http.StatusOK,\n\t\t\texpectResp:   true,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresp, status := ResolveResponseStatus(tc.whenResp, tc.whenErr)\n\n\t\t\tassert.Equal(t, tc.expectResp, resp != nil)\n\t\t\tassert.Equal(t, tc.expectStatus, status)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "ip.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n)\n\n/**\nBy: https://github.com/tmshn (See: https://github.com/labstack/echo/pull/1478 , https://github.com/labstack/echox/pull/134 )\nSource: https://echo.labstack.com/guide/ip-address/\n\nIP address plays fundamental role in HTTP; it's used for access control, auditing, geo-based access analysis and more.\nEcho provides handy method [`Context#RealIP()`](https://godoc.org/github.com/labstack/echo#Context) for that.\n\nHowever, it is not trivial to retrieve the _real_ IP address from requests especially when you put L7 proxies before the application.\nIn such situation, _real_ IP needs to be relayed on HTTP layer from proxies to your app, but you must not trust HTTP headers unconditionally.\nOtherwise, you might give someone a chance of deceiving you. **A security risk!**\n\nTo retrieve IP address reliably/securely, you must let your application be aware of the entire architecture of your infrastructure.\nIn Echo, this can be done by configuring `Echo#IPExtractor` appropriately.\nThis guides show you why and how.\n\n> Note: if you don't set `Echo#IPExtractor` explicitly, Echo fallback to legacy behavior, which is not a good choice.\n\nLet's start from two questions to know the right direction:\n\n1. Do you put any HTTP (L7) proxy in front of the application?\n    - It includes both cloud solutions (such as AWS ALB or GCP HTTP LB) and OSS ones (such as Nginx, Envoy or Istio ingress gateway).\n2. If yes, what HTTP header do your proxies use to pass client IP to the application?\n\n## Case 1. With no proxy\n\nIf you put no proxy (e.g.: directory facing to the internet), all you need to (and have to) see is IP address from network layer.\nAny HTTP header is untrustable because the clients have full control what headers to be set.\n\nIn this case, use `echo.ExtractIPDirect()`.\n\n```go\ne.IPExtractor = echo.ExtractIPDirect()\n```\n\n## Case 2. With proxies using `X-Forwarded-For` header\n\n[`X-Forwared-For` (XFF)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) is the popular header\nto relay clients' IP addresses.\nAt each hop on the proxies, they append the request IP address at the end of the header.\n\nFollowing example diagram illustrates this behavior.\n\n```text\n┌──────────┐            ┌──────────┐            ┌──────────┐            ┌──────────┐\n│ \"Origin\" │───────────>│ Proxy 1  │───────────>│ Proxy 2  │───────────>│ Your app │\n│ (IP: a)  │            │ (IP: b)  │            │ (IP: c)  │            │          │\n└──────────┘            └──────────┘            └──────────┘            └──────────┘\n\nCase 1.\nXFF:  \"\"                    \"a\"                     \"a, b\"\n                                                    ~~~~~~\nCase 2.\nXFF:  \"x\"                   \"x, a\"                  \"x, a, b\"\n                                                    ~~~~~~~~~\n                                                    ↑ What your app will see\n```\n\nIn this case, use **first _untrustable_ IP reading from right**. Never use first one reading from left, as it is\nconfigurable by client. Here \"trustable\" means \"you are sure the IP address belongs to your infrastructure\".\nIn above example, if `b` and `c` are trustable, the IP address of the client is `a` for both cases, never be `x`.\n\nIn Echo, use `ExtractIPFromXFFHeader(...TrustOption)`.\n\n```go\ne.IPExtractor = echo.ExtractIPFromXFFHeader()\n```\n\nBy default, it trusts internal IP addresses (loopback, link-local unicast, private-use and unique local address\nfrom [RFC6890](https://tools.ietf.org/html/rfc6890), [RFC4291](https://tools.ietf.org/html/rfc4291) and\n[RFC4193](https://tools.ietf.org/html/rfc4193)).\nTo control this behavior, use [`TrustOption`](https://godoc.org/github.com/labstack/echo#TrustOption)s.\n\nE.g.:\n\n```go\ne.IPExtractor = echo.ExtractIPFromXFFHeader(\n\tTrustLinkLocal(false),\n\tTrustIPRanges(lbIPRange),\n)\n```\n\n- Ref: https://godoc.org/github.com/labstack/echo#TrustOption\n\n## Case 3. With proxies using `X-Real-IP` header\n\n`X-Real-IP` is another HTTP header to relay clients' IP addresses, but it carries only one address unlike XFF.\n\nIf your proxies set this header, use `ExtractIPFromRealIPHeader(...TrustOption)`.\n\n```go\ne.IPExtractor = echo.ExtractIPFromRealIPHeader()\n```\n\nAgain, it trusts internal IP addresses by default (loopback, link-local unicast, private-use and unique local address\nfrom [RFC6890](https://tools.ietf.org/html/rfc6890), [RFC4291](https://tools.ietf.org/html/rfc4291) and\n[RFC4193](https://tools.ietf.org/html/rfc4193)).\nTo control this behavior, use [`TrustOption`](https://godoc.org/github.com/labstack/echo#TrustOption)s.\n\n- Ref: https://godoc.org/github.com/labstack/echo#TrustOption\n\n> **Never forget** to configure the outermost proxy (i.e.; at the edge of your infrastructure) **not to pass through incoming headers**.\n> Otherwise there is a chance of fraud, as it is what clients can control.\n\n## About default behavior\n\nIn default behavior, Echo sees all of first XFF header, X-Real-IP header and IP from network layer.\n\nAs you might already notice, after reading this article, this is not good.\nSole reason this is default is just backward compatibility.\n\n## Private IP ranges\n\nSee: https://en.wikipedia.org/wiki/Private_network\n\nPrivate IPv4 address ranges (RFC 1918):\n* 10.0.0.0 – 10.255.255.255 (24-bit block)\n* 172.16.0.0 – 172.31.255.255 (20-bit block)\n* 192.168.0.0 – 192.168.255.255 (16-bit block)\n\nPrivate IPv6 address ranges:\n* fc00::/7 address block = RFC 4193 Unique Local Addresses (ULA)\n\n*/\n\ntype ipChecker struct {\n\ttrustExtraRanges []*net.IPNet\n\ttrustLoopback    bool\n\ttrustLinkLocal   bool\n\ttrustPrivateNet  bool\n}\n\n// TrustOption is config for which IP address to trust\ntype TrustOption func(*ipChecker)\n\n// TrustLoopback configures if you trust loopback address (default: true).\nfunc TrustLoopback(v bool) TrustOption {\n\treturn func(c *ipChecker) {\n\t\tc.trustLoopback = v\n\t}\n}\n\n// TrustLinkLocal configures if you trust link-local address (default: true).\nfunc TrustLinkLocal(v bool) TrustOption {\n\treturn func(c *ipChecker) {\n\t\tc.trustLinkLocal = v\n\t}\n}\n\n// TrustPrivateNet configures if you trust private network address (default: true).\nfunc TrustPrivateNet(v bool) TrustOption {\n\treturn func(c *ipChecker) {\n\t\tc.trustPrivateNet = v\n\t}\n}\n\n// TrustIPRange add trustable IP ranges using CIDR notation.\nfunc TrustIPRange(ipRange *net.IPNet) TrustOption {\n\treturn func(c *ipChecker) {\n\t\tc.trustExtraRanges = append(c.trustExtraRanges, ipRange)\n\t}\n}\n\nfunc newIPChecker(configs []TrustOption) *ipChecker {\n\tchecker := &ipChecker{trustLoopback: true, trustLinkLocal: true, trustPrivateNet: true}\n\tfor _, configure := range configs {\n\t\tconfigure(checker)\n\t}\n\treturn checker\n}\n\nfunc (c *ipChecker) trust(ip net.IP) bool {\n\tif c.trustLoopback && ip.IsLoopback() {\n\t\treturn true\n\t}\n\tif c.trustLinkLocal && ip.IsLinkLocalUnicast() {\n\t\treturn true\n\t}\n\tif c.trustPrivateNet && ip.IsPrivate() {\n\t\treturn true\n\t}\n\tfor _, trustedRange := range c.trustExtraRanges {\n\t\tif trustedRange.Contains(ip) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// IPExtractor is a function to extract IP addr from http.Request.\n// Set appropriate one to Echo#IPExtractor.\n// See https://echo.labstack.com/guide/ip-address for more details.\ntype IPExtractor func(*http.Request) string\n\n// ExtractIPDirect extracts IP address using actual IP address.\n// Use this if your server faces to internet directory (i.e.: uses no proxy).\nfunc ExtractIPDirect() IPExtractor {\n\treturn extractIP\n}\n\nfunc extractIP(req *http.Request) string {\n\thost, _, err := net.SplitHostPort(req.RemoteAddr)\n\tif err != nil {\n\t\tif net.ParseIP(req.RemoteAddr) != nil {\n\t\t\treturn req.RemoteAddr\n\t\t}\n\t\treturn \"\"\n\t}\n\treturn host\n}\n\n// ExtractIPFromRealIPHeader extracts IP address using x-real-ip header.\n// Use this if you put proxy which uses this header.\nfunc ExtractIPFromRealIPHeader(options ...TrustOption) IPExtractor {\n\tchecker := newIPChecker(options)\n\treturn func(req *http.Request) string {\n\t\trealIP := req.Header.Get(HeaderXRealIP)\n\t\tif realIP != \"\" {\n\t\t\trealIP = strings.TrimPrefix(realIP, \"[\")\n\t\t\trealIP = strings.TrimSuffix(realIP, \"]\")\n\t\t\tif ip := net.ParseIP(realIP); ip != nil && checker.trust(ip) {\n\t\t\t\treturn realIP\n\t\t\t}\n\t\t}\n\t\treturn extractIP(req)\n\t}\n}\n\n// ExtractIPFromXFFHeader extracts IP address using x-forwarded-for header.\n// Use this if you put proxy which uses this header.\n// This returns nearest untrustable IP. If all IPs are trustable, returns furthest one (i.e.: XFF[0]).\nfunc ExtractIPFromXFFHeader(options ...TrustOption) IPExtractor {\n\tchecker := newIPChecker(options)\n\treturn func(req *http.Request) string {\n\t\tdirectIP := extractIP(req)\n\t\txffs := req.Header[HeaderXForwardedFor]\n\t\tif len(xffs) == 0 {\n\t\t\treturn directIP\n\t\t}\n\t\tips := append(strings.Split(strings.Join(xffs, \",\"), \",\"), directIP)\n\t\tfor i := len(ips) - 1; i >= 0; i-- {\n\t\t\tips[i] = strings.TrimSpace(ips[i])\n\t\t\tips[i] = strings.TrimPrefix(ips[i], \"[\")\n\t\t\tips[i] = strings.TrimSuffix(ips[i], \"]\")\n\t\t\tip := net.ParseIP(ips[i])\n\t\t\tif ip == nil {\n\t\t\t\t// Unable to parse IP; cannot trust entire records\n\t\t\t\treturn directIP\n\t\t\t}\n\t\t\tif !checker.trust(ip) {\n\t\t\t\treturn ip.String()\n\t\t\t}\n\t\t}\n\t\t// All of the IPs are trusted; return first element because it is furthest from server (best effort strategy).\n\t\treturn strings.TrimSpace(ips[0])\n\t}\n}\n"
  },
  {
    "path": "ip_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc mustParseCIDR(s string) *net.IPNet {\n\t_, IPNet, err := net.ParseCIDR(s)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn IPNet\n}\n\nfunc TestIPChecker_TrustOption(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname         string\n\t\twhenIP       string\n\t\tgivenOptions []TrustOption\n\t\texpect       bool\n\t}{\n\t\t{\n\t\t\tname: \"ip is within trust range, trusts additional private IPV6 network\",\n\t\t\tgivenOptions: []TrustOption{\n\t\t\t\tTrustLoopback(false),\n\t\t\t\tTrustLinkLocal(false),\n\t\t\t\tTrustPrivateNet(false),\n\t\t\t\t// this is private IPv6 ip\n\t\t\t\t// CIDR Notation: \t2001:0db8:0000:0000:0000:0000:0000:0000/48\n\t\t\t\t// Address: \t\t\t\t2001:0db8:0000:0000:0000:0000:0000:0103\n\t\t\t\t// Range start: \t\t2001:0db8:0000:0000:0000:0000:0000:0000\n\t\t\t\t// Range end: \t\t\t2001:0db8:0000:ffff:ffff:ffff:ffff:ffff\n\t\t\t\tTrustIPRange(mustParseCIDR(\"2001:db8::103/48\")),\n\t\t\t},\n\t\t\twhenIP: \"2001:0db8:0000:0000:0000:0000:0000:0103\",\n\t\t\texpect: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ip is within trust range, trusts additional private IPV6 network\",\n\t\t\tgivenOptions: []TrustOption{\n\t\t\t\tTrustIPRange(mustParseCIDR(\"2001:db8::103/48\")),\n\t\t\t},\n\t\t\twhenIP: \"2001:0db8:0000:0000:0000:0000:0000:0103\",\n\t\t\texpect: true,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tchecker := newIPChecker(tc.givenOptions)\n\n\t\t\tresult := checker.trust(net.ParseIP(tc.whenIP))\n\t\t\tassert.Equal(t, tc.expect, result)\n\t\t})\n\t}\n}\n\nfunc TestTrustIPRange(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname       string\n\t\tgivenRange string\n\t\twhenIP     string\n\t\texpect     bool\n\t}{\n\t\t{\n\t\t\tname: \"ip is within trust range, IPV6 network range\",\n\t\t\t// CIDR Notation: 2001:0db8:0000:0000:0000:0000:0000:0000/48\n\t\t\t// Address:       2001:0db8:0000:0000:0000:0000:0000:0103\n\t\t\t// Range start:   2001:0db8:0000:0000:0000:0000:0000:0000\n\t\t\t// Range end:     2001:0db8:0000:ffff:ffff:ffff:ffff:ffff\n\t\t\tgivenRange: \"2001:db8::103/48\",\n\t\t\twhenIP:     \"2001:0db8:0000:0000:0000:0000:0000:0103\",\n\t\t\texpect:     true,\n\t\t},\n\t\t{\n\t\t\tname:       \"ip is outside (upper bounds) of trust range, IPV6 network range\",\n\t\t\tgivenRange: \"2001:db8::103/48\",\n\t\t\twhenIP:     \"2001:0db8:0001:0000:0000:0000:0000:0000\",\n\t\t\texpect:     false,\n\t\t},\n\t\t{\n\t\t\tname:       \"ip is outside (lower bounds) of trust range, IPV6 network range\",\n\t\t\tgivenRange: \"2001:db8::103/48\",\n\t\t\twhenIP:     \"2001:0db7:ffff:ffff:ffff:ffff:ffff:ffff\",\n\t\t\texpect:     false,\n\t\t},\n\t\t{\n\t\t\tname: \"ip is within trust range, IPV4 network range\",\n\t\t\t// CIDR Notation: 8.8.8.8/24\n\t\t\t// Address:       8.8.8.8\n\t\t\t// Range start:   8.8.8.0\n\t\t\t// Range end:     8.8.8.255\n\t\t\tgivenRange: \"8.8.8.0/24\",\n\t\t\twhenIP:     \"8.8.8.8\",\n\t\t\texpect:     true,\n\t\t},\n\t\t{\n\t\t\tname: \"ip is within trust range, IPV4 network range\",\n\t\t\t// CIDR Notation: 8.8.8.8/24\n\t\t\t// Address:       8.8.8.8\n\t\t\t// Range start:   8.8.8.0\n\t\t\t// Range end:     8.8.8.255\n\t\t\tgivenRange: \"8.8.8.0/24\",\n\t\t\twhenIP:     \"8.8.8.8\",\n\t\t\texpect:     true,\n\t\t},\n\t\t{\n\t\t\tname:       \"ip is outside (upper bounds) of trust range, IPV4 network range\",\n\t\t\tgivenRange: \"8.8.8.0/24\",\n\t\t\twhenIP:     \"8.8.9.0\",\n\t\t\texpect:     false,\n\t\t},\n\t\t{\n\t\t\tname:       \"ip is outside (lower bounds) of trust range, IPV4 network range\",\n\t\t\tgivenRange: \"8.8.8.0/24\",\n\t\t\twhenIP:     \"8.8.7.255\",\n\t\t\texpect:     false,\n\t\t},\n\t\t{\n\t\t\tname:       \"public ip, trust everything in IPV4 network range\",\n\t\t\tgivenRange: \"0.0.0.0/0\",\n\t\t\twhenIP:     \"8.8.8.8\",\n\t\t\texpect:     true,\n\t\t},\n\t\t{\n\t\t\tname:       \"internal ip, trust everything in IPV4 network range\",\n\t\t\tgivenRange: \"0.0.0.0/0\",\n\t\t\twhenIP:     \"127.0.10.1\",\n\t\t\texpect:     true,\n\t\t},\n\t\t{\n\t\t\tname:       \"public ip, trust everything in IPV6 network range\",\n\t\t\tgivenRange: \"::/0\",\n\t\t\twhenIP:     \"2a00:1450:4026:805::200e\",\n\t\t\texpect:     true,\n\t\t},\n\t\t{\n\t\t\tname:       \"internal ip, trust everything in IPV6 network range\",\n\t\t\tgivenRange: \"::/0\",\n\t\t\twhenIP:     \"0:0:0:0:0:0:0:1\",\n\t\t\texpect:     true,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tcidr := mustParseCIDR(tc.givenRange)\n\t\t\tchecker := newIPChecker([]TrustOption{\n\t\t\t\tTrustLoopback(false),   // disable to avoid interference\n\t\t\t\tTrustLinkLocal(false),  // disable to avoid interference\n\t\t\t\tTrustPrivateNet(false), // disable to avoid interference\n\n\t\t\t\tTrustIPRange(cidr),\n\t\t\t})\n\n\t\t\tresult := checker.trust(net.ParseIP(tc.whenIP))\n\t\t\tassert.Equal(t, tc.expect, result)\n\t\t})\n\t}\n}\n\nfunc TestTrustPrivateNet(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname   string\n\t\twhenIP string\n\t\texpect bool\n\t}{\n\t\t{\n\t\t\tname:   \"do not trust public IPv4 address\",\n\t\t\twhenIP: \"8.8.8.8\",\n\t\t\texpect: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"do not trust public IPv6 address\",\n\t\t\twhenIP: \"2a00:1450:4026:805::200e\",\n\t\t\texpect: false,\n\t\t},\n\n\t\t{ // Class A: 10.0.0.0 — 10.255.255.255\n\t\t\tname:   \"do not trust IPv4 just outside of class A (lower bounds)\",\n\t\t\twhenIP: \"9.255.255.255\",\n\t\t\texpect: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"do not trust IPv4 just outside of class A (upper bounds)\",\n\t\t\twhenIP: \"11.0.0.0\",\n\t\t\texpect: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"trust IPv4 of class A (lower bounds)\",\n\t\t\twhenIP: \"10.0.0.0\",\n\t\t\texpect: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"trust IPv4 of class A (upper bounds)\",\n\t\t\twhenIP: \"10.255.255.255\",\n\t\t\texpect: true,\n\t\t},\n\n\t\t{ // Class B: 172.16.0.0 — 172.31.255.255\n\t\t\tname:   \"do not trust IPv4 just outside of class B (lower bounds)\",\n\t\t\twhenIP: \"172.15.255.255\",\n\t\t\texpect: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"do not trust IPv4 just outside of class B (upper bounds)\",\n\t\t\twhenIP: \"172.32.0.0\",\n\t\t\texpect: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"trust IPv4 of class B (lower bounds)\",\n\t\t\twhenIP: \"172.16.0.0\",\n\t\t\texpect: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"trust IPv4 of class B (upper bounds)\",\n\t\t\twhenIP: \"172.31.255.255\",\n\t\t\texpect: true,\n\t\t},\n\n\t\t{ // Class C: 192.168.0.0 — 192.168.255.255\n\t\t\tname:   \"do not trust IPv4 just outside of class C (lower bounds)\",\n\t\t\twhenIP: \"192.167.255.255\",\n\t\t\texpect: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"do not trust IPv4 just outside of class C (upper bounds)\",\n\t\t\twhenIP: \"192.169.0.0\",\n\t\t\texpect: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"trust IPv4 of class C (lower bounds)\",\n\t\t\twhenIP: \"192.168.0.0\",\n\t\t\texpect: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"trust IPv4 of class C (upper bounds)\",\n\t\t\twhenIP: \"192.168.255.255\",\n\t\t\texpect: true,\n\t\t},\n\n\t\t{ // fc00::/7 address block = RFC 4193 Unique Local Addresses (ULA)\n\t\t\t// splits the address block in two equally sized halves, fc00::/8 and fd00::/8.\n\t\t\t// https://en.wikipedia.org/wiki/Unique_local_address\n\t\t\tname:   \"trust IPv6 private address\",\n\t\t\twhenIP: \"fdfc:3514:2cb3:4bd5::\",\n\t\t\texpect: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"do not trust IPv6 just out of /fd (upper bounds)\",\n\t\t\twhenIP: \"/fe00:0000:0000:0000:0000\",\n\t\t\texpect: false,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tchecker := newIPChecker([]TrustOption{\n\t\t\t\tTrustLoopback(false),  // disable to avoid interference\n\t\t\t\tTrustLinkLocal(false), // disable to avoid interference\n\n\t\t\t\tTrustPrivateNet(true),\n\t\t\t})\n\n\t\t\tresult := checker.trust(net.ParseIP(tc.whenIP))\n\t\t\tassert.Equal(t, tc.expect, result)\n\t\t})\n\t}\n}\n\nfunc TestTrustLinkLocal(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname   string\n\t\twhenIP string\n\t\texpect bool\n\t}{\n\t\t{\n\t\t\tname:   \"trust link local IPv4 address (lower bounds)\",\n\t\t\twhenIP: \"169.254.0.0\",\n\t\t\texpect: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"trust link local  IPv4 address (upper bounds)\",\n\t\t\twhenIP: \"169.254.255.255\",\n\t\t\texpect: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"do not trust link local IPv4 address (outside of lower bounds)\",\n\t\t\twhenIP: \"169.253.255.255\",\n\t\t\texpect: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"do not trust link local  IPv4 address (outside of upper bounds)\",\n\t\t\twhenIP: \"169.255.0.0\",\n\t\t\texpect: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"trust link local IPv6 address \",\n\t\t\twhenIP: \"fe80::1\",\n\t\t\texpect: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"do not trust link local IPv6 address \",\n\t\t\twhenIP: \"fec0::1\",\n\t\t\texpect: false,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tchecker := newIPChecker([]TrustOption{\n\t\t\t\tTrustLoopback(false),   // disable to avoid interference\n\t\t\t\tTrustPrivateNet(false), // disable to avoid interference\n\n\t\t\t\tTrustLinkLocal(true),\n\t\t\t})\n\n\t\t\tresult := checker.trust(net.ParseIP(tc.whenIP))\n\t\t\tassert.Equal(t, tc.expect, result)\n\t\t})\n\t}\n}\n\nfunc TestTrustLoopback(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname   string\n\t\twhenIP string\n\t\texpect bool\n\t}{\n\t\t{\n\t\t\tname:   \"trust IPv4 as localhost\",\n\t\t\twhenIP: \"127.0.0.1\",\n\t\t\texpect: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"trust IPv6 as localhost\",\n\t\t\twhenIP: \"::1\",\n\t\t\texpect: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"do not trust public ip as localhost\",\n\t\t\twhenIP: \"8.8.8.8\",\n\t\t\texpect: false,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tchecker := newIPChecker([]TrustOption{\n\t\t\t\tTrustLinkLocal(false),  // disable to avoid interference\n\t\t\t\tTrustPrivateNet(false), // disable to avoid interference\n\n\t\t\t\tTrustLoopback(true),\n\t\t\t})\n\n\t\t\tresult := checker.trust(net.ParseIP(tc.whenIP))\n\t\t\tassert.Equal(t, tc.expect, result)\n\t\t})\n\t}\n}\n\nfunc TestExtractIPDirect(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname        string\n\t\twhenRequest http.Request\n\t\texpectIP    string\n\t}{\n\t\t{\n\t\t\tname: \"request has no headers, extracts IP from request remote addr\",\n\t\t\twhenRequest: http.Request{\n\t\t\t\tRemoteAddr: \"203.0.113.1:8080\",\n\t\t\t},\n\t\t\texpectIP: \"203.0.113.1\",\n\t\t},\n\t\t{\n\t\t\tname: \"remote addr is IP without port, extracts IP directly\",\n\t\t\twhenRequest: http.Request{\n\t\t\t\tRemoteAddr: \"203.0.113.1\",\n\t\t\t},\n\t\t\texpectIP: \"203.0.113.1\",\n\t\t},\n\t\t{\n\t\t\tname: \"remote addr is IPv6 without port, extracts IP directly\",\n\t\t\twhenRequest: http.Request{\n\t\t\t\tRemoteAddr: \"2001:db8::1\",\n\t\t\t},\n\t\t\texpectIP: \"2001:db8::1\",\n\t\t},\n\t\t{\n\t\t\tname: \"remote addr is IPv6 with port\",\n\t\t\twhenRequest: http.Request{\n\t\t\t\tRemoteAddr: \"[2001:db8::1]:8080\",\n\t\t\t},\n\t\t\texpectIP: \"2001:db8::1\",\n\t\t},\n\t\t{\n\t\t\tname: \"remote addr is invalid, returns empty string\",\n\t\t\twhenRequest: http.Request{\n\t\t\t\tRemoteAddr: \"invalid-ip-format\",\n\t\t\t},\n\t\t\texpectIP: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"request is from external IP has X-Real-Ip header, extractor still extracts IP from request remote addr\",\n\t\t\twhenRequest: http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\tHeaderXRealIP: []string{\"203.0.113.10\"},\n\t\t\t\t},\n\t\t\t\tRemoteAddr: \"203.0.113.1:8080\",\n\t\t\t},\n\t\t\texpectIP: \"203.0.113.1\",\n\t\t},\n\t\t{\n\t\t\tname: \"request is from internal IP and has Real-IP header, extractor still extracts internal IP from request remote addr\",\n\t\t\twhenRequest: http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\tHeaderXRealIP: []string{\"203.0.113.10\"},\n\t\t\t\t},\n\t\t\t\tRemoteAddr: \"127.0.0.1:8080\",\n\t\t\t},\n\t\t\texpectIP: \"127.0.0.1\",\n\t\t},\n\t\t{\n\t\t\tname: \"request is from external IP and has XFF + Real-IP header, extractor still extracts external IP from request remote addr\",\n\t\t\twhenRequest: http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\tHeaderXRealIP:       []string{\"203.0.113.10\"},\n\t\t\t\t\tHeaderXForwardedFor: []string{\"192.0.2.106, 198.51.100.105, fc00::104, 2001:db8::103, 192.168.0.102, 169.254.0.101\"},\n\t\t\t\t},\n\t\t\t\tRemoteAddr: \"203.0.113.1:8080\",\n\t\t\t},\n\t\t\texpectIP: \"203.0.113.1\",\n\t\t},\n\t\t{\n\t\t\tname: \"request is from internal IP and has XFF + Real-IP header, extractor still extracts internal IP from request remote addr\",\n\t\t\twhenRequest: http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\tHeaderXRealIP:       []string{\"127.0.0.1\"},\n\t\t\t\t\tHeaderXForwardedFor: []string{\"192.0.2.106, 198.51.100.105, fc00::104, 2001:db8::103, 192.168.0.102, 169.254.0.101\"},\n\t\t\t\t},\n\t\t\t\tRemoteAddr: \"127.0.0.1:8080\",\n\t\t\t},\n\t\t\texpectIP: \"127.0.0.1\",\n\t\t},\n\t\t{\n\t\t\tname: \"request is from external IP and has XFF header, extractor still extracts external IP from request remote addr\",\n\t\t\twhenRequest: http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\tHeaderXForwardedFor: []string{\"192.0.2.106, 198.51.100.105, fc00::104, 2001:db8::103, 192.168.0.102, 169.254.0.101\"},\n\t\t\t\t},\n\t\t\t\tRemoteAddr: \"203.0.113.1:8080\",\n\t\t\t},\n\t\t\texpectIP: \"203.0.113.1\",\n\t\t},\n\t\t{\n\t\t\tname: \"request is from internal IP and has XFF header, extractor still extracts internal IP from request remote addr\",\n\t\t\twhenRequest: http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\tHeaderXForwardedFor: []string{\"192.0.2.106, 198.51.100.105, fc00::104, 2001:db8::103, 192.168.0.102, 169.254.0.101\"},\n\t\t\t\t},\n\t\t\t\tRemoteAddr: \"127.0.0.1:8080\",\n\t\t\t},\n\t\t\texpectIP: \"127.0.0.1\",\n\t\t},\n\t\t{\n\t\t\tname: \"request is from internal IP and has INVALID XFF header, extractor still extracts internal IP from request remote addr\",\n\t\t\twhenRequest: http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\tHeaderXForwardedFor: []string{\"this.is.broken.lol, 169.254.0.101\"},\n\t\t\t\t},\n\t\t\t\tRemoteAddr: \"127.0.0.1:8080\",\n\t\t\t},\n\t\t\texpectIP: \"127.0.0.1\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\textractedIP := ExtractIPDirect()(&tc.whenRequest)\n\t\t\tassert.Equal(t, tc.expectIP, extractedIP)\n\t\t})\n\t}\n}\n\nfunc TestExtractIPFromRealIPHeader(t *testing.T) {\n\t_, ipForRemoteAddrExternalRange, _ := net.ParseCIDR(\"203.0.113.199/24\")\n\t_, ipv6ForRemoteAddrExternalRange, _ := net.ParseCIDR(\"2001:db8::/64\")\n\n\tvar testCases = []struct {\n\t\twhenRequest       http.Request\n\t\tname              string\n\t\texpectIP          string\n\t\tgivenTrustOptions []TrustOption\n\t}{\n\t\t{\n\t\t\tname: \"request has no headers, extracts IP from request remote addr\",\n\t\t\twhenRequest: http.Request{\n\t\t\t\tRemoteAddr: \"203.0.113.1:8080\",\n\t\t\t},\n\t\t\texpectIP: \"203.0.113.1\",\n\t\t},\n\t\t{\n\t\t\tname: \"request is from external IP has INVALID external X-Real-Ip header, extract IP from remote addr\",\n\t\t\twhenRequest: http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\tHeaderXRealIP: []string{\"xxx.yyy.zzz.ccc\"}, // <-- this is invalid\n\t\t\t\t},\n\t\t\t\tRemoteAddr: \"203.0.113.1:8080\",\n\t\t\t},\n\t\t\texpectIP: \"203.0.113.1\",\n\t\t},\n\t\t{\n\t\t\tname: \"request is from external IP has valid + UNTRUSTED external X-Real-Ip header, extract IP from remote addr\",\n\t\t\twhenRequest: http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\tHeaderXRealIP: []string{\"203.0.113.199\"}, // <-- this is untrusted\n\t\t\t\t},\n\t\t\t\tRemoteAddr: \"203.0.113.1:8080\",\n\t\t\t},\n\t\t\texpectIP: \"203.0.113.1\",\n\t\t},\n\t\t{\n\t\t\tname: \"request is from external IP has valid + UNTRUSTED external X-Real-Ip header, extract IP from remote addr\",\n\t\t\twhenRequest: http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\tHeaderXRealIP: []string{\"[2001:db8::113:199]\"}, // <-- this is untrusted\n\t\t\t\t},\n\t\t\t\tRemoteAddr: \"[2001:db8::113:1]:8080\",\n\t\t\t},\n\t\t\texpectIP: \"2001:db8::113:1\",\n\t\t},\n\t\t{\n\t\t\tname: \"request is from external IP has valid + TRUSTED X-Real-Ip header, extract IP from X-Real-Ip header\",\n\t\t\tgivenTrustOptions: []TrustOption{ // case for \"trust direct-facing proxy\"\n\t\t\t\tTrustIPRange(ipForRemoteAddrExternalRange), // we trust external IP range \"203.0.113.199/24\"\n\t\t\t},\n\t\t\twhenRequest: http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\tHeaderXRealIP: []string{\"203.0.113.199\"},\n\t\t\t\t},\n\t\t\t\tRemoteAddr: \"203.0.113.1:8080\",\n\t\t\t},\n\t\t\texpectIP: \"203.0.113.199\",\n\t\t},\n\t\t{\n\t\t\tname: \"request is from external IP has valid + TRUSTED X-Real-Ip header, extract IP from X-Real-Ip header\",\n\t\t\tgivenTrustOptions: []TrustOption{ // case for \"trust direct-facing proxy\"\n\t\t\t\tTrustIPRange(ipv6ForRemoteAddrExternalRange), // we trust external IP range \"2001:db8::/64\"\n\t\t\t},\n\t\t\twhenRequest: http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\tHeaderXRealIP: []string{\"[2001:db8::113:199]\"},\n\t\t\t\t},\n\t\t\t\tRemoteAddr: \"[2001:db8::113:1]:8080\",\n\t\t\t},\n\t\t\texpectIP: \"2001:db8::113:199\",\n\t\t},\n\t\t{\n\t\t\tname: \"request is from external IP has XFF and valid + TRUSTED X-Real-Ip header, extract IP from X-Real-Ip header\",\n\t\t\tgivenTrustOptions: []TrustOption{ // case for \"trust direct-facing proxy\"\n\t\t\t\tTrustIPRange(ipForRemoteAddrExternalRange), // we trust external IP range \"203.0.113.199/24\"\n\t\t\t},\n\t\t\twhenRequest: http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\tHeaderXRealIP:       []string{\"203.0.113.199\"},\n\t\t\t\t\tHeaderXForwardedFor: []string{\"203.0.113.198, 203.0.113.197\"}, // <-- should not affect anything\n\t\t\t\t},\n\t\t\t\tRemoteAddr: \"203.0.113.1:8080\",\n\t\t\t},\n\t\t\texpectIP: \"203.0.113.199\",\n\t\t},\n\t\t{\n\t\t\tname: \"request is from external IP has XFF and valid + TRUSTED X-Real-Ip header, extract IP from X-Real-Ip header\",\n\t\t\tgivenTrustOptions: []TrustOption{ // case for \"trust direct-facing proxy\"\n\t\t\t\tTrustIPRange(ipv6ForRemoteAddrExternalRange), // we trust external IP range \"2001:db8::/64\"\n\t\t\t},\n\t\t\twhenRequest: http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\tHeaderXRealIP:       []string{\"[2001:db8::113:199]\"},\n\t\t\t\t\tHeaderXForwardedFor: []string{\"[2001:db8::113:198], [2001:db8::113:197]\"}, // <-- should not affect anything\n\t\t\t\t},\n\t\t\t\tRemoteAddr: \"[2001:db8::113:1]:8080\",\n\t\t\t},\n\t\t\texpectIP: \"2001:db8::113:199\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\textractedIP := ExtractIPFromRealIPHeader(tc.givenTrustOptions...)(&tc.whenRequest)\n\t\t\tassert.Equal(t, tc.expectIP, extractedIP)\n\t\t})\n\t}\n}\n\nfunc TestExtractIPFromXFFHeader(t *testing.T) {\n\t_, ipForRemoteAddrExternalRange, _ := net.ParseCIDR(\"203.0.113.199/24\")\n\t_, ipv6ForRemoteAddrExternalRange, _ := net.ParseCIDR(\"2001:db8::/64\")\n\n\tvar testCases = []struct {\n\t\twhenRequest       http.Request\n\t\tname              string\n\t\texpectIP          string\n\t\tgivenTrustOptions []TrustOption\n\t}{\n\t\t{\n\t\t\tname: \"request has no headers, extracts IP from request remote addr\",\n\t\t\twhenRequest: http.Request{\n\t\t\t\tRemoteAddr: \"203.0.113.1:8080\",\n\t\t\t},\n\t\t\texpectIP: \"203.0.113.1\",\n\t\t},\n\t\t{\n\t\t\tname: \"request has INVALID external XFF header, extract IP from remote addr\",\n\t\t\twhenRequest: http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\tHeaderXForwardedFor: []string{\"xxx.yyy.zzz.ccc, 127.0.0.2\"}, // <-- this is invalid\n\t\t\t\t},\n\t\t\t\tRemoteAddr: \"127.0.0.1:8080\",\n\t\t\t},\n\t\t\texpectIP: \"127.0.0.1\",\n\t\t},\n\t\t{\n\t\t\tname: \"request trusts all IPs in XFF header, extract IP from furthest in XFF chain\",\n\t\t\twhenRequest: http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\tHeaderXForwardedFor: []string{\"127.0.0.3, 127.0.0.2, 127.0.0.1\"},\n\t\t\t\t},\n\t\t\t\tRemoteAddr: \"127.0.0.1:8080\",\n\t\t\t},\n\t\t\texpectIP: \"127.0.0.3\",\n\t\t},\n\t\t{\n\t\t\tname: \"request trusts all IPs in XFF header, extract IP from furthest in XFF chain\",\n\t\t\twhenRequest: http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\tHeaderXForwardedFor: []string{\"[fe80::3], [fe80::2], [fe80::1]\"},\n\t\t\t\t},\n\t\t\t\tRemoteAddr: \"[fe80::1]:8080\",\n\t\t\t},\n\t\t\texpectIP: \"fe80::3\",\n\t\t},\n\t\t{\n\t\t\tname: \"request is from external IP has valid + UNTRUSTED external XFF header, extract IP from remote addr\",\n\t\t\twhenRequest: http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\tHeaderXForwardedFor: []string{\"203.0.113.199\"}, // <-- this is untrusted\n\t\t\t\t},\n\t\t\t\tRemoteAddr: \"203.0.113.1:8080\",\n\t\t\t},\n\t\t\texpectIP: \"203.0.113.1\",\n\t\t},\n\t\t{\n\t\t\tname: \"request is from external IP has valid + UNTRUSTED external XFF header, extract IP from remote addr\",\n\t\t\twhenRequest: http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\tHeaderXForwardedFor: []string{\"[2001:db8::1]\"}, // <-- this is untrusted\n\t\t\t\t},\n\t\t\t\tRemoteAddr: \"[2001:db8::2]:8080\",\n\t\t\t},\n\t\t\texpectIP: \"2001:db8::2\",\n\t\t},\n\t\t{\n\t\t\tname: \"request is from external IP is valid and has some IPs TRUSTED XFF header, extract IP from XFF header\",\n\t\t\tgivenTrustOptions: []TrustOption{\n\t\t\t\tTrustIPRange(ipForRemoteAddrExternalRange), // we trust external IP range \"203.0.113.199/24\"\n\t\t\t},\n\t\t\t// from request its seems that request has been proxied through 6 servers.\n\t\t\t// 1) 203.0.1.100 (this is external IP set by 203.0.100.100 which we do not trust - could be spoofed)\n\t\t\t// 2) 203.0.100.100 (this is outside of our network but set by 203.0.113.199 which we trust to set correct IPs)\n\t\t\t// 3) 203.0.113.199 (we trust, for example maybe our proxy from some other office)\n\t\t\t// 4) 192.168.1.100 (internal IP, some internal upstream loadbalancer ala SSL offloading with F5 products)\n\t\t\t// 5) 127.0.0.1 (is proxy on localhost. maybe we have Nginx in front of our Echo instance doing some routing)\n\t\t\twhenRequest: http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\tHeaderXForwardedFor: []string{\"203.0.1.100, 203.0.100.100, 203.0.113.199, 192.168.1.100\"},\n\t\t\t\t},\n\t\t\t\tRemoteAddr: \"127.0.0.1:8080\", // IP of proxy upstream of our APP\n\t\t\t},\n\t\t\texpectIP: \"203.0.100.100\", // this is first trusted IP in XFF chain\n\t\t},\n\t\t{\n\t\t\tname: \"request is from external IP is valid and has some IPs TRUSTED XFF header, extract IP from XFF header\",\n\t\t\tgivenTrustOptions: []TrustOption{\n\t\t\t\tTrustIPRange(ipv6ForRemoteAddrExternalRange), // we trust external IP range \"2001:db8::/64\"\n\t\t\t},\n\t\t\t// from request its seems that request has been proxied through 6 servers.\n\t\t\t// 1) 2001:db8:1::1:100 (this is external IP set by 2001:db8:2::100:100 which we do not trust - could be spoofed)\n\t\t\t// 2) 2001:db8:2::100:100  (this is outside of our network but set by 2001:db8::113:199 which we trust to set correct IPs)\n\t\t\t// 3) 2001:db8::113:199 (we trust, for example maybe our proxy from some other office)\n\t\t\t// 4) fd12:3456:789a:1::1 (internal IP, some internal upstream loadbalancer ala SSL offloading with F5 products)\n\t\t\t// 5) fe80::1 (is proxy on localhost. maybe we have Nginx in front of our Echo instance doing some routing)\n\t\t\twhenRequest: http.Request{\n\t\t\t\tHeader: http.Header{\n\t\t\t\t\tHeaderXForwardedFor: []string{\"[2001:db8:1::1:100], [2001:db8:2::100:100], [2001:db8::113:199], [fd12:3456:789a:1::1]\"},\n\t\t\t\t},\n\t\t\t\tRemoteAddr: \"[fe80::1]:8080\", // IP of proxy upstream of our APP\n\t\t\t},\n\t\t\texpectIP: \"2001:db8:2::100:100\", // this is first trusted IP in XFF chain\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\textractedIP := ExtractIPFromXFFHeader(tc.givenTrustOptions...)(&tc.whenRequest)\n\t\t\tassert.Equal(t, tc.expectIP, extractedIP)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "json.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"encoding/json\"\n)\n\n// DefaultJSONSerializer implements JSON encoding using encoding/json.\ntype DefaultJSONSerializer struct{}\n\n// Serialize converts an interface into a json and writes it to the response.\n// You can optionally use the indent parameter to produce pretty JSONs.\nfunc (d DefaultJSONSerializer) Serialize(c *Context, target any, indent string) error {\n\tenc := json.NewEncoder(c.Response())\n\tif indent != \"\" {\n\t\tenc.SetIndent(\"\", indent)\n\t}\n\treturn enc.Encode(target)\n}\n\n// Deserialize reads a JSON from a request body and converts it into an interface.\nfunc (d DefaultJSONSerializer) Deserialize(c *Context, target any) error {\n\tif err := json.NewDecoder(c.Request().Body).Decode(target); err != nil {\n\t\treturn ErrBadRequest.Wrap(err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "json_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"github.com/stretchr/testify/assert\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n)\n\n// Note this test is deliberately simple as there's not a lot to test.\n// Just need to ensure it writes JSONs. The heavy work is done by the context methods.\nfunc TestDefaultJSONCodec_Encode(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodPost, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\t// Echo\n\tassert.Equal(t, e, c.Echo())\n\n\t// Request\n\tassert.NotNil(t, c.Request())\n\n\t// Response\n\tassert.NotNil(t, c.Response())\n\n\t//--------\n\t// Default JSON encoder\n\t//--------\n\n\tenc := new(DefaultJSONSerializer)\n\n\terr := enc.Serialize(c, user{ID: 1, Name: \"Jon Snow\"}, \"\")\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, userJSON+\"\\n\", rec.Body.String())\n\t}\n\n\treq = httptest.NewRequest(http.MethodPost, \"/\", nil)\n\trec = httptest.NewRecorder()\n\tc = e.NewContext(req, rec)\n\terr = enc.Serialize(c, user{ID: 1, Name: \"Jon Snow\"}, \"  \")\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, userJSONPretty+\"\\n\", rec.Body.String())\n\t}\n}\n\n// Note this test is deliberately simple as there's not a lot to test.\n// Just need to ensure it writes JSONs. The heavy work is done by the context methods.\nfunc TestDefaultJSONCodec_Decode(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(userJSON))\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\t// Echo\n\tassert.Equal(t, e, c.Echo())\n\n\t// Request\n\tassert.NotNil(t, c.Request())\n\n\t// Response\n\tassert.NotNil(t, c.Response())\n\n\t//--------\n\t// Default JSON encoder\n\t//--------\n\n\tenc := new(DefaultJSONSerializer)\n\n\tvar u = user{}\n\terr := enc.Deserialize(c, &u)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, u, user{ID: 1, Name: \"Jon Snow\"})\n\t}\n\n\tvar userUnmarshalSyntaxError = user{}\n\treq = httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(invalidContent))\n\trec = httptest.NewRecorder()\n\tc = e.NewContext(req, rec)\n\terr = enc.Deserialize(c, &userUnmarshalSyntaxError)\n\tassert.IsType(t, &HTTPError{}, err)\n\tassert.EqualError(t, err, \"code=400, message=Bad Request, err=invalid character 'i' looking for beginning of value\")\n\n\tvar userUnmarshalTypeError = struct {\n\t\tID   string `json:\"id\"`\n\t\tName string `json:\"name\"`\n\t}{}\n\n\treq = httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(userJSON))\n\trec = httptest.NewRecorder()\n\tc = e.NewContext(req, rec)\n\terr = enc.Deserialize(c, &userUnmarshalTypeError)\n\tassert.IsType(t, &HTTPError{}, err)\n\tassert.EqualError(t, err, \"code=400, message=Bad Request, err=json: cannot unmarshal number into Go struct field .id of type string\")\n\n}\n"
  },
  {
    "path": "middleware/DEVELOPMENT.md",
    "content": "# Development Guidelines for middlewares\n\n## Best practices:\n\n* Do not use `panic` in middleware creator functions in case of invalid configuration.\n* In case of an error in middleware function handling request avoid using `c.Error()` and returning no error instead\n  because previous middlewares up in call chain could have logic for dealing with returned errors.\n* Create middleware configuration structs that implement `MiddlewareConfigurator` interface so can decide if they\n  want to create middleware with panics or with returning errors on configuration errors.\n* When adding `echo.Context` to function type or fields make it first parameter so all functions with Context looks same.\n\n"
  },
  {
    "path": "middleware/basic_auth.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"bytes\"\n\t\"cmp\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\n// BasicAuthConfig defines the config for BasicAuthWithConfig middleware.\n//\n// SECURITY: The Validator function is responsible for securely comparing credentials.\n// See BasicAuthValidator documentation for guidance on preventing timing attacks.\ntype BasicAuthConfig struct {\n\t// Skipper defines a function to skip middleware.\n\tSkipper Skipper\n\n\t// Validator is a function to validate BasicAuthWithConfig credentials. Note: if request contains multiple basic auth headers\n\t// this function would be called once for each header until first valid result is returned\n\t// Required.\n\tValidator BasicAuthValidator\n\n\t// Realm is a string to define realm attribute of BasicAuthWithConfig.\n\t// Default value \"Restricted\".\n\tRealm string\n\n\t// AllowedCheckLimit set how many headers are allowed to be checked. This is useful\n\t// environments like corporate test environments with application proxies restricting\n\t// access to environment with their own auth scheme.\n\t// Defaults to 1.\n\tAllowedCheckLimit uint\n}\n\n// BasicAuthValidator defines a function to validate BasicAuthWithConfig credentials.\n//\n// SECURITY WARNING: To prevent timing attacks that could allow attackers to enumerate\n// valid usernames or passwords, validator implementations MUST use constant-time\n// comparison for credential checking. Use crypto/subtle.ConstantTimeCompare instead\n// of standard string equality (==) or switch statements.\n//\n// Example of SECURE implementation:\n//\n//\timport \"crypto/subtle\"\n//\n//\tvalidator := func(c *echo.Context, username, password string) (bool, error) {\n//\t    // Fetch expected credentials from database/config\n//\t    expectedUser := \"admin\"\n//\t    expectedPass := \"secretpassword\"\n//\n//\t    // Use constant-time comparison to prevent timing attacks\n//\t    userMatch := subtle.ConstantTimeCompare([]byte(username), []byte(expectedUser)) == 1\n//\t    passMatch := subtle.ConstantTimeCompare([]byte(password), []byte(expectedPass)) == 1\n//\n//\t    if userMatch && passMatch {\n//\t        return true, nil\n//\t    }\n//\t    return false, nil\n//\t}\n//\n// Example of INSECURE implementation (DO NOT USE):\n//\n//\t// VULNERABLE TO TIMING ATTACKS - DO NOT USE\n//\tvalidator := func(c *echo.Context, username, password string) (bool, error) {\n//\t    if username == \"admin\" && password == \"secret\" {  // Timing leak!\n//\t        return true, nil\n//\t    }\n//\t    return false, nil\n//\t}\ntype BasicAuthValidator func(c *echo.Context, user string, password string) (bool, error)\n\nconst (\n\tbasic        = \"basic\"\n\tdefaultRealm = \"Restricted\"\n)\n\n// BasicAuth returns an BasicAuth middleware.\n//\n// For valid credentials it calls the next handler.\n// For missing or invalid credentials, it sends \"401 - Unauthorized\" response.\nfunc BasicAuth(fn BasicAuthValidator) echo.MiddlewareFunc {\n\treturn BasicAuthWithConfig(BasicAuthConfig{Validator: fn})\n}\n\n// BasicAuthWithConfig returns an BasicAuthWithConfig middleware with config.\nfunc BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc {\n\treturn toMiddlewareOrPanic(config)\n}\n\n// ToMiddleware converts BasicAuthConfig to middleware or returns an error for invalid configuration\nfunc (config BasicAuthConfig) ToMiddleware() (echo.MiddlewareFunc, error) {\n\tif config.Validator == nil {\n\t\treturn nil, errors.New(\"echo basic-auth middleware requires a validator function\")\n\t}\n\tif config.Skipper == nil {\n\t\tconfig.Skipper = DefaultSkipper\n\t}\n\trealm := defaultRealm\n\tif config.Realm != \"\" {\n\t\trealm = config.Realm\n\t}\n\trealm = strconv.Quote(realm)\n\tlimit := cmp.Or(config.AllowedCheckLimit, 1)\n\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) error {\n\t\t\tif config.Skipper(c) {\n\t\t\t\treturn next(c)\n\t\t\t}\n\n\t\t\tvar lastError error\n\t\t\tl := len(basic)\n\t\t\ti := uint(0)\n\t\t\tfor _, auth := range c.Request().Header[echo.HeaderAuthorization] {\n\t\t\t\tif i >= limit {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tif !(len(auth) > l+1 && strings.EqualFold(auth[:l], basic)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\ti++\n\n\t\t\t\t// Invalid base64 shouldn't be treated as error\n\t\t\t\t// instead should be treated as invalid client input\n\t\t\t\tb, errDecode := base64.StdEncoding.DecodeString(auth[l+1:])\n\t\t\t\tif errDecode != nil {\n\t\t\t\t\tlastError = echo.ErrBadRequest.Wrap(errDecode)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tidx := bytes.IndexByte(b, ':')\n\t\t\t\tif idx >= 0 {\n\t\t\t\t\tvalid, errValidate := config.Validator(c, string(b[:idx]), string(b[idx+1:]))\n\t\t\t\t\tif errValidate != nil {\n\t\t\t\t\t\tlastError = errValidate\n\t\t\t\t\t} else if valid {\n\t\t\t\t\t\treturn next(c)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif lastError != nil {\n\t\t\t\treturn lastError\n\t\t\t}\n\n\t\t\t// Need to return `401` for browsers to pop-up login box.\n\t\t\tc.Response().Header().Set(echo.HeaderWWWAuthenticate, basic+\" realm=\"+realm)\n\t\t\treturn echo.ErrUnauthorized\n\t\t}\n\t}, nil\n}\n"
  },
  {
    "path": "middleware/basic_auth_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"crypto/subtle\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestBasicAuth(t *testing.T) {\n\tvalidatorFunc := func(c *echo.Context, u, p string) (bool, error) {\n\t\t// Use constant-time comparison to prevent timing attacks\n\t\tuserMatch := subtle.ConstantTimeCompare([]byte(u), []byte(\"joe\")) == 1\n\t\tpassMatch := subtle.ConstantTimeCompare([]byte(p), []byte(\"secret\")) == 1\n\n\t\tif userMatch && passMatch {\n\t\t\treturn true, nil\n\t\t}\n\n\t\t// Special case for testing error handling\n\t\tif u == \"error\" {\n\t\t\treturn false, errors.New(p)\n\t\t}\n\n\t\treturn false, nil\n\t}\n\tdefaultConfig := BasicAuthConfig{Validator: validatorFunc}\n\n\tvar testCases = []struct {\n\t\tname         string\n\t\tgivenConfig  BasicAuthConfig\n\t\twhenAuth     []string\n\t\texpectHeader string\n\t\texpectErr    string\n\t}{\n\t\t{\n\t\t\tname:        \"ok\",\n\t\t\tgivenConfig: defaultConfig,\n\t\t\twhenAuth:    []string{basic + \" \" + base64.StdEncoding.EncodeToString([]byte(\"joe:secret\"))},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, multiple\",\n\t\t\tgivenConfig: BasicAuthConfig{Validator: validatorFunc, AllowedCheckLimit: 2},\n\t\t\twhenAuth: []string{\n\t\t\t\t\"Bearer \" + base64.StdEncoding.EncodeToString([]byte(\"token\")),\n\t\t\t\tbasic + \" NOT_BASE64\",\n\t\t\t\tbasic + \" \" + base64.StdEncoding.EncodeToString([]byte(\"joe:secret\")),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, multiple, valid out of limit\",\n\t\t\tgivenConfig: BasicAuthConfig{Validator: validatorFunc, AllowedCheckLimit: 1},\n\t\t\twhenAuth: []string{\n\t\t\t\t\"Bearer \" + base64.StdEncoding.EncodeToString([]byte(\"token\")),\n\t\t\t\tbasic + \" \" + base64.StdEncoding.EncodeToString([]byte(\"joe:invalid_password\")),\n\t\t\t\t// limit only check first and should not check auth below\n\t\t\t\tbasic + \" \" + base64.StdEncoding.EncodeToString([]byte(\"joe:secret\")),\n\t\t\t},\n\t\t\texpectHeader: basic + ` realm=\"Restricted\"`,\n\t\t\texpectErr:    \"Unauthorized\",\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, invalid Authorization header\",\n\t\t\tgivenConfig:  defaultConfig,\n\t\t\twhenAuth:     []string{strings.ToUpper(basic) + \" \" + base64.StdEncoding.EncodeToString([]byte(\"invalid\"))},\n\t\t\texpectHeader: basic + ` realm=\"Restricted\"`,\n\t\t\texpectErr:    \"Unauthorized\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, not base64 Authorization header\",\n\t\t\tgivenConfig: defaultConfig,\n\t\t\twhenAuth:    []string{strings.ToUpper(basic) + \" NOT_BASE64\"},\n\t\t\texpectErr:   \"code=400, message=Bad Request, err=illegal base64 data at input byte 3\",\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, missing Authorization header\",\n\t\t\tgivenConfig:  defaultConfig,\n\t\t\texpectHeader: basic + ` realm=\"Restricted\"`,\n\t\t\texpectErr:    \"Unauthorized\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, realm\",\n\t\t\tgivenConfig: BasicAuthConfig{Validator: validatorFunc, Realm: \"someRealm\"},\n\t\t\twhenAuth:    []string{basic + \" \" + base64.StdEncoding.EncodeToString([]byte(\"joe:secret\"))},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, realm, case-insensitive header scheme\",\n\t\t\tgivenConfig: BasicAuthConfig{Validator: validatorFunc, Realm: \"someRealm\"},\n\t\t\twhenAuth:    []string{strings.ToUpper(basic) + \" \" + base64.StdEncoding.EncodeToString([]byte(\"joe:secret\"))},\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, realm, invalid Authorization header\",\n\t\t\tgivenConfig:  BasicAuthConfig{Validator: validatorFunc, Realm: \"someRealm\"},\n\t\t\twhenAuth:     []string{strings.ToUpper(basic) + \" \" + base64.StdEncoding.EncodeToString([]byte(\"invalid\"))},\n\t\t\texpectHeader: basic + ` realm=\"someRealm\"`,\n\t\t\texpectErr:    \"Unauthorized\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, validator func returns an error\",\n\t\t\tgivenConfig: defaultConfig,\n\t\t\twhenAuth:    []string{strings.ToUpper(basic) + \" \" + base64.StdEncoding.EncodeToString([]byte(\"error:my_error\"))},\n\t\t\texpectErr:   \"my_error\",\n\t\t},\n\t\t{\n\t\t\tname: \"ok, skipped\",\n\t\t\tgivenConfig: BasicAuthConfig{Validator: validatorFunc, Skipper: func(c *echo.Context) bool {\n\t\t\t\treturn true\n\t\t\t}},\n\t\t\twhenAuth: []string{strings.ToUpper(basic) + \" \" + base64.StdEncoding.EncodeToString([]byte(\"invalid\"))},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := echo.New()\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\t\tres := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, res)\n\n\t\t\tconfig := tc.givenConfig\n\n\t\t\tmw, err := config.ToMiddleware()\n\t\t\tassert.NoError(t, err)\n\n\t\t\th := mw(func(c *echo.Context) error {\n\t\t\t\treturn c.String(http.StatusTeapot, \"test\")\n\t\t\t})\n\n\t\t\tif len(tc.whenAuth) != 0 {\n\t\t\t\tfor _, a := range tc.whenAuth {\n\t\t\t\t\treq.Header.Add(echo.HeaderAuthorization, a)\n\t\t\t\t}\n\t\t\t}\n\t\t\terr = h(c)\n\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.Equal(t, http.StatusOK, res.Code)\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, http.StatusTeapot, res.Code)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tif tc.expectHeader != \"\" {\n\t\t\t\tassert.Equal(t, tc.expectHeader, res.Header().Get(echo.HeaderWWWAuthenticate))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBasicAuth_panic(t *testing.T) {\n\tassert.Panics(t, func() {\n\t\tmw := BasicAuth(nil)\n\t\tassert.NotNil(t, mw)\n\t})\n\n\tmw := BasicAuth(func(c *echo.Context, user string, password string) (bool, error) {\n\t\treturn true, nil\n\t})\n\tassert.NotNil(t, mw)\n}\n\nfunc TestBasicAuthWithConfig_panic(t *testing.T) {\n\tassert.Panics(t, func() {\n\t\tmw := BasicAuthWithConfig(BasicAuthConfig{Validator: nil})\n\t\tassert.NotNil(t, mw)\n\t})\n\n\tmw := BasicAuthWithConfig(BasicAuthConfig{Validator: func(c *echo.Context, user string, password string) (bool, error) {\n\t\treturn true, nil\n\t}})\n\tassert.NotNil(t, mw)\n}\n\nfunc TestBasicAuthRealm(t *testing.T) {\n\te := echo.New()\n\tmockValidator := func(c *echo.Context, u, p string) (bool, error) {\n\t\treturn false, nil // Always fail to trigger WWW-Authenticate header\n\t}\n\n\ttests := []struct {\n\t\tname         string\n\t\trealm        string\n\t\texpectedAuth string\n\t}{\n\t\t{\n\t\t\tname:         \"Default realm\",\n\t\t\trealm:        \"Restricted\",\n\t\t\texpectedAuth: `basic realm=\"Restricted\"`,\n\t\t},\n\t\t{\n\t\t\tname:         \"Custom realm\",\n\t\t\trealm:        \"My API\",\n\t\t\texpectedAuth: `basic realm=\"My API\"`,\n\t\t},\n\t\t{\n\t\t\tname:         \"Realm with special characters\",\n\t\t\trealm:        `Realm with \"quotes\" and \\backslashes`,\n\t\t\texpectedAuth: `basic realm=\"Realm with \\\"quotes\\\" and \\\\backslashes\"`,\n\t\t},\n\t\t{\n\t\t\tname:         \"Empty realm (falls back to default)\",\n\t\t\trealm:        \"\",\n\t\t\texpectedAuth: `basic realm=\"Restricted\"`,\n\t\t},\n\t\t{\n\t\t\tname:         \"Realm with unicode\",\n\t\t\trealm:        \"测试领域\",\n\t\t\texpectedAuth: `basic realm=\"测试领域\"`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\t\tres := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, res)\n\n\t\t\th := BasicAuthWithConfig(BasicAuthConfig{\n\t\t\t\tValidator: mockValidator,\n\t\t\t\tRealm:     tt.realm,\n\t\t\t})(func(c *echo.Context) error {\n\t\t\t\treturn c.String(http.StatusOK, \"test\")\n\t\t\t})\n\n\t\t\terr := h(c)\n\n\t\t\tassert.Equal(t, echo.ErrUnauthorized, err)\n\t\t\tassert.Equal(t, tt.expectedAuth, res.Header().Get(echo.HeaderWWWAuthenticate))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "middleware/body_dump.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"sync\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\n// BodyDumpConfig defines the config for BodyDump middleware.\ntype BodyDumpConfig struct {\n\t// Skipper defines a function to skip middleware.\n\tSkipper Skipper\n\n\t// Handler receives request, response payloads and handler error if there are any.\n\t// Required.\n\tHandler BodyDumpHandler\n\n\t// MaxRequestBytes limits how much of the request body to dump.\n\t// If the request body exceeds this limit, only the first MaxRequestBytes\n\t// are dumped. The handler callback receives truncated data.\n\t// Default: 5 * MB (5,242,880 bytes)\n\t// Set to -1 to disable limits (not recommended in production).\n\tMaxRequestBytes int64\n\n\t// MaxResponseBytes limits how much of the response body to dump.\n\t// If the response body exceeds this limit, only the first MaxResponseBytes\n\t// are dumped. The handler callback receives truncated data.\n\t// Default: 5 * MB (5,242,880 bytes)\n\t// Set to -1 to disable limits (not recommended in production).\n\tMaxResponseBytes int64\n}\n\n// BodyDumpHandler receives the request and response payload.\ntype BodyDumpHandler func(c *echo.Context, reqBody []byte, resBody []byte, err error)\n\ntype bodyDumpResponseWriter struct {\n\tio.Writer\n\thttp.ResponseWriter\n}\n\n// BodyDump returns a BodyDump middleware.\n//\n// BodyDump middleware captures the request and response payload and calls the\n// registered handler.\n//\n// SECURITY: By default, this limits dumped bodies to 5MB to prevent memory exhaustion\n// attacks. To customize limits, use BodyDumpWithConfig. To disable limits (not recommended\n// in production), explicitly set MaxRequestBytes and MaxResponseBytes to -1.\nfunc BodyDump(handler BodyDumpHandler) echo.MiddlewareFunc {\n\treturn BodyDumpWithConfig(BodyDumpConfig{Handler: handler})\n}\n\n// BodyDumpWithConfig returns a BodyDump middleware with config.\n// See: `BodyDump()`.\n//\n// SECURITY: If MaxRequestBytes and MaxResponseBytes are not set (zero values), they default\n// to 5MB each to prevent DoS attacks via large payloads. Set them explicitly to -1 to disable\n// limits if needed for your use case.\nfunc BodyDumpWithConfig(config BodyDumpConfig) echo.MiddlewareFunc {\n\treturn toMiddlewareOrPanic(config)\n}\n\n// ToMiddleware converts BodyDumpConfig to middleware or returns an error for invalid configuration\nfunc (config BodyDumpConfig) ToMiddleware() (echo.MiddlewareFunc, error) {\n\tif config.Handler == nil {\n\t\treturn nil, errors.New(\"echo body-dump middleware requires a handler function\")\n\t}\n\tif config.Skipper == nil {\n\t\tconfig.Skipper = DefaultSkipper\n\t}\n\tif config.MaxRequestBytes == 0 {\n\t\tconfig.MaxRequestBytes = 5 * MB\n\t}\n\tif config.MaxResponseBytes == 0 {\n\t\tconfig.MaxResponseBytes = 5 * MB\n\t}\n\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) error {\n\t\t\tif config.Skipper(c) {\n\t\t\t\treturn next(c)\n\t\t\t}\n\n\t\t\treqBuf := bodyDumpBufferPool.Get().(*bytes.Buffer)\n\t\t\treqBuf.Reset()\n\t\t\tdefer bodyDumpBufferPool.Put(reqBuf)\n\n\t\t\tvar bodyReader io.Reader = c.Request().Body\n\t\t\tif config.MaxRequestBytes > 0 {\n\t\t\t\tbodyReader = io.LimitReader(c.Request().Body, config.MaxRequestBytes)\n\t\t\t}\n\t\t\t_, readErr := io.Copy(reqBuf, bodyReader)\n\t\t\tif readErr != nil && readErr != io.EOF {\n\t\t\t\treturn readErr\n\t\t\t}\n\t\t\tif config.MaxRequestBytes > 0 {\n\t\t\t\t// Drain any remaining body data to prevent connection issues\n\t\t\t\t_, _ = io.Copy(io.Discard, c.Request().Body)\n\t\t\t\t_ = c.Request().Body.Close()\n\t\t\t}\n\n\t\t\treqBody := make([]byte, reqBuf.Len())\n\t\t\tcopy(reqBody, reqBuf.Bytes())\n\t\t\tc.Request().Body = io.NopCloser(bytes.NewReader(reqBody))\n\n\t\t\t// response part\n\t\t\tresBuf := bodyDumpBufferPool.Get().(*bytes.Buffer)\n\t\t\tresBuf.Reset()\n\t\t\tdefer bodyDumpBufferPool.Put(resBuf)\n\n\t\t\tvar respWriter io.Writer\n\t\t\tif config.MaxResponseBytes > 0 {\n\t\t\t\trespWriter = &limitedWriter{\n\t\t\t\t\tresponse: c.Response(),\n\t\t\t\t\tdumpBuf:  resBuf,\n\t\t\t\t\tlimit:    config.MaxResponseBytes,\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\trespWriter = io.MultiWriter(c.Response(), resBuf)\n\t\t\t}\n\t\t\twriter := &bodyDumpResponseWriter{\n\t\t\t\tWriter:         respWriter,\n\t\t\t\tResponseWriter: c.Response(),\n\t\t\t}\n\t\t\tc.SetResponse(writer)\n\n\t\t\terr := next(c)\n\n\t\t\t// Callback\n\t\t\tconfig.Handler(c, reqBody, resBuf.Bytes(), err)\n\n\t\t\treturn err\n\t\t}\n\t}, nil\n}\n\nfunc (w *bodyDumpResponseWriter) WriteHeader(code int) {\n\tw.ResponseWriter.WriteHeader(code)\n}\n\nfunc (w *bodyDumpResponseWriter) Write(b []byte) (int, error) {\n\treturn w.Writer.Write(b)\n}\n\nfunc (w *bodyDumpResponseWriter) Flush() {\n\terr := http.NewResponseController(w.ResponseWriter).Flush()\n\tif err != nil && errors.Is(err, http.ErrNotSupported) {\n\t\tpanic(errors.New(\"response writer flushing is not supported\"))\n\t}\n}\n\nfunc (w *bodyDumpResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {\n\treturn http.NewResponseController(w.ResponseWriter).Hijack()\n}\n\nfunc (w *bodyDumpResponseWriter) Unwrap() http.ResponseWriter {\n\treturn w.ResponseWriter\n}\n\nvar bodyDumpBufferPool = sync.Pool{\n\tNew: func() any {\n\t\treturn new(bytes.Buffer)\n\t},\n}\n\ntype limitedWriter struct {\n\tresponse http.ResponseWriter\n\tdumpBuf  *bytes.Buffer\n\tdumped   int64\n\tlimit    int64\n}\n\nfunc (w *limitedWriter) Write(b []byte) (n int, err error) {\n\t// Always write full data to actual response (don't truncate client response)\n\tn, err = w.response.Write(b)\n\tif err != nil {\n\t\treturn n, err\n\t}\n\n\t// Write to dump buffer only up to limit\n\tif w.dumped < w.limit {\n\t\tremaining := w.limit - w.dumped\n\t\ttoDump := int64(n)\n\t\tif toDump > remaining {\n\t\t\ttoDump = remaining\n\t\t}\n\t\tw.dumpBuf.Write(b[:toDump])\n\t\tw.dumped += toDump\n\t}\n\n\treturn n, nil\n}\n"
  },
  {
    "path": "middleware/body_dump_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestBodyDump(t *testing.T) {\n\te := echo.New()\n\thw := \"Hello, World!\"\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(hw))\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\th := func(c *echo.Context) error {\n\t\tbody, err := io.ReadAll(c.Request().Body)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn c.String(http.StatusOK, string(body))\n\t}\n\n\trequestBody := \"\"\n\tresponseBody := \"\"\n\tmw, err := BodyDumpConfig{Handler: func(c *echo.Context, reqBody, resBody []byte, err error) {\n\t\trequestBody = string(reqBody)\n\t\tresponseBody = string(resBody)\n\t}}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\tif assert.NoError(t, mw(h)(c)) {\n\t\tassert.Equal(t, requestBody, hw)\n\t\tassert.Equal(t, responseBody, hw)\n\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\tassert.Equal(t, hw, rec.Body.String())\n\t}\n\n}\n\nfunc TestBodyDump_skipper(t *testing.T) {\n\te := echo.New()\n\n\tisCalled := false\n\tmw, err := BodyDumpConfig{\n\t\tSkipper: func(c *echo.Context) bool {\n\t\t\treturn true\n\t\t},\n\t\tHandler: func(c *echo.Context, reqBody, resBody []byte, err error) {\n\t\t\tisCalled = true\n\t\t},\n\t}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(\"{}\"))\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\th := func(c *echo.Context) error {\n\t\treturn errors.New(\"some error\")\n\t}\n\n\terr = mw(h)(c)\n\tassert.EqualError(t, err, \"some error\")\n\tassert.False(t, isCalled)\n}\n\nfunc TestBodyDump_fails(t *testing.T) {\n\te := echo.New()\n\thw := \"Hello, World!\"\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(hw))\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\th := func(c *echo.Context) error {\n\t\treturn errors.New(\"some error\")\n\t}\n\n\tmw, err := BodyDumpConfig{Handler: func(c *echo.Context, reqBody, resBody []byte, err error) {}}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\terr = mw(h)(c)\n\tassert.EqualError(t, err, \"some error\")\n\tassert.Equal(t, http.StatusOK, rec.Code)\n\n}\n\nfunc TestBodyDumpWithConfig_panic(t *testing.T) {\n\tassert.Panics(t, func() {\n\t\tmw := BodyDumpWithConfig(BodyDumpConfig{\n\t\t\tSkipper: nil,\n\t\t\tHandler: nil,\n\t\t})\n\t\tassert.NotNil(t, mw)\n\t})\n\n\tassert.NotPanics(t, func() {\n\t\tmw := BodyDumpWithConfig(BodyDumpConfig{Handler: func(c *echo.Context, reqBody, resBody []byte, err error) {}})\n\t\tassert.NotNil(t, mw)\n\t})\n}\n\nfunc TestBodyDump_panic(t *testing.T) {\n\tassert.Panics(t, func() {\n\t\tmw := BodyDump(nil)\n\t\tassert.NotNil(t, mw)\n\t})\n\n\tassert.NotPanics(t, func() {\n\t\tBodyDump(func(c *echo.Context, reqBody, resBody []byte, err error) {})\n\t})\n}\n\nfunc TestBodyDumpResponseWriter_CanNotFlush(t *testing.T) {\n\tbdrw := bodyDumpResponseWriter{\n\t\tResponseWriter: new(testResponseWriterNoFlushHijack), // this RW does not support flush\n\t}\n\tassert.PanicsWithError(t, \"response writer flushing is not supported\", func() {\n\t\tbdrw.Flush()\n\t})\n}\n\nfunc TestBodyDumpResponseWriter_CanFlush(t *testing.T) {\n\ttrwu := testResponseWriterUnwrapperHijack{testResponseWriterUnwrapper: testResponseWriterUnwrapper{rw: httptest.NewRecorder()}}\n\tbdrw := bodyDumpResponseWriter{\n\t\tResponseWriter: &trwu,\n\t}\n\tbdrw.Flush()\n\tassert.Equal(t, 1, trwu.unwrapCalled)\n}\n\nfunc TestBodyDumpResponseWriter_CanUnwrap(t *testing.T) {\n\ttrwu := &testResponseWriterUnwrapper{rw: httptest.NewRecorder()}\n\tbdrw := bodyDumpResponseWriter{\n\t\tResponseWriter: trwu,\n\t}\n\tresult := bdrw.Unwrap()\n\tassert.Equal(t, trwu, result)\n}\n\nfunc TestBodyDumpResponseWriter_CanHijack(t *testing.T) {\n\ttrwu := testResponseWriterUnwrapperHijack{testResponseWriterUnwrapper: testResponseWriterUnwrapper{rw: httptest.NewRecorder()}}\n\tbdrw := bodyDumpResponseWriter{\n\t\tResponseWriter: &trwu, // this RW supports hijacking through unwrapping\n\t}\n\t_, _, err := bdrw.Hijack()\n\tassert.EqualError(t, err, \"can hijack\")\n}\n\nfunc TestBodyDumpResponseWriter_CanNotHijack(t *testing.T) {\n\ttrwu := testResponseWriterUnwrapper{rw: httptest.NewRecorder()}\n\tbdrw := bodyDumpResponseWriter{\n\t\tResponseWriter: &trwu, // this RW supports hijacking through unwrapping\n\t}\n\t_, _, err := bdrw.Hijack()\n\tassert.EqualError(t, err, \"feature not supported\")\n}\n\nfunc TestBodyDump_ReadError(t *testing.T) {\n\te := echo.New()\n\n\t// Create a reader that fails during read\n\tfailingReader := &failingReadCloser{\n\t\tdata:     []byte(\"partial data\"),\n\t\tfailAt:   7, // Fail after 7 bytes\n\t\tfailWith: errors.New(\"connection reset\"),\n\t}\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", failingReader)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\th := func(c *echo.Context) error {\n\t\t// This handler should not be reached if body read fails\n\t\tbody, _ := io.ReadAll(c.Request().Body)\n\t\treturn c.String(http.StatusOK, string(body))\n\t}\n\n\trequestBodyReceived := \"\"\n\tmw := BodyDump(func(c *echo.Context, reqBody, resBody []byte, err error) {\n\t\trequestBodyReceived = string(reqBody)\n\t})\n\n\terr := mw(h)(c)\n\n\t// Verify error is propagated\n\tassert.Error(t, err)\n\tassert.Contains(t, err.Error(), \"connection reset\")\n\n\t// Verify handler was not executed (callback wouldn't have received data)\n\tassert.Empty(t, requestBodyReceived)\n}\n\n// failingReadCloser is a helper type for testing read errors\ntype failingReadCloser struct {\n\tdata     []byte\n\tpos      int\n\tfailAt   int\n\tfailWith error\n}\n\nfunc (f *failingReadCloser) Read(p []byte) (n int, err error) {\n\tif f.pos >= f.failAt {\n\t\treturn 0, f.failWith\n\t}\n\n\tn = copy(p, f.data[f.pos:])\n\tf.pos += n\n\n\tif f.pos >= f.failAt {\n\t\treturn n, f.failWith\n\t}\n\n\treturn n, nil\n}\n\nfunc (f *failingReadCloser) Close() error {\n\treturn nil\n}\n\nfunc TestBodyDump_RequestWithinLimit(t *testing.T) {\n\te := echo.New()\n\trequestData := \"Hello, World!\"\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(requestData))\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\th := func(c *echo.Context) error {\n\t\tbody, _ := io.ReadAll(c.Request().Body)\n\t\treturn c.String(http.StatusOK, string(body))\n\t}\n\n\trequestBodyDumped := \"\"\n\tmw, err := BodyDumpConfig{\n\t\tHandler: func(c *echo.Context, reqBody, resBody []byte, err error) {\n\t\t\trequestBodyDumped = string(reqBody)\n\t\t},\n\t\tMaxRequestBytes:  1 * MB, // 1MB limit\n\t\tMaxResponseBytes: 1 * MB,\n\t}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\terr = mw(h)(c)\n\tassert.NoError(t, err)\n\tassert.Equal(t, requestData, requestBodyDumped, \"Small request should be fully dumped\")\n\tassert.Equal(t, requestData, rec.Body.String(), \"Handler should receive full request\")\n}\n\nfunc TestBodyDump_RequestExceedsLimit(t *testing.T) {\n\te := echo.New()\n\t// Create 2KB of data but limit to 1KB\n\tlargeData := strings.Repeat(\"A\", 2*1024)\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(largeData))\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\th := func(c *echo.Context) error {\n\t\tbody, _ := io.ReadAll(c.Request().Body)\n\t\treturn c.String(http.StatusOK, string(body))\n\t}\n\n\trequestBodyDumped := \"\"\n\tlimit := int64(1024) // 1KB limit\n\tmw, err := BodyDumpConfig{\n\t\tHandler: func(c *echo.Context, reqBody, resBody []byte, err error) {\n\t\t\trequestBodyDumped = string(reqBody)\n\t\t},\n\t\tMaxRequestBytes:  limit,\n\t\tMaxResponseBytes: 1 * MB,\n\t}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\terr = mw(h)(c)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int(limit), len(requestBodyDumped), \"Dumped request should be truncated to limit\")\n\tassert.Equal(t, strings.Repeat(\"A\", 1024), requestBodyDumped, \"Dumped data should match first N bytes\")\n\t// Handler should receive truncated data (what was dumped)\n\tassert.Equal(t, strings.Repeat(\"A\", 1024), rec.Body.String())\n}\n\nfunc TestBodyDump_RequestAtExactLimit(t *testing.T) {\n\te := echo.New()\n\texactData := strings.Repeat(\"B\", 1024)\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(exactData))\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\th := func(c *echo.Context) error {\n\t\tbody, _ := io.ReadAll(c.Request().Body)\n\t\treturn c.String(http.StatusOK, string(body))\n\t}\n\n\trequestBodyDumped := \"\"\n\tlimit := int64(1024)\n\tmw, err := BodyDumpConfig{\n\t\tHandler: func(c *echo.Context, reqBody, resBody []byte, err error) {\n\t\t\trequestBodyDumped = string(reqBody)\n\t\t},\n\t\tMaxRequestBytes:  limit,\n\t\tMaxResponseBytes: 1 * MB,\n\t}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\terr = mw(h)(c)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int(limit), len(requestBodyDumped), \"Exact limit should dump full data\")\n\tassert.Equal(t, exactData, requestBodyDumped)\n}\n\nfunc TestBodyDump_ResponseWithinLimit(t *testing.T) {\n\te := echo.New()\n\tresponseData := \"Response data\"\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\th := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, responseData)\n\t}\n\n\tresponseBodyDumped := \"\"\n\tmw, err := BodyDumpConfig{\n\t\tHandler: func(c *echo.Context, reqBody, resBody []byte, err error) {\n\t\t\tresponseBodyDumped = string(resBody)\n\t\t},\n\t\tMaxRequestBytes:  1 * MB,\n\t\tMaxResponseBytes: 1 * MB,\n\t}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\terr = mw(h)(c)\n\tassert.NoError(t, err)\n\tassert.Equal(t, responseData, responseBodyDumped, \"Small response should be fully dumped\")\n\tassert.Equal(t, responseData, rec.Body.String(), \"Client should receive full response\")\n}\n\nfunc TestBodyDump_ResponseExceedsLimit(t *testing.T) {\n\te := echo.New()\n\tlargeResponse := strings.Repeat(\"X\", 2*1024) // 2KB\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\th := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, largeResponse)\n\t}\n\n\tresponseBodyDumped := \"\"\n\tlimit := int64(1024) // 1KB limit\n\tmw, err := BodyDumpConfig{\n\t\tHandler: func(c *echo.Context, reqBody, resBody []byte, err error) {\n\t\t\tresponseBodyDumped = string(resBody)\n\t\t},\n\t\tMaxRequestBytes:  1 * MB,\n\t\tMaxResponseBytes: limit,\n\t}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\terr = mw(h)(c)\n\tassert.NoError(t, err)\n\t// Dump should be truncated\n\tassert.Equal(t, int(limit), len(responseBodyDumped), \"Dumped response should be truncated to limit\")\n\tassert.Equal(t, strings.Repeat(\"X\", 1024), responseBodyDumped)\n\t// Client should still receive full response!\n\tassert.Equal(t, largeResponse, rec.Body.String(), \"Client must receive full response despite dump truncation\")\n}\n\nfunc TestBodyDump_ClientGetsFullResponse(t *testing.T) {\n\te := echo.New()\n\t// This is critical - even when dump is limited, client gets everything\n\tlargeResponse := strings.Repeat(\"DATA\", 500) // 2KB\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\th := func(c *echo.Context) error {\n\t\t// Write response in chunks to test incremental writes\n\t\tfor i := 0; i < 4; i++ {\n\t\t\tc.Response().Write([]byte(strings.Repeat(\"DATA\", 125)))\n\t\t}\n\t\treturn nil\n\t}\n\n\tresponseBodyDumped := \"\"\n\tmw, err := BodyDumpConfig{\n\t\tHandler: func(c *echo.Context, reqBody, resBody []byte, err error) {\n\t\t\tresponseBodyDumped = string(resBody)\n\t\t},\n\t\tMaxRequestBytes:  1 * MB,\n\t\tMaxResponseBytes: 512, // Very small limit\n\t}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\terr = mw(h)(c)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 512, len(responseBodyDumped), \"Dump should be limited\")\n\tassert.Equal(t, largeResponse, rec.Body.String(), \"Client must get full response\")\n}\n\nfunc TestBodyDump_BothLimitsSimultaneous(t *testing.T) {\n\te := echo.New()\n\tlargeRequest := strings.Repeat(\"Q\", 2*1024)\n\tlargeResponse := strings.Repeat(\"R\", 2*1024)\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(largeRequest))\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\th := func(c *echo.Context) error {\n\t\tio.ReadAll(c.Request().Body) // Consume request\n\t\treturn c.String(http.StatusOK, largeResponse)\n\t}\n\n\trequestBodyDumped := \"\"\n\tresponseBodyDumped := \"\"\n\tlimit := int64(1024)\n\tmw, err := BodyDumpConfig{\n\t\tHandler: func(c *echo.Context, reqBody, resBody []byte, err error) {\n\t\t\trequestBodyDumped = string(reqBody)\n\t\t\tresponseBodyDumped = string(resBody)\n\t\t},\n\t\tMaxRequestBytes:  limit,\n\t\tMaxResponseBytes: limit,\n\t}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\terr = mw(h)(c)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int(limit), len(requestBodyDumped), \"Request dump should be limited\")\n\tassert.Equal(t, int(limit), len(responseBodyDumped), \"Response dump should be limited\")\n}\n\nfunc TestBodyDump_DefaultConfig(t *testing.T) {\n\te := echo.New()\n\tsmallData := \"test\"\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(smallData))\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\th := func(c *echo.Context) error {\n\t\tbody, _ := io.ReadAll(c.Request().Body)\n\t\treturn c.String(http.StatusOK, string(body))\n\t}\n\n\trequestBodyDumped := \"\"\n\t// Use default config which should have 1MB limits\n\tconfig := BodyDumpConfig{}\n\tconfig.Handler = func(c *echo.Context, reqBody, resBody []byte, err error) {\n\t\trequestBodyDumped = string(reqBody)\n\t}\n\tmw, err := config.ToMiddleware()\n\tassert.NoError(t, err)\n\n\terr = mw(h)(c)\n\tassert.NoError(t, err)\n\tassert.Equal(t, smallData, requestBodyDumped)\n}\n\nfunc TestBodyDump_LargeRequestDosPrevention(t *testing.T) {\n\te := echo.New()\n\t// Simulate a very large request (10MB) that could cause OOM\n\tlargeSize := 10 * 1024 * 1024 // 10MB\n\tlargeData := strings.Repeat(\"M\", largeSize)\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(largeData))\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\th := func(c *echo.Context) error {\n\t\tbody, _ := io.ReadAll(c.Request().Body)\n\t\treturn c.String(http.StatusOK, string(body))\n\t}\n\n\trequestBodyDumped := \"\"\n\tlimit := int64(1 * MB) // Only dump 1MB max\n\tmw, err := BodyDumpConfig{\n\t\tHandler: func(c *echo.Context, reqBody, resBody []byte, err error) {\n\t\t\trequestBodyDumped = string(reqBody)\n\t\t},\n\t\tMaxRequestBytes:  limit,\n\t\tMaxResponseBytes: 1 * MB,\n\t}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\terr = mw(h)(c)\n\tassert.NoError(t, err)\n\t// Verify only 1MB was dumped, not 10MB\n\tassert.Equal(t, int(limit), len(requestBodyDumped), \"Should only dump up to limit, preventing DoS\")\n\tassert.Less(t, len(requestBodyDumped), largeSize, \"Dump should be much smaller than full request\")\n}\n\nfunc TestBodyDump_LargeResponseDosPrevention(t *testing.T) {\n\te := echo.New()\n\t// Simulate a very large response (10MB)\n\tlargeSize := 10 * 1024 * 1024 // 10MB\n\tlargeResponse := strings.Repeat(\"R\", largeSize)\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\th := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, largeResponse)\n\t}\n\n\tresponseBodyDumped := \"\"\n\tlimit := int64(1 * MB) // Only dump 1MB max\n\tmw, err := BodyDumpConfig{\n\t\tHandler: func(c *echo.Context, reqBody, resBody []byte, err error) {\n\t\t\tresponseBodyDumped = string(resBody)\n\t\t},\n\t\tMaxRequestBytes:  1 * MB,\n\t\tMaxResponseBytes: limit,\n\t}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\terr = mw(h)(c)\n\tassert.NoError(t, err)\n\t// Verify only 1MB was dumped, not 10MB\n\tassert.Equal(t, int(limit), len(responseBodyDumped), \"Should only dump up to limit, preventing DoS\")\n\tassert.Less(t, len(responseBodyDumped), largeSize, \"Dump should be much smaller than full response\")\n\t// Client still gets full response\n\tassert.Equal(t, largeSize, rec.Body.Len(), \"Client must receive full response\")\n}\n\nfunc BenchmarkBodyDump_WithLimit(b *testing.B) {\n\te := echo.New()\n\trequestData := strings.Repeat(\"data\", 256)  // 1KB\n\tresponseData := strings.Repeat(\"resp\", 256) // 1KB\n\n\th := func(c *echo.Context) error {\n\t\tio.ReadAll(c.Request().Body)\n\t\treturn c.String(http.StatusOK, responseData)\n\t}\n\n\tmw, _ := BodyDumpConfig{\n\t\tHandler: func(c *echo.Context, reqBody, resBody []byte, err error) {\n\t\t\t// Simulate logging\n\t\t\t_ = len(reqBody) + len(resBody)\n\t\t},\n\t\tMaxRequestBytes:  1 * MB,\n\t\tMaxResponseBytes: 1 * MB,\n\t}.ToMiddleware()\n\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(requestData))\n\t\trec := httptest.NewRecorder()\n\t\tc := e.NewContext(req, rec)\n\t\tmw(h)(c)\n\t}\n}\n\nfunc BenchmarkBodyDump_BufferPooling(b *testing.B) {\n\te := echo.New()\n\trequestData := strings.Repeat(\"x\", 1024)\n\tresponseData := \"response\"\n\n\th := func(c *echo.Context) error {\n\t\tio.ReadAll(c.Request().Body)\n\t\treturn c.String(http.StatusOK, responseData)\n\t}\n\n\tmw, _ := BodyDumpConfig{\n\t\tHandler:          func(c *echo.Context, reqBody, resBody []byte, err error) {},\n\t\tMaxRequestBytes:  1 * MB,\n\t\tMaxResponseBytes: 1 * MB,\n\t}.ToMiddleware()\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(requestData))\n\t\trec := httptest.NewRecorder()\n\t\tc := e.NewContext(req, rec)\n\t\tmw(h)(c)\n\t}\n}\n"
  },
  {
    "path": "middleware/body_limit.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"sync\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\n// BodyLimitConfig defines the config for BodyLimitWithConfig middleware.\ntype BodyLimitConfig struct {\n\t// Skipper defines a function to skip middleware.\n\tSkipper Skipper\n\n\t// LimitBytes is maximum allowed size in bytes for a request body\n\tLimitBytes int64\n}\n\ntype limitedReader struct {\n\tBodyLimitConfig\n\treader io.ReadCloser\n\tread   int64\n}\n\n// BodyLimit returns a BodyLimit middleware.\n//\n// BodyLimit middleware sets the maximum allowed size for a request body, if the size exceeds the configured limit, it\n// sends \"413 - Request Entity Too Large\" response. The BodyLimit is determined based on both `Content-Length` request\n// header and actual content read, which makes it super secure.\nfunc BodyLimit(limitBytes int64) echo.MiddlewareFunc {\n\treturn BodyLimitWithConfig(BodyLimitConfig{LimitBytes: limitBytes})\n}\n\n// BodyLimitWithConfig returns a BodyLimitWithConfig middleware. Middleware sets the maximum allowed size in bytes for\n// a request body, if the  size exceeds the configured limit, it sends \"413 - Request Entity Too Large\" response.\n// The BodyLimitWithConfig is determined based on both `Content-Length` request header and actual content read, which\n// makes it super secure.\nfunc BodyLimitWithConfig(config BodyLimitConfig) echo.MiddlewareFunc {\n\treturn toMiddlewareOrPanic(config)\n}\n\n// ToMiddleware converts BodyLimitConfig to middleware or returns an error for invalid configuration\nfunc (config BodyLimitConfig) ToMiddleware() (echo.MiddlewareFunc, error) {\n\tif config.Skipper == nil {\n\t\tconfig.Skipper = DefaultSkipper\n\t}\n\tpool := sync.Pool{\n\t\tNew: func() any {\n\t\t\treturn &limitedReader{BodyLimitConfig: config}\n\t\t},\n\t}\n\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) error {\n\t\t\tif config.Skipper(c) {\n\t\t\t\treturn next(c)\n\t\t\t}\n\t\t\treq := c.Request()\n\n\t\t\t// Based on content length\n\t\t\tif req.ContentLength > config.LimitBytes {\n\t\t\t\treturn echo.ErrStatusRequestEntityTooLarge\n\t\t\t}\n\n\t\t\t// Based on content read\n\t\t\tr, ok := pool.Get().(*limitedReader)\n\t\t\tif !ok {\n\t\t\t\treturn echo.NewHTTPError(http.StatusInternalServerError, \"invalid pool object\")\n\t\t\t}\n\t\t\tr.Reset(req.Body)\n\t\t\tdefer pool.Put(r)\n\t\t\treq.Body = r\n\n\t\t\treturn next(c)\n\t\t}\n\t}, nil\n}\n\nfunc (r *limitedReader) Read(b []byte) (n int, err error) {\n\tn, err = r.reader.Read(b)\n\tr.read += int64(n)\n\tif r.read > r.LimitBytes {\n\t\treturn n, echo.ErrStatusRequestEntityTooLarge\n\t}\n\treturn\n}\n\nfunc (r *limitedReader) Close() error {\n\treturn r.reader.Close()\n}\n\nfunc (r *limitedReader) Reset(reader io.ReadCloser) {\n\tr.reader = reader\n\tr.read = 0\n}\n"
  },
  {
    "path": "middleware/body_limit_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestBodyLimitConfig_ToMiddleware(t *testing.T) {\n\te := echo.New()\n\thw := []byte(\"Hello, World!\")\n\treq := httptest.NewRequest(http.MethodPost, \"/\", bytes.NewReader(hw))\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\th := func(c *echo.Context) error {\n\t\tbody, err := io.ReadAll(c.Request().Body)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn c.String(http.StatusOK, string(body))\n\t}\n\n\t// Based on content length (within limit)\n\tmw, err := BodyLimitConfig{LimitBytes: 2 * MB}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\terr = mw(h)(c)\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\tassert.Equal(t, hw, rec.Body.Bytes())\n\t}\n\n\t// Based on content read (overlimit)\n\tmw, err = BodyLimitConfig{LimitBytes: 2}.ToMiddleware()\n\tassert.NoError(t, err)\n\the := mw(h)(c).(echo.HTTPStatusCoder)\n\tassert.Equal(t, http.StatusRequestEntityTooLarge, he.StatusCode())\n\n\t// Based on content read (within limit)\n\treq = httptest.NewRequest(http.MethodPost, \"/\", bytes.NewReader(hw))\n\treq.ContentLength = -1\n\trec = httptest.NewRecorder()\n\tc = e.NewContext(req, rec)\n\n\tmw, err = BodyLimitConfig{LimitBytes: 2 * MB}.ToMiddleware()\n\tassert.NoError(t, err)\n\terr = mw(h)(c)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, rec.Code)\n\tassert.Equal(t, \"Hello, World!\", rec.Body.String())\n\n\t// Based on content read (overlimit)\n\treq = httptest.NewRequest(http.MethodPost, \"/\", bytes.NewReader(hw))\n\treq.ContentLength = -1\n\trec = httptest.NewRecorder()\n\tc = e.NewContext(req, rec)\n\tmw, err = BodyLimitConfig{LimitBytes: 2}.ToMiddleware()\n\tassert.NoError(t, err)\n\the = mw(h)(c).(echo.HTTPStatusCoder)\n\tassert.Equal(t, http.StatusRequestEntityTooLarge, he.StatusCode())\n}\n\nfunc TestBodyLimitReader(t *testing.T) {\n\thw := []byte(\"Hello, World!\")\n\n\tconfig := BodyLimitConfig{\n\t\tSkipper:    DefaultSkipper,\n\t\tLimitBytes: 2,\n\t}\n\treader := &limitedReader{\n\t\tBodyLimitConfig: config,\n\t\treader:          io.NopCloser(bytes.NewReader(hw)),\n\t}\n\n\t// read all should return ErrStatusRequestEntityTooLarge\n\t_, err := io.ReadAll(reader)\n\the := err.(echo.HTTPStatusCoder)\n\tassert.Equal(t, http.StatusRequestEntityTooLarge, he.StatusCode())\n\n\t// reset reader and read two bytes must succeed\n\tbt := make([]byte, 2)\n\treader.Reset(io.NopCloser(bytes.NewReader(hw)))\n\tn, err := reader.Read(bt)\n\tassert.Equal(t, 2, n)\n\tassert.Equal(t, nil, err)\n}\n\nfunc TestBodyLimit_skipper(t *testing.T) {\n\te := echo.New()\n\th := func(c *echo.Context) error {\n\t\tbody, err := io.ReadAll(c.Request().Body)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn c.String(http.StatusOK, string(body))\n\t}\n\tmw, err := BodyLimitConfig{\n\t\tSkipper: func(c *echo.Context) bool {\n\t\t\treturn true\n\t\t},\n\t\tLimitBytes: 2,\n\t}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\thw := []byte(\"Hello, World!\")\n\treq := httptest.NewRequest(http.MethodPost, \"/\", bytes.NewReader(hw))\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\terr = mw(h)(c)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, rec.Code)\n\tassert.Equal(t, hw, rec.Body.Bytes())\n}\n\nfunc TestBodyLimitWithConfig(t *testing.T) {\n\te := echo.New()\n\thw := []byte(\"Hello, World!\")\n\treq := httptest.NewRequest(http.MethodPost, \"/\", bytes.NewReader(hw))\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\th := func(c *echo.Context) error {\n\t\tbody, err := io.ReadAll(c.Request().Body)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn c.String(http.StatusOK, string(body))\n\t}\n\n\tmw := BodyLimitWithConfig(BodyLimitConfig{LimitBytes: 2 * MB})\n\n\terr := mw(h)(c)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, rec.Code)\n\tassert.Equal(t, hw, rec.Body.Bytes())\n}\n\nfunc TestBodyLimit(t *testing.T) {\n\te := echo.New()\n\thw := []byte(\"Hello, World!\")\n\treq := httptest.NewRequest(http.MethodPost, \"/\", bytes.NewReader(hw))\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\th := func(c *echo.Context) error {\n\t\tbody, err := io.ReadAll(c.Request().Body)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn c.String(http.StatusOK, string(body))\n\t}\n\n\tmw := BodyLimit(2 * MB)\n\n\terr := mw(h)(c)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, rec.Code)\n\tassert.Equal(t, hw, rec.Body.Bytes())\n}\n"
  },
  {
    "path": "middleware/compress.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\nconst (\n\tgzipScheme = \"gzip\"\n)\n\n// GzipConfig defines the config for Gzip middleware.\ntype GzipConfig struct {\n\t// Skipper defines a function to skip middleware.\n\tSkipper Skipper\n\n\t// Gzip compression level.\n\t// Optional. Default value -1.\n\tLevel int\n\n\t// Length threshold before gzip compression is applied.\n\t// Optional. Default value 0.\n\t//\n\t// Most of the time you will not need to change the default. Compressing\n\t// a short response might increase the transmitted data because of the\n\t// gzip format overhead. Compressing the response will also consume CPU\n\t// and time on the server and the client (for decompressing). Depending on\n\t// your use case such a threshold might be useful.\n\t//\n\t// See also:\n\t// https://webmasters.stackexchange.com/questions/31750/what-is-recommended-minimum-object-size-for-gzip-performance-benefits\n\tMinLength int\n}\n\ntype gzipResponseWriter struct {\n\tio.Writer\n\thttp.ResponseWriter\n\twroteHeader       bool\n\twroteBody         bool\n\tminLength         int\n\tminLengthExceeded bool\n\tbuffer            *bytes.Buffer\n\tcode              int\n}\n\n// Gzip returns a middleware which compresses HTTP response using gzip compression scheme.\nfunc Gzip() echo.MiddlewareFunc {\n\treturn GzipWithConfig(GzipConfig{})\n}\n\n// GzipWithConfig returns a middleware which compresses HTTP response using gzip compression scheme.\nfunc GzipWithConfig(config GzipConfig) echo.MiddlewareFunc {\n\treturn toMiddlewareOrPanic(config)\n}\n\n// ToMiddleware converts GzipConfig to middleware or returns an error for invalid configuration\nfunc (config GzipConfig) ToMiddleware() (echo.MiddlewareFunc, error) {\n\tif config.Skipper == nil {\n\t\tconfig.Skipper = DefaultSkipper\n\t}\n\tif config.Level < -2 || config.Level > 9 { // these are consts: gzip.HuffmanOnly and gzip.BestCompression\n\t\treturn nil, errors.New(\"invalid gzip level\")\n\t}\n\tif config.Level == 0 {\n\t\tconfig.Level = -1\n\t}\n\tif config.MinLength < 0 {\n\t\tconfig.MinLength = 0\n\t}\n\n\tpool := gzipCompressPool(config)\n\tbpool := bufferPool()\n\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) error {\n\t\t\tif config.Skipper(c) {\n\t\t\t\treturn next(c)\n\t\t\t}\n\n\t\t\tres := c.Response()\n\t\t\tres.Header().Add(echo.HeaderVary, echo.HeaderAcceptEncoding)\n\t\t\tif strings.Contains(c.Request().Header.Get(echo.HeaderAcceptEncoding), gzipScheme) {\n\t\t\t\ti := pool.Get()\n\t\t\t\tw, ok := i.(*gzip.Writer)\n\t\t\t\tif !ok {\n\t\t\t\t\treturn echo.NewHTTPError(http.StatusInternalServerError, \"invalid pool object\")\n\t\t\t\t}\n\t\t\t\trw := res\n\t\t\t\tw.Reset(rw)\n\t\t\t\tbuf := bpool.Get().(*bytes.Buffer)\n\t\t\t\tbuf.Reset()\n\n\t\t\t\tgrw := &gzipResponseWriter{\n\t\t\t\t\tWriter:         w,\n\t\t\t\t\tResponseWriter: rw,\n\t\t\t\t\tminLength:      config.MinLength,\n\t\t\t\t\tbuffer:         buf,\n\t\t\t\t}\n\t\t\t\tc.SetResponse(grw)\n\t\t\t\tdefer func() {\n\t\t\t\t\t// There are different reasons for cases when we have not yet written response to the client and now need to do so.\n\t\t\t\t\t// a) handler response had only response code and no response body (ala 404 or redirects etc). Response code need to be written now.\n\t\t\t\t\t// b) body is shorter than our minimum length threshold and being buffered currently and needs to be written\n\t\t\t\t\tif !grw.wroteBody {\n\t\t\t\t\t\tif res.Header().Get(echo.HeaderContentEncoding) == gzipScheme {\n\t\t\t\t\t\t\tres.Header().Del(echo.HeaderContentEncoding)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif grw.wroteHeader {\n\t\t\t\t\t\t\trw.WriteHeader(grw.code)\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// We have to reset response to it's pristine state when\n\t\t\t\t\t\t// nothing is written to body or error is returned.\n\t\t\t\t\t\t// See issue #424, #407.\n\t\t\t\t\t\tc.SetResponse(rw)\n\t\t\t\t\t\tw.Reset(io.Discard)\n\t\t\t\t\t} else if !grw.minLengthExceeded {\n\t\t\t\t\t\t// Write uncompressed response\n\t\t\t\t\t\tc.SetResponse(rw)\n\t\t\t\t\t\tif grw.wroteHeader {\n\t\t\t\t\t\t\tgrw.ResponseWriter.WriteHeader(grw.code)\n\t\t\t\t\t\t}\n\t\t\t\t\t\t_, _ = grw.buffer.WriteTo(rw)\n\t\t\t\t\t\tw.Reset(io.Discard)\n\t\t\t\t\t}\n\t\t\t\t\t_ = w.Close()\n\t\t\t\t\tbpool.Put(buf)\n\t\t\t\t\tpool.Put(w)\n\t\t\t\t}()\n\t\t\t}\n\t\t\treturn next(c)\n\t\t}\n\t}, nil\n}\n\nfunc (w *gzipResponseWriter) WriteHeader(code int) {\n\tw.Header().Del(echo.HeaderContentLength) // Issue #444\n\n\tw.wroteHeader = true\n\n\t// Delay writing of the header until we know if we'll actually compress the response\n\tw.code = code\n}\n\nfunc (w *gzipResponseWriter) Write(b []byte) (int, error) {\n\tif w.Header().Get(echo.HeaderContentType) == \"\" {\n\t\tw.Header().Set(echo.HeaderContentType, http.DetectContentType(b))\n\t}\n\tw.wroteBody = true\n\n\tif !w.minLengthExceeded {\n\t\tn, err := w.buffer.Write(b)\n\n\t\tif w.buffer.Len() >= w.minLength {\n\t\t\tw.minLengthExceeded = true\n\n\t\t\t// The minimum length is exceeded, add Content-Encoding header and write the header\n\t\t\tw.Header().Set(echo.HeaderContentEncoding, gzipScheme) // Issue #806\n\t\t\tif w.wroteHeader {\n\t\t\t\tw.ResponseWriter.WriteHeader(w.code)\n\t\t\t}\n\n\t\t\treturn w.Writer.Write(w.buffer.Bytes())\n\t\t}\n\n\t\treturn n, err\n\t}\n\n\treturn w.Writer.Write(b)\n}\n\nfunc (w *gzipResponseWriter) Flush() {\n\tif !w.minLengthExceeded {\n\t\t// Enforce compression because we will not know how much more data will come\n\t\tw.minLengthExceeded = true\n\t\tw.Header().Set(echo.HeaderContentEncoding, gzipScheme) // Issue #806\n\t\tif w.wroteHeader {\n\t\t\tw.ResponseWriter.WriteHeader(w.code)\n\t\t}\n\n\t\t_, _ = w.Writer.Write(w.buffer.Bytes())\n\t}\n\n\tif gw, ok := w.Writer.(*gzip.Writer); ok {\n\t\tgw.Flush()\n\t}\n\t_ = http.NewResponseController(w.ResponseWriter).Flush()\n}\n\nfunc (w *gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {\n\treturn http.NewResponseController(w.ResponseWriter).Hijack()\n}\n\nfunc (w *gzipResponseWriter) Unwrap() http.ResponseWriter {\n\treturn w.ResponseWriter\n}\n\nfunc (w *gzipResponseWriter) Push(target string, opts *http.PushOptions) error {\n\tif p, ok := w.ResponseWriter.(http.Pusher); ok {\n\t\treturn p.Push(target, opts)\n\t}\n\treturn http.ErrNotSupported\n}\n\nfunc gzipCompressPool(config GzipConfig) sync.Pool {\n\treturn sync.Pool{\n\t\tNew: func() any {\n\t\t\tw, err := gzip.NewWriterLevel(io.Discard, config.Level)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn w\n\t\t},\n\t}\n}\n\nfunc bufferPool() sync.Pool {\n\treturn sync.Pool{\n\t\tNew: func() any {\n\t\t\tb := &bytes.Buffer{}\n\t\t\treturn b\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "middleware/compress_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGzip_NoAcceptEncodingHeader(t *testing.T) {\n\t// Skip if no Accept-Encoding header\n\th := Gzip()(func(c *echo.Context) error {\n\t\tc.Response().Write([]byte(\"test\")) // For Content-Type sniffing\n\t\treturn nil\n\t})\n\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\terr := h(c)\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, \"test\", rec.Body.String())\n}\n\nfunc TestMustGzipWithConfig_panics(t *testing.T) {\n\tassert.Panics(t, func() {\n\t\tGzipWithConfig(GzipConfig{Level: 999})\n\t})\n}\n\nfunc TestGzip_AcceptEncodingHeader(t *testing.T) {\n\th := Gzip()(func(c *echo.Context) error {\n\t\tc.Response().Write([]byte(\"test\")) // For Content-Type sniffing\n\t\treturn nil\n\t})\n\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)\n\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\terr := h(c)\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, gzipScheme, rec.Header().Get(echo.HeaderContentEncoding))\n\tassert.Contains(t, rec.Header().Get(echo.HeaderContentType), echo.MIMETextPlain)\n\n\tr, err := gzip.NewReader(rec.Body)\n\tassert.NoError(t, err)\n\tbuf := new(bytes.Buffer)\n\tdefer r.Close()\n\tbuf.ReadFrom(r)\n\tassert.Equal(t, \"test\", buf.String())\n}\n\nfunc TestGzip_chunked(t *testing.T) {\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\tchunkChan := make(chan struct{})\n\twaitChan := make(chan struct{})\n\th := Gzip()(func(c *echo.Context) error {\n\t\trc := http.NewResponseController(c.Response())\n\t\tc.Response().Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tc.Response().Header().Set(\"Transfer-Encoding\", \"chunked\")\n\n\t\t// Write and flush the first part of the data\n\t\tc.Response().Write([]byte(\"first\\n\"))\n\t\trc.Flush()\n\n\t\tchunkChan <- struct{}{}\n\t\t<-waitChan\n\n\t\t// Write and flush the second part of the data\n\t\tc.Response().Write([]byte(\"second\\n\"))\n\t\trc.Flush()\n\n\t\tchunkChan <- struct{}{}\n\t\t<-waitChan\n\n\t\t// Write the final part of the data and return\n\t\tc.Response().Write([]byte(\"third\"))\n\n\t\tchunkChan <- struct{}{}\n\t\treturn nil\n\t})\n\n\tgo func() {\n\t\terr := h(c)\n\t\tchunkChan <- struct{}{}\n\t\tassert.NoError(t, err)\n\t}()\n\n\t<-chunkChan // wait for first write\n\twaitChan <- struct{}{}\n\n\t<-chunkChan // wait for second write\n\twaitChan <- struct{}{}\n\n\t<-chunkChan                      // wait for final write in handler\n\t<-chunkChan                      // wait for return from handler\n\ttime.Sleep(5 * time.Millisecond) // to have time for flushing\n\n\tassert.Equal(t, gzipScheme, rec.Header().Get(echo.HeaderContentEncoding))\n\n\tr, err := gzip.NewReader(rec.Body)\n\tassert.NoError(t, err)\n\tbuf := new(bytes.Buffer)\n\tbuf.ReadFrom(r)\n\tassert.Equal(t, \"first\\nsecond\\nthird\", buf.String())\n}\n\nfunc TestGzip_NoContent(t *testing.T) {\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\th := Gzip()(func(c *echo.Context) error {\n\t\treturn c.NoContent(http.StatusNoContent)\n\t})\n\tif assert.NoError(t, h(c)) {\n\t\tassert.Empty(t, rec.Header().Get(echo.HeaderContentEncoding))\n\t\tassert.Empty(t, rec.Header().Get(echo.HeaderContentType))\n\t\tassert.Equal(t, 0, len(rec.Body.Bytes()))\n\t}\n}\n\nfunc TestGzip_Empty(t *testing.T) {\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\th := Gzip()(func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"\")\n\t})\n\tif assert.NoError(t, h(c)) {\n\t\tassert.Equal(t, gzipScheme, rec.Header().Get(echo.HeaderContentEncoding))\n\t\tassert.Equal(t, \"text/plain; charset=UTF-8\", rec.Header().Get(echo.HeaderContentType))\n\t\tr, err := gzip.NewReader(rec.Body)\n\t\tif assert.NoError(t, err) {\n\t\t\tvar buf bytes.Buffer\n\t\t\tbuf.ReadFrom(r)\n\t\t\tassert.Equal(t, \"\", buf.String())\n\t\t}\n\t}\n}\n\nfunc TestGzip_ErrorReturned(t *testing.T) {\n\te := echo.New()\n\te.Use(Gzip())\n\te.GET(\"/\", func(c *echo.Context) error {\n\t\treturn echo.ErrNotFound\n\t})\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)\n\trec := httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\tassert.Equal(t, http.StatusNotFound, rec.Code)\n\tassert.Empty(t, rec.Header().Get(echo.HeaderContentEncoding))\n}\n\nfunc TestGzipWithConfig_invalidLevel(t *testing.T) {\n\tmw, err := GzipConfig{Level: 12}.ToMiddleware()\n\tassert.EqualError(t, err, \"invalid gzip level\")\n\tassert.Nil(t, mw)\n}\n\n// Issue #806\nfunc TestGzipWithStatic(t *testing.T) {\n\te := echo.New()\n\te.Filesystem = os.DirFS(\"../\")\n\n\te.Use(Gzip())\n\te.Static(\"/test\", \"_fixture/images\")\n\treq := httptest.NewRequest(http.MethodGet, \"/test/walle.png\", nil)\n\treq.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)\n\trec := httptest.NewRecorder()\n\n\te.ServeHTTP(rec, req)\n\n\tassert.Equal(t, http.StatusOK, rec.Code)\n\t// Data is written out in chunks when Content-Length == \"\", so only\n\t// validate the content length if it's not set.\n\tif cl := rec.Header().Get(\"Content-Length\"); cl != \"\" {\n\t\tassert.Equal(t, cl, rec.Body.Len())\n\t}\n\tr, err := gzip.NewReader(rec.Body)\n\tif assert.NoError(t, err) {\n\t\tdefer r.Close()\n\t\twant, err := os.ReadFile(\"../_fixture/images/walle.png\")\n\t\tif assert.NoError(t, err) {\n\t\t\tbuf := new(bytes.Buffer)\n\t\t\tbuf.ReadFrom(r)\n\t\t\tassert.Equal(t, want, buf.Bytes())\n\t\t}\n\t}\n}\n\nfunc TestGzipWithMinLength(t *testing.T) {\n\te := echo.New()\n\t// Minimal response length\n\te.Use(GzipWithConfig(GzipConfig{MinLength: 10}))\n\te.GET(\"/\", func(c *echo.Context) error {\n\t\tc.Response().Write([]byte(\"foobarfoobar\"))\n\t\treturn nil\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)\n\trec := httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\tassert.Equal(t, gzipScheme, rec.Header().Get(echo.HeaderContentEncoding))\n\tr, err := gzip.NewReader(rec.Body)\n\tif assert.NoError(t, err) {\n\t\tbuf := new(bytes.Buffer)\n\t\tdefer r.Close()\n\t\tbuf.ReadFrom(r)\n\t\tassert.Equal(t, \"foobarfoobar\", buf.String())\n\t}\n}\n\nfunc TestGzipWithMinLengthTooShort(t *testing.T) {\n\te := echo.New()\n\t// Minimal response length\n\te.Use(GzipWithConfig(GzipConfig{MinLength: 10}))\n\te.GET(\"/\", func(c *echo.Context) error {\n\t\tc.Response().Write([]byte(\"test\"))\n\t\treturn nil\n\t})\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)\n\trec := httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\tassert.Equal(t, \"\", rec.Header().Get(echo.HeaderContentEncoding))\n\tassert.Contains(t, rec.Body.String(), \"test\")\n}\n\nfunc TestGzipWithResponseWithoutBody(t *testing.T) {\n\te := echo.New()\n\n\te.Use(Gzip())\n\te.GET(\"/\", func(c *echo.Context) error {\n\t\treturn c.Redirect(http.StatusMovedPermanently, \"http://localhost\")\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)\n\trec := httptest.NewRecorder()\n\n\te.ServeHTTP(rec, req)\n\n\tassert.Equal(t, http.StatusMovedPermanently, rec.Code)\n\tassert.Equal(t, \"\", rec.Header().Get(echo.HeaderContentEncoding))\n}\n\nfunc TestGzipWithMinLengthChunked(t *testing.T) {\n\te := echo.New()\n\n\t// Gzip chunked\n\tchunkBuf := make([]byte, 5)\n\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)\n\trec := httptest.NewRecorder()\n\n\tvar r *gzip.Reader = nil\n\n\tc := e.NewContext(req, rec)\n\tnext := func(c *echo.Context) error {\n\t\trc := http.NewResponseController(c.Response())\n\t\tc.Response().Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tc.Response().Header().Set(\"Transfer-Encoding\", \"chunked\")\n\n\t\t// Write and flush the first part of the data\n\t\tc.Response().Write([]byte(\"test\\n\"))\n\t\trc.Flush()\n\n\t\t// Read the first part of the data\n\t\tassert.True(t, rec.Flushed)\n\t\tassert.Equal(t, gzipScheme, rec.Header().Get(echo.HeaderContentEncoding))\n\n\t\tvar err error\n\t\tr, err = gzip.NewReader(rec.Body)\n\t\tassert.NoError(t, err)\n\n\t\t_, err = io.ReadFull(r, chunkBuf)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, \"test\\n\", string(chunkBuf))\n\n\t\t// Write and flush the second part of the data\n\t\tc.Response().Write([]byte(\"test\\n\"))\n\t\trc.Flush()\n\n\t\t_, err = io.ReadFull(r, chunkBuf)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, \"test\\n\", string(chunkBuf))\n\n\t\t// Write the final part of the data and return\n\t\tc.Response().Write([]byte(\"test\"))\n\t\treturn nil\n\t}\n\terr := GzipWithConfig(GzipConfig{MinLength: 10})(next)(c)\n\n\tassert.NoError(t, err)\n\tassert.NotNil(t, r)\n\n\tbuf := new(bytes.Buffer)\n\n\tbuf.ReadFrom(r)\n\tassert.Equal(t, \"test\", buf.String())\n\n\tr.Close()\n}\n\nfunc TestGzipWithMinLengthNoContent(t *testing.T) {\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\th := GzipWithConfig(GzipConfig{MinLength: 10})(func(c *echo.Context) error {\n\t\treturn c.NoContent(http.StatusNoContent)\n\t})\n\tif assert.NoError(t, h(c)) {\n\t\tassert.Empty(t, rec.Header().Get(echo.HeaderContentEncoding))\n\t\tassert.Empty(t, rec.Header().Get(echo.HeaderContentType))\n\t\tassert.Equal(t, 0, len(rec.Body.Bytes()))\n\t}\n}\n\nfunc TestGzipResponseWriter_CanUnwrap(t *testing.T) {\n\ttrwu := &testResponseWriterUnwrapper{rw: httptest.NewRecorder()}\n\tbdrw := gzipResponseWriter{\n\t\tResponseWriter: trwu,\n\t}\n\tresult := bdrw.Unwrap()\n\tassert.Equal(t, trwu, result)\n}\n\nfunc TestGzipResponseWriter_CanHijack(t *testing.T) {\n\ttrwu := testResponseWriterUnwrapperHijack{testResponseWriterUnwrapper: testResponseWriterUnwrapper{rw: httptest.NewRecorder()}}\n\tbdrw := gzipResponseWriter{\n\t\tResponseWriter: &trwu, // this RW supports hijacking through unwrapping\n\t}\n\t_, _, err := bdrw.Hijack()\n\tassert.EqualError(t, err, \"can hijack\")\n}\n\nfunc TestGzipResponseWriter_CanNotHijack(t *testing.T) {\n\ttrwu := testResponseWriterUnwrapper{rw: httptest.NewRecorder()}\n\tbdrw := gzipResponseWriter{\n\t\tResponseWriter: &trwu, // this RW supports hijacking through unwrapping\n\t}\n\t_, _, err := bdrw.Hijack()\n\tassert.EqualError(t, err, \"feature not supported\")\n}\n\nfunc BenchmarkGzip(b *testing.B) {\n\te := echo.New()\n\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)\n\n\th := Gzip()(func(c *echo.Context) error {\n\t\tc.Response().Write([]byte(\"test\")) // For Content-Type sniffing\n\t\treturn nil\n\t})\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tfor i := 0; i < b.N; i++ {\n\t\t// Gzip\n\t\trec := httptest.NewRecorder()\n\t\tc := e.NewContext(req, rec)\n\t\th(c)\n\t}\n}\n"
  },
  {
    "path": "middleware/context_timeout.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\n// ContextTimeoutConfig defines the config for ContextTimeout middleware.\ntype ContextTimeoutConfig struct {\n\t// Skipper defines a function to skip middleware.\n\tSkipper Skipper\n\n\t// ErrorHandler is a function when error arises in middeware execution.\n\tErrorHandler func(c *echo.Context, err error) error\n\n\t// Timeout configures a timeout for the middleware\n\tTimeout time.Duration\n}\n\n// ContextTimeout returns a middleware which returns error (503 Service Unavailable error) to client\n// when underlying method returns context.DeadlineExceeded error.\nfunc ContextTimeout(timeout time.Duration) echo.MiddlewareFunc {\n\treturn ContextTimeoutWithConfig(ContextTimeoutConfig{Timeout: timeout})\n}\n\n// ContextTimeoutWithConfig returns a Timeout middleware with config.\nfunc ContextTimeoutWithConfig(config ContextTimeoutConfig) echo.MiddlewareFunc {\n\treturn toMiddlewareOrPanic(config)\n}\n\n// ToMiddleware converts Config to middleware.\nfunc (config ContextTimeoutConfig) ToMiddleware() (echo.MiddlewareFunc, error) {\n\tif config.Timeout == 0 {\n\t\treturn nil, errors.New(\"timeout must be set\")\n\t}\n\tif config.Skipper == nil {\n\t\tconfig.Skipper = DefaultSkipper\n\t}\n\tif config.ErrorHandler == nil {\n\t\tconfig.ErrorHandler = func(c *echo.Context, err error) error {\n\t\t\tif err != nil && errors.Is(err, context.DeadlineExceeded) {\n\t\t\t\treturn echo.ErrServiceUnavailable.Wrap(err)\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) error {\n\t\t\tif config.Skipper(c) {\n\t\t\t\treturn next(c)\n\t\t\t}\n\n\t\t\ttimeoutContext, cancel := context.WithTimeout(c.Request().Context(), config.Timeout)\n\t\t\tdefer cancel()\n\n\t\t\tc.SetRequest(c.Request().WithContext(timeoutContext))\n\n\t\t\tif err := next(c); err != nil {\n\t\t\t\treturn config.ErrorHandler(c, err)\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}, nil\n}\n"
  },
  {
    "path": "middleware/context_timeout_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"github.com/labstack/echo/v5\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestContextTimeoutSkipper(t *testing.T) {\n\tt.Parallel()\n\tm := ContextTimeoutWithConfig(ContextTimeoutConfig{\n\t\tSkipper: func(context *echo.Context) bool {\n\t\t\treturn true\n\t\t},\n\t\tTimeout: 10 * time.Millisecond,\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\n\te := echo.New()\n\tc := e.NewContext(req, rec)\n\n\terr := m(func(c *echo.Context) error {\n\t\tif err := sleepWithContext(c.Request().Context(), time.Duration(20*time.Millisecond)); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn errors.New(\"response from handler\")\n\t})(c)\n\n\t// if not skipped we would have not returned error due context timeout logic\n\tassert.EqualError(t, err, \"response from handler\")\n}\n\nfunc TestContextTimeoutWithTimeout0(t *testing.T) {\n\tt.Parallel()\n\tassert.Panics(t, func() {\n\t\tContextTimeout(time.Duration(0))\n\t})\n}\n\nfunc TestContextTimeoutErrorOutInHandler(t *testing.T) {\n\tt.Parallel()\n\tm := ContextTimeoutWithConfig(ContextTimeoutConfig{\n\t\t// Timeout has to be defined or the whole flow for timeout middleware will be skipped\n\t\tTimeout: 10 * time.Millisecond,\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\n\te := echo.New()\n\tc := e.NewContext(req, rec)\n\n\trec.Code = 1 // we want to be sure that even 200 will not be sent\n\terr := m(func(c *echo.Context) error {\n\t\t// this error must not be written to the client response. Middlewares upstream of timeout middleware must be able\n\t\t// to handle returned error and this can be done only then handler has not yet committed (written status code)\n\t\t// the response.\n\t\treturn echo.NewHTTPError(http.StatusTeapot, \"err\")\n\t})(c)\n\n\tassert.Error(t, err)\n\tassert.EqualError(t, err, \"code=418, message=err\")\n\tassert.Equal(t, 1, rec.Code)\n\tassert.Equal(t, \"\", rec.Body.String())\n}\n\nfunc TestContextTimeoutSuccessfulRequest(t *testing.T) {\n\tt.Parallel()\n\tm := ContextTimeoutWithConfig(ContextTimeoutConfig{\n\t\t// Timeout has to be defined or the whole flow for timeout middleware will be skipped\n\t\tTimeout: 10 * time.Millisecond,\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\n\te := echo.New()\n\tc := e.NewContext(req, rec)\n\n\terr := m(func(c *echo.Context) error {\n\t\treturn c.JSON(http.StatusCreated, map[string]string{\"data\": \"ok\"})\n\t})(c)\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusCreated, rec.Code)\n\tassert.Equal(t, \"{\\\"data\\\":\\\"ok\\\"}\\n\", rec.Body.String())\n}\n\nfunc TestContextTimeoutTestRequestClone(t *testing.T) {\n\tt.Parallel()\n\treq := httptest.NewRequest(http.MethodPost, \"/uri?query=value\", strings.NewReader(url.Values{\"form\": {\"value\"}}.Encode()))\n\treq.AddCookie(&http.Cookie{Name: \"cookie\", Value: \"value\"})\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\trec := httptest.NewRecorder()\n\n\tm := ContextTimeoutWithConfig(ContextTimeoutConfig{\n\t\t// Timeout has to be defined or the whole flow for timeout middleware will be skipped\n\t\tTimeout: 1 * time.Second,\n\t})\n\n\te := echo.New()\n\tc := e.NewContext(req, rec)\n\n\terr := m(func(c *echo.Context) error {\n\t\t// Cookie test\n\t\tcookie, err := c.Request().Cookie(\"cookie\")\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.EqualValues(t, \"cookie\", cookie.Name)\n\t\t\tassert.EqualValues(t, \"value\", cookie.Value)\n\t\t}\n\n\t\t// Form values\n\t\tif assert.NoError(t, c.Request().ParseForm()) {\n\t\t\tassert.EqualValues(t, \"value\", c.Request().FormValue(\"form\"))\n\t\t}\n\n\t\t// Query string\n\t\tassert.EqualValues(t, \"value\", c.Request().URL.Query()[\"query\"][0])\n\t\treturn nil\n\t})(c)\n\n\tassert.NoError(t, err)\n}\n\nfunc TestContextTimeoutWithDefaultErrorMessage(t *testing.T) {\n\tt.Parallel()\n\n\ttimeout := 10 * time.Millisecond\n\tm := ContextTimeoutWithConfig(ContextTimeoutConfig{\n\t\tTimeout: timeout,\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\n\te := echo.New()\n\tc := e.NewContext(req, rec)\n\n\terr := m(func(c *echo.Context) error {\n\t\tif err := sleepWithContext(c.Request().Context(), time.Duration(80*time.Millisecond)); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn c.String(http.StatusOK, \"Hello, World!\")\n\t})(c)\n\n\tassert.Error(t, err)\n\tif assert.IsType(t, &echo.HTTPError{}, err) {\n\t\tassert.Equal(t, http.StatusServiceUnavailable, err.(*echo.HTTPError).Code)\n\t\tassert.Equal(t, \"Service Unavailable\", err.(*echo.HTTPError).Message)\n\t}\n}\n\nfunc TestContextTimeoutCanHandleContextDeadlineOnNextHandler(t *testing.T) {\n\tt.Parallel()\n\n\ttimeoutErrorHandler := func(c *echo.Context, err error) error {\n\t\tif err != nil {\n\t\t\tif errors.Is(err, context.DeadlineExceeded) {\n\t\t\t\treturn &echo.HTTPError{\n\t\t\t\t\tCode:    http.StatusServiceUnavailable,\n\t\t\t\t\tMessage: \"Timeout! change me\",\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\n\ttimeout := 50 * time.Millisecond\n\tm := ContextTimeoutWithConfig(ContextTimeoutConfig{\n\t\tTimeout:      timeout,\n\t\tErrorHandler: timeoutErrorHandler,\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\n\te := echo.New()\n\tc := e.NewContext(req, rec)\n\n\terr := m(func(c *echo.Context) error {\n\t\t// NOTE: Very short periods are not reliable for tests due to Go routine scheduling and the unpredictable order\n\t\t// for 1) request and 2) time goroutine. For most OS this works as expected, but MacOS seems most flaky.\n\n\t\tif err := sleepWithContext(c.Request().Context(), 100*time.Millisecond); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// The Request Context should have a Deadline set by http.ContextTimeoutHandler\n\t\tif _, ok := c.Request().Context().Deadline(); !ok {\n\t\t\tassert.Fail(t, \"No timeout set on Request Context\")\n\t\t}\n\t\treturn c.String(http.StatusOK, \"Hello, World!\")\n\t})(c)\n\n\tassert.IsType(t, &echo.HTTPError{}, err)\n\tassert.Error(t, err)\n\tassert.Equal(t, http.StatusServiceUnavailable, err.(*echo.HTTPError).Code)\n\tassert.Equal(t, \"Timeout! change me\", err.(*echo.HTTPError).Message)\n}\n\nfunc sleepWithContext(ctx context.Context, d time.Duration) error {\n\ttimer := time.NewTimer(d)\n\n\tdefer func() {\n\t\t_ = timer.Stop()\n\t}()\n\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn context.DeadlineExceeded\n\tcase <-timer.C:\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "middleware/cors.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\n// CORSConfig defines the config for CORS middleware.\ntype CORSConfig struct {\n\t// Skipper defines a function to skip middleware.\n\tSkipper Skipper\n\n\t// AllowOrigins determines the value of the Access-Control-Allow-Origin\n\t// response header.  This header defines a list of origins that may access the\n\t// resource.\n\t//\n\t// Origin consist of following parts: `scheme + \"://\" + host + optional \":\" + port`\n\t// Wildcard can be used, but has to be set explicitly []string{\"*\"}\n\t// Example: `https://example.com`, `http://example.com:8080`, `*`\n\t//\n\t// Security: use extreme caution when handling the origin, and carefully\n\t// validate any logic. Remember that attackers may register hostile domain names.\n\t// See https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html\n\t// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin\n\t//\n\t// Mandatory.\n\tAllowOrigins []string\n\n\t// UnsafeAllowOriginFunc is an optional custom function to validate the origin. It takes the\n\t// origin as an argument and returns\n\t// - string, allowed origin\n\t// - bool, true if allowed or false otherwise.\n\t// - error, if an error is returned, it is returned immediately by the handler.\n\t// If this option is set, AllowOrigins is ignored.\n\t//\n\t// Security: use extreme caution when handling the origin, and carefully\n\t// validate any logic. Remember that attackers may register hostile (sub)domain names.\n\t// See https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html\n\t//\n\t// Sub-domain checks example:\n\t// \t\tUnsafeAllowOriginFunc: func(c *echo.Context, origin string) (string, bool, error) {\n\t//\t\t\tif strings.HasSuffix(origin, \".example.com\") {\n\t//\t\t\t\treturn origin, true, nil\n\t//\t\t\t}\n\t//\t\t\treturn \"\", false, nil\n\t//\t\t},\n\t//\n\t// Optional.\n\tUnsafeAllowOriginFunc func(c *echo.Context, origin string) (allowedOrigin string, allowed bool, err error)\n\n\t// AllowMethods determines the value of the Access-Control-Allow-Methods\n\t// response header.  This header specified the list of methods allowed when\n\t// accessing the resource.  This is used in response to a preflight request.\n\t//\n\t// Optional. Default value DefaultCORSConfig.AllowMethods.\n\t// If `allowMethods` is left empty, this middleware will fill for preflight\n\t// request `Access-Control-Allow-Methods` header value\n\t// from `Allow` header that echo.Router set into context.\n\t//\n\t// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods\n\tAllowMethods []string\n\n\t// AllowHeaders determines the value of the Access-Control-Allow-Headers\n\t// response header.  This header is used in response to a preflight request to\n\t// indicate which HTTP headers can be used when making the actual request.\n\t//\n\t// Optional. Defaults to empty list. No domains allowed for CORS.\n\t//\n\t// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers\n\tAllowHeaders []string\n\n\t// AllowCredentials determines the value of the\n\t// Access-Control-Allow-Credentials response header.  This header indicates\n\t// whether or not the response to the request can be exposed when the\n\t// credentials mode (Request.credentials) is true. When used as part of a\n\t// response to a preflight request, this indicates whether or not the actual\n\t// request can be made using credentials.  See also\n\t// [MDN: Access-Control-Allow-Credentials].\n\t//\n\t// Optional. Default value false, in which case the header is not set.\n\t//\n\t// Security: avoid using `AllowCredentials = true` with `AllowOrigins = *`.\n\t// See \"Exploiting CORS misconfigurations for Bitcoins and bounties\",\n\t// https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html\n\t//\n\t// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials\n\tAllowCredentials bool\n\n\t// ExposeHeaders determines the value of Access-Control-Expose-Headers, which\n\t// defines a list of headers that clients are allowed to access.\n\t//\n\t// Optional. Default value []string{}, in which case the header is not set.\n\t//\n\t// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Header\n\tExposeHeaders []string\n\n\t// MaxAge determines the value of the Access-Control-Max-Age response header.\n\t// This header indicates how long (in seconds) the results of a preflight\n\t// request can be cached.\n\t// The header is set only if MaxAge != 0, negative value sends \"0\" which instructs browsers not to cache that response.\n\t//\n\t// Optional. Default value 0 - meaning header is not sent.\n\t//\n\t// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age\n\tMaxAge int\n}\n\n// CORS returns a Cross-Origin Resource Sharing (CORS) middleware.\n// See also [MDN: Cross-Origin Resource Sharing (CORS)].\n//\n// Origin consist of following parts: `scheme + \"://\" + host + optional \":\" + port`\n// Wildcard `*` can be used, but has to be set explicitly.\n// Example: `https://example.com`, `http://example.com:8080`, `*`\n//\n// Security: Poorly configured CORS can compromise security because it allows\n// relaxation of the browser's Same-Origin policy.  See [Exploiting CORS\n// misconfigurations for Bitcoins and bounties] and [Portswigger: Cross-origin\n// resource sharing (CORS)] for more details.\n//\n// [MDN: Cross-Origin Resource Sharing (CORS)]: https://developer.mozilla.org/en/docs/Web/HTTP/Access_control_CORS\n// [Exploiting CORS misconfigurations for Bitcoins and bounties]: https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html\n// [Portswigger: Cross-origin resource sharing (CORS)]: https://portswigger.net/web-security/cors\nfunc CORS(allowOrigins ...string) echo.MiddlewareFunc {\n\tc := CORSConfig{\n\t\tAllowOrigins: allowOrigins,\n\t}\n\treturn CORSWithConfig(c)\n}\n\n// CORSWithConfig returns a CORS middleware with config or panics on invalid configuration.\n// See: [CORS].\nfunc CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {\n\treturn toMiddlewareOrPanic(config)\n}\n\n// ToMiddleware converts CORSConfig to middleware or returns an error for invalid configuration\nfunc (config CORSConfig) ToMiddleware() (echo.MiddlewareFunc, error) {\n\t// Defaults\n\tif config.Skipper == nil {\n\t\tconfig.Skipper = DefaultSkipper\n\t}\n\thasCustomAllowMethods := true\n\tif len(config.AllowMethods) == 0 {\n\t\thasCustomAllowMethods = false\n\t\tconfig.AllowMethods = []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete}\n\t}\n\n\tallowMethods := strings.Join(config.AllowMethods, \",\")\n\tallowHeaders := strings.Join(config.AllowHeaders, \",\")\n\texposeHeaders := strings.Join(config.ExposeHeaders, \",\")\n\n\tmaxAge := \"0\"\n\tif config.MaxAge > 0 {\n\t\tmaxAge = strconv.Itoa(config.MaxAge)\n\t}\n\n\tallowOriginFunc := config.UnsafeAllowOriginFunc\n\tif config.UnsafeAllowOriginFunc == nil {\n\t\tif len(config.AllowOrigins) == 0 {\n\t\t\treturn nil, errors.New(\"at least one AllowOrigins is required or UnsafeAllowOriginFunc must be provided\")\n\t\t}\n\t\tallowOriginFunc = config.defaultAllowOriginFunc\n\t\tfor _, origin := range config.AllowOrigins {\n\t\t\tif origin == \"*\" {\n\t\t\t\tif config.AllowCredentials {\n\t\t\t\t\treturn nil, fmt.Errorf(\"* as allowed origin and AllowCredentials=true is insecure and not allowed. Use custom UnsafeAllowOriginFunc\")\n\t\t\t\t}\n\t\t\t\tallowOriginFunc = config.starAllowOriginFunc\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif err := validateOrigin(origin, \"allow origin\"); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tconfig.AllowOrigins = append([]string(nil), config.AllowOrigins...)\n\t}\n\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) error {\n\t\t\tif config.Skipper(c) {\n\t\t\t\treturn next(c)\n\t\t\t}\n\n\t\t\treq := c.Request()\n\t\t\tres := c.Response()\n\t\t\torigin := req.Header.Get(echo.HeaderOrigin)\n\n\t\t\tres.Header().Add(echo.HeaderVary, echo.HeaderOrigin)\n\n\t\t\t// Preflight request is an OPTIONS request, using three HTTP request headers: Access-Control-Request-Method,\n\t\t\t// Access-Control-Request-Headers, and the Origin header. See: https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request\n\t\t\t// For simplicity we just consider method type and later `Origin` header.\n\t\t\tpreflight := req.Method == http.MethodOptions\n\n\t\t\t// Although router adds special handler in case of OPTIONS method we avoid calling next for OPTIONS in this middleware\n\t\t\t// as CORS requests do not have cookies / authentication headers by default, so we could get stuck in auth\n\t\t\t// middlewares by calling next(c).\n\t\t\t// But we still want to send `Allow` header as response in case of Non-CORS OPTIONS request as router default\n\t\t\t// handler does.\n\t\t\trouterAllowMethods := \"\"\n\t\t\tif preflight {\n\t\t\t\ttmpAllowMethods, ok := c.Get(echo.ContextKeyHeaderAllow).(string)\n\t\t\t\tif ok && tmpAllowMethods != \"\" {\n\t\t\t\t\trouterAllowMethods = tmpAllowMethods\n\t\t\t\t\tc.Response().Header().Set(echo.HeaderAllow, routerAllowMethods)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// No Origin provided. This is (probably) not request from actual browser - proceed executing middleware chain\n\t\t\tif origin == \"\" {\n\t\t\t\tif preflight { // req.Method=OPTIONS\n\t\t\t\t\treturn c.NoContent(http.StatusNoContent)\n\t\t\t\t}\n\t\t\t\treturn next(c) // let non-browser calls through\n\t\t\t}\n\n\t\t\tallowedOrigin, allowed, err := allowOriginFunc(c, origin)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif !allowed {\n\t\t\t\t// Origin existed and was NOT allowed\n\t\t\t\tif preflight {\n\t\t\t\t\t// From: https://github.com/labstack/echo/issues/2767\n\t\t\t\t\t// If the request's origin isn't allowed by the CORS configuration,\n\t\t\t\t\t// the middleware should simply omit the relevant CORS headers from the response\n\t\t\t\t\t// and let the browser fail the CORS check (if any).\n\t\t\t\t\treturn c.NoContent(http.StatusNoContent)\n\t\t\t\t}\n\t\t\t\t// From: https://github.com/labstack/echo/issues/2767\n\t\t\t\t// no CORS middleware should block non-preflight requests;\n\t\t\t\t// such requests should be let through. One reason is that not all requests that\n\t\t\t\t// carry an Origin header participate in the CORS protocol.\n\t\t\t\treturn next(c)\n\t\t\t}\n\n\t\t\t// Origin existed and was allowed\n\n\t\t\tres.Header().Set(echo.HeaderAccessControlAllowOrigin, allowedOrigin)\n\t\t\tif config.AllowCredentials {\n\t\t\t\tres.Header().Set(echo.HeaderAccessControlAllowCredentials, \"true\")\n\t\t\t}\n\n\t\t\t// Simple request will be let though\n\t\t\tif !preflight {\n\t\t\t\tif exposeHeaders != \"\" {\n\t\t\t\t\tres.Header().Set(echo.HeaderAccessControlExposeHeaders, exposeHeaders)\n\t\t\t\t}\n\t\t\t\treturn next(c)\n\t\t\t}\n\t\t\t// Below code is for Preflight (OPTIONS) request\n\t\t\t//\n\t\t\t// Preflight will end with c.NoContent(http.StatusNoContent) as we do not know if\n\t\t\t// at the end of handler chain is actual OPTIONS route or 404/405 route which\n\t\t\t// response code will confuse browsers\n\t\t\tres.Header().Add(echo.HeaderVary, echo.HeaderAccessControlRequestMethod)\n\t\t\tres.Header().Add(echo.HeaderVary, echo.HeaderAccessControlRequestHeaders)\n\n\t\t\tif !hasCustomAllowMethods && routerAllowMethods != \"\" {\n\t\t\t\tres.Header().Set(echo.HeaderAccessControlAllowMethods, routerAllowMethods)\n\t\t\t} else {\n\t\t\t\tres.Header().Set(echo.HeaderAccessControlAllowMethods, allowMethods)\n\t\t\t}\n\n\t\t\tif allowHeaders != \"\" {\n\t\t\t\tres.Header().Set(echo.HeaderAccessControlAllowHeaders, allowHeaders)\n\t\t\t} else {\n\t\t\t\th := req.Header.Get(echo.HeaderAccessControlRequestHeaders)\n\t\t\t\tif h != \"\" {\n\t\t\t\t\tres.Header().Set(echo.HeaderAccessControlAllowHeaders, h)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif config.MaxAge != 0 {\n\t\t\t\tres.Header().Set(echo.HeaderAccessControlMaxAge, maxAge)\n\t\t\t}\n\t\t\treturn c.NoContent(http.StatusNoContent)\n\t\t}\n\t}, nil\n}\n\nfunc (config CORSConfig) starAllowOriginFunc(c *echo.Context, origin string) (string, bool, error) {\n\treturn \"*\", true, nil\n}\n\nfunc (config CORSConfig) defaultAllowOriginFunc(c *echo.Context, origin string) (string, bool, error) {\n\tfor _, allowedOrigin := range config.AllowOrigins {\n\t\tif strings.EqualFold(allowedOrigin, origin) {\n\t\t\treturn allowedOrigin, true, nil\n\t\t}\n\t}\n\treturn \"\", false, nil\n}\n"
  },
  {
    "path": "middleware/cors_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"cmp\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCORS(t *testing.T) {\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodOptions, \"/\", nil) // Preflight request\n\treq.Header.Set(echo.HeaderOrigin, \"http://example.com\")\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\tmw := CORS(\"*\")\n\thandler := mw(func(c *echo.Context) error {\n\t\treturn nil\n\t})\n\n\terr := handler(c)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusNoContent, rec.Code)\n\tassert.Equal(t, \"*\", rec.Header().Get(echo.HeaderAccessControlAllowOrigin))\n}\n\nfunc TestCORSConfig(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname             string\n\t\tgivenConfig      *CORSConfig\n\t\twhenMethod       string\n\t\twhenHeaders      map[string]string\n\t\texpectHeaders    map[string]string\n\t\tnotExpectHeaders map[string]string\n\t\texpectErr        string\n\t}{\n\t\t{\n\t\t\tname: \"ok, wildcard origin\",\n\t\t\tgivenConfig: &CORSConfig{\n\t\t\t\tAllowOrigins: []string{\"*\"},\n\t\t\t},\n\t\t\twhenHeaders:   map[string]string{echo.HeaderOrigin: \"localhost\"},\n\t\t\texpectHeaders: map[string]string{echo.HeaderAccessControlAllowOrigin: \"*\"},\n\t\t},\n\t\t{\n\t\t\tname: \"ok, wildcard AllowedOrigin with no Origin header in request\",\n\t\t\tgivenConfig: &CORSConfig{\n\t\t\t\tAllowOrigins: []string{\"*\"},\n\t\t\t},\n\t\t\tnotExpectHeaders: map[string]string{echo.HeaderAccessControlAllowOrigin: \"\"},\n\t\t},\n\t\t{\n\t\t\tname: \"ok, specific AllowOrigins and AllowCredentials\",\n\t\t\tgivenConfig: &CORSConfig{\n\t\t\t\tAllowOrigins:     []string{\"http://localhost\", \"http://localhost:8080\"},\n\t\t\t\tAllowCredentials: true,\n\t\t\t\tMaxAge:           3600,\n\t\t\t},\n\t\t\twhenHeaders: map[string]string{echo.HeaderOrigin: \"http://localhost\"},\n\t\t\texpectHeaders: map[string]string{\n\t\t\t\techo.HeaderAccessControlAllowOrigin:      \"http://localhost\",\n\t\t\t\techo.HeaderAccessControlAllowCredentials: \"true\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok, preflight request with matching origin for `AllowOrigins`\",\n\t\t\tgivenConfig: &CORSConfig{\n\t\t\t\tAllowOrigins:     []string{\"http://localhost\"},\n\t\t\t\tAllowCredentials: true,\n\t\t\t\tMaxAge:           3600,\n\t\t\t},\n\t\t\twhenMethod: http.MethodOptions,\n\t\t\twhenHeaders: map[string]string{\n\t\t\t\techo.HeaderOrigin:      \"http://localhost\",\n\t\t\t\techo.HeaderContentType: echo.MIMEApplicationJSON,\n\t\t\t},\n\t\t\texpectHeaders: map[string]string{\n\t\t\t\techo.HeaderAccessControlAllowOrigin:      \"http://localhost\",\n\t\t\t\techo.HeaderAccessControlAllowMethods:     \"GET,HEAD,PUT,PATCH,POST,DELETE\",\n\t\t\t\techo.HeaderAccessControlAllowCredentials: \"true\",\n\t\t\t\techo.HeaderAccessControlMaxAge:           \"3600\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok, preflight request when `Access-Control-Max-Age` is set\",\n\t\t\tgivenConfig: &CORSConfig{\n\t\t\t\tAllowOrigins:     []string{\"http://localhost\"},\n\t\t\t\tAllowCredentials: true,\n\t\t\t\tMaxAge:           1,\n\t\t\t},\n\t\t\twhenMethod: http.MethodOptions,\n\t\t\twhenHeaders: map[string]string{\n\t\t\t\techo.HeaderOrigin:      \"http://localhost\",\n\t\t\t\techo.HeaderContentType: echo.MIMEApplicationJSON,\n\t\t\t},\n\t\t\texpectHeaders: map[string]string{\n\t\t\t\techo.HeaderAccessControlMaxAge: \"1\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok, preflight request when `Access-Control-Max-Age` is set to 0 - not to cache response\",\n\t\t\tgivenConfig: &CORSConfig{\n\t\t\t\tAllowOrigins:     []string{\"http://localhost\"},\n\t\t\t\tAllowCredentials: true,\n\t\t\t\tMaxAge:           -1, // forces `Access-Control-Max-Age: 0`\n\t\t\t},\n\t\t\twhenMethod: http.MethodOptions,\n\t\t\twhenHeaders: map[string]string{\n\t\t\t\techo.HeaderOrigin:      \"http://localhost\",\n\t\t\t\techo.HeaderContentType: echo.MIMEApplicationJSON,\n\t\t\t},\n\t\t\texpectHeaders: map[string]string{\n\t\t\t\techo.HeaderAccessControlMaxAge: \"0\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok, CORS check are skipped\",\n\t\t\tgivenConfig: &CORSConfig{\n\t\t\t\tAllowOrigins:     []string{\"http://localhost\"},\n\t\t\t\tAllowCredentials: true,\n\t\t\t\tSkipper: func(c *echo.Context) bool {\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t},\n\t\t\twhenMethod: http.MethodOptions,\n\t\t\twhenHeaders: map[string]string{\n\t\t\t\techo.HeaderOrigin:      \"http://localhost\",\n\t\t\t\techo.HeaderContentType: echo.MIMEApplicationJSON,\n\t\t\t},\n\t\t\tnotExpectHeaders: map[string]string{\n\t\t\t\techo.HeaderAccessControlAllowOrigin:      \"localhost\",\n\t\t\t\techo.HeaderAccessControlAllowMethods:     \"GET,HEAD,PUT,PATCH,POST,DELETE\",\n\t\t\t\techo.HeaderAccessControlAllowCredentials: \"true\",\n\t\t\t\techo.HeaderAccessControlMaxAge:           \"3600\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"nok, preflight request with wildcard `AllowOrigins` and `AllowCredentials` true\",\n\t\t\tgivenConfig: &CORSConfig{\n\t\t\t\tAllowOrigins:     []string{\"*\"},\n\t\t\t\tAllowCredentials: true,\n\t\t\t\tMaxAge:           3600,\n\t\t\t},\n\t\t\twhenMethod: http.MethodOptions,\n\t\t\twhenHeaders: map[string]string{\n\t\t\t\techo.HeaderOrigin:      \"localhost\",\n\t\t\t\techo.HeaderContentType: echo.MIMEApplicationJSON,\n\t\t\t},\n\t\t\texpectErr: `* as allowed origin and AllowCredentials=true is insecure and not allowed. Use custom UnsafeAllowOriginFunc`,\n\t\t},\n\t\t{\n\t\t\tname: \"nok, preflight request with invalid `AllowOrigins` value\",\n\t\t\tgivenConfig: &CORSConfig{\n\t\t\t\tAllowOrigins: []string{\"http://server\", \"missing-scheme\"},\n\t\t\t},\n\t\t\texpectErr: `allow origin is missing scheme or host: missing-scheme`,\n\t\t},\n\t\t{\n\t\t\tname: \"ok, preflight request with wildcard `AllowOrigins` and `AllowCredentials` false\",\n\t\t\tgivenConfig: &CORSConfig{\n\t\t\t\tAllowOrigins:     []string{\"*\"},\n\t\t\t\tAllowCredentials: false, // important for this testcase\n\t\t\t\tMaxAge:           3600,\n\t\t\t},\n\t\t\twhenMethod: http.MethodOptions,\n\t\t\twhenHeaders: map[string]string{\n\t\t\t\techo.HeaderOrigin:      \"localhost\",\n\t\t\t\techo.HeaderContentType: echo.MIMEApplicationJSON,\n\t\t\t},\n\t\t\texpectHeaders: map[string]string{\n\t\t\t\techo.HeaderAccessControlAllowOrigin:  \"*\",\n\t\t\t\techo.HeaderAccessControlAllowMethods: \"GET,HEAD,PUT,PATCH,POST,DELETE\",\n\t\t\t\techo.HeaderAccessControlMaxAge:       \"3600\",\n\t\t\t},\n\t\t\tnotExpectHeaders: map[string]string{\n\t\t\t\techo.HeaderAccessControlAllowCredentials: \"\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok, INSECURE preflight request with wildcard `AllowOrigins` and `AllowCredentials` true\",\n\t\t\tgivenConfig: &CORSConfig{\n\t\t\t\tAllowOrigins:     []string{\"*\"},\n\t\t\t\tAllowCredentials: true,\n\t\t\t\tMaxAge:           3600,\n\t\t\t},\n\t\t\twhenMethod: http.MethodOptions,\n\t\t\twhenHeaders: map[string]string{\n\t\t\t\techo.HeaderOrigin:      \"localhost\",\n\t\t\t\techo.HeaderContentType: echo.MIMEApplicationJSON,\n\t\t\t},\n\t\t\texpectErr: `* as allowed origin and AllowCredentials=true is insecure and not allowed. Use custom UnsafeAllowOriginFunc`,\n\t\t},\n\t\t{\n\t\t\tname: \"ok, preflight request with Access-Control-Request-Headers\",\n\t\t\tgivenConfig: &CORSConfig{\n\t\t\t\tAllowOrigins: []string{\"*\"},\n\t\t\t},\n\t\t\twhenMethod: http.MethodOptions,\n\t\t\twhenHeaders: map[string]string{\n\t\t\t\techo.HeaderOrigin:                      \"localhost\",\n\t\t\t\techo.HeaderContentType:                 echo.MIMEApplicationJSON,\n\t\t\t\techo.HeaderAccessControlRequestHeaders: \"Special-Request-Header\",\n\t\t\t},\n\t\t\texpectHeaders: map[string]string{\n\t\t\t\techo.HeaderAccessControlAllowOrigin:  \"*\",\n\t\t\t\techo.HeaderAccessControlAllowHeaders: \"Special-Request-Header\",\n\t\t\t\techo.HeaderAccessControlAllowMethods: \"GET,HEAD,PUT,PATCH,POST,DELETE\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok, preflight request with `AllowOrigins` which allow all subdomains aaa with *\",\n\t\t\tgivenConfig: &CORSConfig{\n\t\t\t\tUnsafeAllowOriginFunc: func(c *echo.Context, origin string) (allowedOrigin string, allowed bool, err error) {\n\t\t\t\t\tif strings.HasSuffix(origin, \".example.com\") {\n\t\t\t\t\t\tallowed = true\n\t\t\t\t\t}\n\t\t\t\t\treturn origin, allowed, nil\n\t\t\t\t},\n\t\t\t},\n\t\t\twhenMethod:    http.MethodOptions,\n\t\t\twhenHeaders:   map[string]string{echo.HeaderOrigin: \"http://aaa.example.com\"},\n\t\t\texpectHeaders: map[string]string{echo.HeaderAccessControlAllowOrigin: \"http://aaa.example.com\"},\n\t\t},\n\t\t{\n\t\t\tname: \"ok, preflight request with `AllowOrigins` which allow all subdomains bbb with *\",\n\t\t\tgivenConfig: &CORSConfig{\n\t\t\t\tUnsafeAllowOriginFunc: func(c *echo.Context, origin string) (string, bool, error) {\n\t\t\t\t\tif strings.HasSuffix(origin, \".example.com\") {\n\t\t\t\t\t\treturn origin, true, nil\n\t\t\t\t\t}\n\t\t\t\t\treturn \"\", false, nil\n\t\t\t\t},\n\t\t\t},\n\t\t\twhenMethod:    http.MethodOptions,\n\t\t\twhenHeaders:   map[string]string{echo.HeaderOrigin: \"http://bbb.example.com\"},\n\t\t\texpectHeaders: map[string]string{echo.HeaderAccessControlAllowOrigin: \"http://bbb.example.com\"},\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := echo.New()\n\n\t\t\tvar mw echo.MiddlewareFunc\n\t\t\tvar err error\n\t\t\tif tc.givenConfig != nil {\n\t\t\t\tmw, err = tc.givenConfig.ToMiddleware()\n\t\t\t} else {\n\t\t\t\tmw, err = CORSConfig{}.ToMiddleware()\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tif tc.expectErr != \"\" {\n\t\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\th := mw(func(c *echo.Context) error {\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\tmethod := cmp.Or(tc.whenMethod, http.MethodGet)\n\t\t\treq := httptest.NewRequest(method, \"/\", nil)\n\t\t\trec := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, rec)\n\t\t\tfor k, v := range tc.whenHeaders {\n\t\t\t\treq.Header.Set(k, v)\n\t\t\t}\n\n\t\t\terr = h(c)\n\n\t\t\tassert.NoError(t, err)\n\t\t\theader := rec.Header()\n\t\t\tfor k, v := range tc.expectHeaders {\n\t\t\t\tassert.Equal(t, v, header.Get(k), \"header: `%v` should be `%v`\", k, v)\n\t\t\t}\n\t\t\tfor k, v := range tc.notExpectHeaders {\n\t\t\t\tif v == \"\" {\n\t\t\t\t\tassert.Len(t, header.Values(k), 0, \"header: `%v` should not be set\", k)\n\t\t\t\t} else {\n\t\t\t\t\tassert.NotEqual(t, v, header.Get(k), \"header: `%v` should not be `%v`\", k, v)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_allowOriginScheme(t *testing.T) {\n\ttests := []struct {\n\t\tdomain, pattern string\n\t\texpected        bool\n\t}{\n\t\t{\n\t\t\tdomain:   \"http://example.com\",\n\t\t\tpattern:  \"http://example.com\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tdomain:   \"https://example.com\",\n\t\t\tpattern:  \"https://example.com\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tdomain:   \"http://example.com\",\n\t\t\tpattern:  \"https://example.com\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tdomain:   \"https://example.com\",\n\t\t\tpattern:  \"http://example.com\",\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\te := echo.New()\n\tfor _, tt := range tests {\n\t\treq := httptest.NewRequest(http.MethodOptions, \"/\", nil)\n\t\trec := httptest.NewRecorder()\n\t\tc := e.NewContext(req, rec)\n\t\treq.Header.Set(echo.HeaderOrigin, tt.domain)\n\t\tcors := CORSWithConfig(CORSConfig{\n\t\t\tAllowOrigins: []string{tt.pattern},\n\t\t})\n\t\th := cors(func(c *echo.Context) error { return echo.ErrNotFound })\n\t\th(c)\n\n\t\tif tt.expected {\n\t\t\tassert.Equal(t, tt.domain, rec.Header().Get(echo.HeaderAccessControlAllowOrigin))\n\t\t} else {\n\t\t\tassert.NotContains(t, rec.Header(), echo.HeaderAccessControlAllowOrigin)\n\t\t}\n\t}\n}\n\nfunc TestCORSWithConfig_AllowMethods(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname                            string\n\t\tgivenAllowOrigins               []string\n\t\tgivenAllowMethods               []string\n\t\twhenAllowContextKey             string\n\t\twhenOrigin                      string\n\t\texpectAllow                     string\n\t\texpectAccessControlAllowMethods string\n\t}{\n\t\t{\n\t\t\tname:                \"custom AllowMethods, preflight, no origin, sets only allow header from context key\",\n\t\t\tgivenAllowOrigins:   []string{\"*\"},\n\t\t\tgivenAllowMethods:   []string{http.MethodGet, http.MethodHead},\n\t\t\twhenAllowContextKey: \"OPTIONS, GET\",\n\t\t\twhenOrigin:          \"\",\n\t\t\texpectAllow:         \"OPTIONS, GET\",\n\t\t},\n\t\t{\n\t\t\tname:                \"default AllowMethods, preflight, no origin, no allow header in context key and in response\",\n\t\t\tgivenAllowOrigins:   []string{\"*\"},\n\t\t\tgivenAllowMethods:   nil,\n\t\t\twhenAllowContextKey: \"\",\n\t\t\twhenOrigin:          \"\",\n\t\t\texpectAllow:         \"\",\n\t\t},\n\t\t{\n\t\t\tname:                            \"custom AllowMethods, preflight, existing origin, sets both headers different values\",\n\t\t\tgivenAllowOrigins:               []string{\"*\"},\n\t\t\tgivenAllowMethods:               []string{http.MethodGet, http.MethodHead},\n\t\t\twhenAllowContextKey:             \"OPTIONS, GET\",\n\t\t\twhenOrigin:                      \"http://google.com\",\n\t\t\texpectAllow:                     \"OPTIONS, GET\",\n\t\t\texpectAccessControlAllowMethods: \"GET,HEAD\",\n\t\t},\n\t\t{\n\t\t\tname:                            \"default AllowMethods, preflight, existing origin, sets both headers\",\n\t\t\tgivenAllowOrigins:               []string{\"*\"},\n\t\t\tgivenAllowMethods:               nil,\n\t\t\twhenAllowContextKey:             \"OPTIONS, GET\",\n\t\t\twhenOrigin:                      \"http://google.com\",\n\t\t\texpectAllow:                     \"OPTIONS, GET\",\n\t\t\texpectAccessControlAllowMethods: \"OPTIONS, GET\",\n\t\t},\n\t\t{\n\t\t\tname:                            \"default AllowMethods, preflight, existing origin, no allows, sets only CORS allow methods\",\n\t\t\tgivenAllowOrigins:               []string{\"*\"},\n\t\t\tgivenAllowMethods:               nil,\n\t\t\twhenAllowContextKey:             \"\",\n\t\t\twhenOrigin:                      \"http://google.com\",\n\t\t\texpectAllow:                     \"\",\n\t\t\texpectAccessControlAllowMethods: \"GET,HEAD,PUT,PATCH,POST,DELETE\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := echo.New()\n\t\t\te.GET(\"/test\", func(c *echo.Context) error {\n\t\t\t\treturn c.String(http.StatusOK, \"OK\")\n\t\t\t})\n\n\t\t\tcors := CORSWithConfig(CORSConfig{\n\t\t\t\tAllowOrigins: tc.givenAllowOrigins,\n\t\t\t\tAllowMethods: tc.givenAllowMethods,\n\t\t\t})\n\n\t\t\treq := httptest.NewRequest(http.MethodOptions, \"/test\", nil)\n\t\t\trec := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\treq.Header.Set(echo.HeaderOrigin, tc.whenOrigin)\n\t\t\tif tc.whenAllowContextKey != \"\" {\n\t\t\t\tc.Set(echo.ContextKeyHeaderAllow, tc.whenAllowContextKey)\n\t\t\t}\n\n\t\t\th := cors(func(c *echo.Context) error {\n\t\t\t\treturn c.String(http.StatusOK, \"OK\")\n\t\t\t})\n\t\t\th(c)\n\n\t\t\tassert.Equal(t, tc.expectAllow, rec.Header().Get(echo.HeaderAllow))\n\t\t\tassert.Equal(t, tc.expectAccessControlAllowMethods, rec.Header().Get(echo.HeaderAccessControlAllowMethods))\n\t\t})\n\t}\n}\n\nfunc TestCorsHeaders(t *testing.T) {\n\ttests := []struct {\n\t\tname              string\n\t\toriginDomain      string\n\t\tmethod            string\n\t\tallowedOrigin     string\n\t\texpected          bool\n\t\texpectStatus      int\n\t\texpectAllowHeader string\n\t}{\n\t\t{\n\t\t\tname:          \"non-preflight request, allow any origin, missing origin header = no CORS logic done\",\n\t\t\toriginDomain:  \"\",\n\t\t\tallowedOrigin: \"*\",\n\t\t\tmethod:        http.MethodGet,\n\t\t\texpected:      false,\n\t\t\texpectStatus:  http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:          \"non-preflight request, allow any origin, specific origin domain\",\n\t\t\toriginDomain:  \"http://example.com\",\n\t\t\tallowedOrigin: \"*\",\n\t\t\tmethod:        http.MethodGet,\n\t\t\texpected:      true,\n\t\t\texpectStatus:  http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:          \"non-preflight request, allow specific origin, missing origin header = no CORS logic done\",\n\t\t\toriginDomain:  \"\", // Request does not have Origin header\n\t\t\tallowedOrigin: \"http://example.com\",\n\t\t\tmethod:        http.MethodGet,\n\t\t\texpected:      false,\n\t\t\texpectStatus:  http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:          \"non-preflight request, allow specific origin, different origin header = CORS logic failure\",\n\t\t\toriginDomain:  \"http://bar.com\",\n\t\t\tallowedOrigin: \"http://example.com\",\n\t\t\tmethod:        http.MethodGet,\n\t\t\texpected:      false,\n\t\t\texpectStatus:  http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:          \"non-preflight request, allow specific origin, matching origin header = CORS logic done\",\n\t\t\toriginDomain:  \"http://example.com\",\n\t\t\tallowedOrigin: \"http://example.com\",\n\t\t\tmethod:        http.MethodGet,\n\t\t\texpected:      true,\n\t\t\texpectStatus:  http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:              \"preflight, allow any origin, missing origin header = no CORS logic done\",\n\t\t\toriginDomain:      \"\", // Request does not have Origin header\n\t\t\tallowedOrigin:     \"*\",\n\t\t\tmethod:            http.MethodOptions,\n\t\t\texpected:          false,\n\t\t\texpectStatus:      http.StatusNoContent,\n\t\t\texpectAllowHeader: \"OPTIONS, GET, POST\",\n\t\t},\n\t\t{\n\t\t\tname:              \"preflight, allow any origin, existing origin header = CORS logic done\",\n\t\t\toriginDomain:      \"http://example.com\",\n\t\t\tallowedOrigin:     \"*\",\n\t\t\tmethod:            http.MethodOptions,\n\t\t\texpected:          true,\n\t\t\texpectStatus:      http.StatusNoContent,\n\t\t\texpectAllowHeader: \"OPTIONS, GET, POST\",\n\t\t},\n\t\t{\n\t\t\tname:              \"preflight, allow any origin, missing origin header = no CORS logic done\",\n\t\t\toriginDomain:      \"\", // Request does not have Origin header\n\t\t\tallowedOrigin:     \"http://example.com\",\n\t\t\tmethod:            http.MethodOptions,\n\t\t\texpected:          false,\n\t\t\texpectStatus:      http.StatusNoContent,\n\t\t\texpectAllowHeader: \"OPTIONS, GET, POST\",\n\t\t},\n\t\t{\n\t\t\tname:              \"preflight, allow specific origin, different origin header = no CORS logic done\",\n\t\t\toriginDomain:      \"http://bar.com\",\n\t\t\tallowedOrigin:     \"http://example.com\",\n\t\t\tmethod:            http.MethodOptions,\n\t\t\texpected:          false,\n\t\t\texpectStatus:      http.StatusNoContent,\n\t\t\texpectAllowHeader: \"OPTIONS, GET, POST\",\n\t\t},\n\t\t{\n\t\t\tname:              \"preflight, allow specific origin, matching origin header = CORS logic done\",\n\t\t\toriginDomain:      \"http://example.com\",\n\t\t\tallowedOrigin:     \"http://example.com\",\n\t\t\tmethod:            http.MethodOptions,\n\t\t\texpected:          true,\n\t\t\texpectStatus:      http.StatusNoContent,\n\t\t\texpectAllowHeader: \"OPTIONS, GET, POST\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := echo.New()\n\n\t\t\te.Use(CORSWithConfig(CORSConfig{\n\t\t\t\tAllowOrigins: []string{tc.allowedOrigin},\n\t\t\t\t//AllowCredentials: true,\n\t\t\t\t//MaxAge:           3600,\n\t\t\t}))\n\n\t\t\te.GET(\"/\", func(c *echo.Context) error {\n\t\t\t\treturn c.String(http.StatusOK, \"OK\")\n\t\t\t})\n\t\t\te.POST(\"/\", func(c *echo.Context) error {\n\t\t\t\treturn c.String(http.StatusCreated, \"OK\")\n\t\t\t})\n\n\t\t\treq := httptest.NewRequest(tc.method, \"/\", nil)\n\t\t\trec := httptest.NewRecorder()\n\n\t\t\tif tc.originDomain != \"\" {\n\t\t\t\treq.Header.Set(echo.HeaderOrigin, tc.originDomain)\n\t\t\t}\n\n\t\t\t// we run through whole Echo handler chain to see how CORS works with Router OPTIONS handler\n\t\t\te.ServeHTTP(rec, req)\n\n\t\t\tassert.Equal(t, echo.HeaderOrigin, rec.Header().Get(echo.HeaderVary))\n\t\t\tassert.Equal(t, tc.expectAllowHeader, rec.Header().Get(echo.HeaderAllow))\n\t\t\tassert.Equal(t, tc.expectStatus, rec.Code)\n\n\t\t\texpectedAllowOrigin := \"\"\n\t\t\tif tc.allowedOrigin == \"*\" {\n\t\t\t\texpectedAllowOrigin = \"*\"\n\t\t\t} else {\n\t\t\t\texpectedAllowOrigin = tc.originDomain\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase tc.expected && tc.method == http.MethodOptions:\n\t\t\t\tassert.Contains(t, rec.Header(), echo.HeaderAccessControlAllowMethods)\n\t\t\t\tassert.Equal(t, expectedAllowOrigin, rec.Header().Get(echo.HeaderAccessControlAllowOrigin))\n\n\t\t\t\tassert.Equal(t, 3, len(rec.Header()[echo.HeaderVary]))\n\n\t\t\tcase tc.expected && tc.method == http.MethodGet:\n\t\t\t\tassert.Equal(t, expectedAllowOrigin, rec.Header().Get(echo.HeaderAccessControlAllowOrigin))\n\t\t\t\tassert.Equal(t, 1, len(rec.Header()[echo.HeaderVary])) // Vary: Origin\n\t\t\tdefault:\n\t\t\t\tassert.NotContains(t, rec.Header(), echo.HeaderAccessControlAllowOrigin)\n\t\t\t\tassert.Equal(t, 1, len(rec.Header()[echo.HeaderVary])) // Vary: Origin\n\t\t\t}\n\t\t})\n\n\t}\n}\n\nfunc Test_allowOriginFunc(t *testing.T) {\n\treturnTrue := func(c *echo.Context, origin string) (string, bool, error) {\n\t\treturn origin, true, nil\n\t}\n\treturnFalse := func(c *echo.Context, origin string) (string, bool, error) {\n\t\treturn origin, false, nil\n\t}\n\treturnError := func(c *echo.Context, origin string) (string, bool, error) {\n\t\treturn origin, true, errors.New(\"this is a test error\")\n\t}\n\n\tallowOriginFuncs := []func(c *echo.Context, origin string) (string, bool, error){\n\t\treturnTrue,\n\t\treturnFalse,\n\t\treturnError,\n\t}\n\n\tconst origin = \"http://example.com\"\n\n\te := echo.New()\n\tfor _, allowOriginFunc := range allowOriginFuncs {\n\t\treq := httptest.NewRequest(http.MethodOptions, \"/\", nil)\n\t\trec := httptest.NewRecorder()\n\t\tc := e.NewContext(req, rec)\n\t\treq.Header.Set(echo.HeaderOrigin, origin)\n\t\tcors, err := CORSConfig{UnsafeAllowOriginFunc: allowOriginFunc}.ToMiddleware()\n\t\tassert.NoError(t, err)\n\n\t\th := cors(func(c *echo.Context) error { return echo.ErrNotFound })\n\t\terr = h(c)\n\n\t\tallowedOrigin, allowed, expectedErr := allowOriginFunc(c, origin)\n\t\tif expectedErr != nil {\n\t\t\tassert.Equal(t, expectedErr, err)\n\t\t\tassert.Equal(t, \"\", rec.Header().Get(echo.HeaderAccessControlAllowOrigin))\n\t\t\tcontinue\n\t\t}\n\n\t\tif allowed {\n\t\t\tassert.Equal(t, allowedOrigin, rec.Header().Get(echo.HeaderAccessControlAllowOrigin))\n\t\t} else {\n\t\t\tassert.Equal(t, \"\", rec.Header().Get(echo.HeaderAccessControlAllowOrigin))\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "middleware/csrf.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"crypto/subtle\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\n// CSRFUsingSecFetchSite is a context key for CSRF middleware what is set when the client browser is using Sec-Fetch-Site\n// header and the request is deemed safe.\n// It is a dummy token value that can be used to render CSRF token for form by handlers.\n//\n// We know that the client is using a browser that supports Sec-Fetch-Site header, so when the form is submitted in\n// the future with this dummy token value it is OK. Although the request is safe, the template rendered by the\n// handler may need this value to render CSRF token for form.\nconst CSRFUsingSecFetchSite = \"_echo_csrf_using_sec_fetch_site_\"\n\n// CSRFConfig defines the config for CSRF middleware.\ntype CSRFConfig struct {\n\t// Skipper defines a function to skip middleware.\n\tSkipper Skipper\n\t// TrustedOrigin permits any request with `Sec-Fetch-Site` header whose `Origin` header\n\t// exactly matches the specified value.\n\t// Values should be formatted as Origin header \"scheme://host[:port]\".\n\t//\n\t// See [Origin]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin\n\t// See [Sec-Fetch-Site]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#fetch-metadata-headers\n\tTrustedOrigins []string\n\n\t// AllowSecFetchSameSite allows custom behaviour for `Sec-Fetch-Site` requests that are about to\n\t// fail with CRSF error, to be allowed or replaced with custom error.\n\t// This function applies to `Sec-Fetch-Site` values:\n\t// - `same-site` \t\tsame registrable domain (subdomain and/or different port)\n\t// - `cross-site`\t\trequest originates from different site\n\t// See [Sec-Fetch-Site]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#fetch-metadata-headers\n\tAllowSecFetchSiteFunc func(c *echo.Context) (bool, error)\n\n\t// TokenLength is the length of the generated token.\n\tTokenLength uint8\n\t// Optional. Default value 32.\n\n\t// TokenLookup is a string in the form of \"<source>:<name>\" or \"<source>:<name>,<source>:<name>\" that is used\n\t// to extract token from the request.\n\t// Optional. Default value \"header:X-CSRF-Token\".\n\t// Possible values:\n\t// - \"header:<name>\" or \"header:<name>:<cut-prefix>\"\n\t// - \"query:<name>\"\n\t// - \"form:<name>\"\n\t// Multiple sources example:\n\t// - \"header:X-CSRF-Token,query:csrf\"\n\tTokenLookup string `yaml:\"token_lookup\"`\n\n\t// Generator defines a function to generate token.\n\t// Optional. Defaults tp randomString(TokenLength).\n\tGenerator func() string\n\n\t// Context key to store generated CSRF token into context.\n\t// Optional. Default value \"csrf\".\n\tContextKey string\n\n\t// Name of the CSRF cookie. This cookie will store CSRF token.\n\t// Optional. Default value \"csrf\".\n\tCookieName string\n\n\t// Domain of the CSRF cookie.\n\t// Optional. Default value none.\n\tCookieDomain string\n\n\t// Path of the CSRF cookie.\n\t// Optional. Default value none.\n\tCookiePath string\n\n\t// Max age (in seconds) of the CSRF cookie.\n\t// Optional. Default value 86400 (24hr).\n\tCookieMaxAge int\n\n\t// Indicates if CSRF cookie is secure.\n\t// Optional. Default value false.\n\tCookieSecure bool\n\n\t// Indicates if CSRF cookie is HTTP only.\n\t// Optional. Default value false.\n\tCookieHTTPOnly bool\n\n\t// Indicates SameSite mode of the CSRF cookie.\n\t// Optional. Default value SameSiteDefaultMode.\n\tCookieSameSite http.SameSite\n\n\t// ErrorHandler defines a function which is executed for returning custom errors.\n\tErrorHandler func(c *echo.Context, err error) error\n}\n\n// ErrCSRFInvalid is returned when CSRF check fails\nvar ErrCSRFInvalid = &echo.HTTPError{Code: http.StatusForbidden, Message: \"invalid csrf token\"}\n\n// DefaultCSRFConfig is the default CSRF middleware config.\nvar DefaultCSRFConfig = CSRFConfig{\n\tSkipper:        DefaultSkipper,\n\tTokenLength:    32,\n\tTokenLookup:    \"header:\" + echo.HeaderXCSRFToken,\n\tContextKey:     \"csrf\",\n\tCookieName:     \"_csrf\",\n\tCookieMaxAge:   86400,\n\tCookieSameSite: http.SameSiteDefaultMode,\n}\n\n// CSRF returns a Cross-Site Request Forgery (CSRF) middleware.\n// See: https://en.wikipedia.org/wiki/Cross-site_request_forgery\nfunc CSRF() echo.MiddlewareFunc {\n\treturn CSRFWithConfig(DefaultCSRFConfig)\n}\n\n// CSRFWithConfig returns a CSRF middleware with config or panics on invalid configuration.\nfunc CSRFWithConfig(config CSRFConfig) echo.MiddlewareFunc {\n\treturn toMiddlewareOrPanic(config)\n}\n\n// ToMiddleware converts CSRFConfig to middleware or returns an error for invalid configuration\nfunc (config CSRFConfig) ToMiddleware() (echo.MiddlewareFunc, error) {\n\t// Defaults\n\tif config.Skipper == nil {\n\t\tconfig.Skipper = DefaultCSRFConfig.Skipper\n\t}\n\tif config.TokenLength == 0 {\n\t\tconfig.TokenLength = DefaultCSRFConfig.TokenLength\n\t}\n\tif config.Generator == nil {\n\t\tconfig.Generator = createRandomStringGenerator(config.TokenLength)\n\t}\n\tif config.TokenLookup == \"\" {\n\t\tconfig.TokenLookup = DefaultCSRFConfig.TokenLookup\n\t}\n\tif config.ContextKey == \"\" {\n\t\tconfig.ContextKey = DefaultCSRFConfig.ContextKey\n\t}\n\tif config.CookieName == \"\" {\n\t\tconfig.CookieName = DefaultCSRFConfig.CookieName\n\t}\n\tif config.CookieMaxAge == 0 {\n\t\tconfig.CookieMaxAge = DefaultCSRFConfig.CookieMaxAge\n\t}\n\tif config.CookieSameSite == http.SameSiteNoneMode {\n\t\tconfig.CookieSecure = true\n\t}\n\tif len(config.TrustedOrigins) > 0 {\n\t\tif err := validateOrigins(config.TrustedOrigins, \"trusted origin\"); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tconfig.TrustedOrigins = append([]string(nil), config.TrustedOrigins...)\n\t}\n\n\textractors, cErr := createExtractors(config.TokenLookup, 1)\n\tif cErr != nil {\n\t\treturn nil, cErr\n\t}\n\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) error {\n\t\t\tif config.Skipper(c) {\n\t\t\t\treturn next(c)\n\t\t\t}\n\n\t\t\t// use the `Sec-Fetch-Site` header as part of a modern approach to CSRF protection\n\t\t\tallow, err := config.checkSecFetchSiteRequest(c)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif allow {\n\t\t\t\treturn next(c)\n\t\t\t}\n\n\t\t\t// Fallback to legacy token based CSRF protection\n\n\t\t\ttoken := \"\"\n\t\t\tif k, err := c.Cookie(config.CookieName); err != nil {\n\t\t\t\ttoken = config.Generator() // Generate token\n\t\t\t} else {\n\t\t\t\ttoken = k.Value // Reuse token\n\t\t\t}\n\n\t\t\tswitch c.Request().Method {\n\t\t\tcase http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace:\n\t\t\tdefault:\n\t\t\t\t// Validate token only for requests which are not defined as 'safe' by RFC7231\n\t\t\t\tvar lastExtractorErr error\n\t\t\t\tvar lastTokenErr error\n\t\t\touter:\n\t\t\t\tfor _, extractor := range extractors {\n\t\t\t\t\tclientTokens, _, err := extractor(c)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlastExtractorErr = err\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tfor _, clientToken := range clientTokens {\n\t\t\t\t\t\tif validateCSRFToken(token, clientToken) {\n\t\t\t\t\t\t\tlastTokenErr = nil\n\t\t\t\t\t\t\tlastExtractorErr = nil\n\t\t\t\t\t\t\tbreak outer\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlastTokenErr = ErrCSRFInvalid\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tvar finalErr error\n\t\t\t\tif lastTokenErr != nil {\n\t\t\t\t\tfinalErr = lastTokenErr\n\t\t\t\t} else if lastExtractorErr != nil {\n\t\t\t\t\tfinalErr = echo.ErrBadRequest.Wrap(lastExtractorErr)\n\t\t\t\t}\n\t\t\t\tif finalErr != nil {\n\t\t\t\t\tif config.ErrorHandler != nil {\n\t\t\t\t\t\treturn config.ErrorHandler(c, finalErr)\n\t\t\t\t\t}\n\t\t\t\t\treturn finalErr\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Set CSRF cookie\n\t\t\tcookie := new(http.Cookie)\n\t\t\tcookie.Name = config.CookieName\n\t\t\tcookie.Value = token\n\t\t\tif config.CookiePath != \"\" {\n\t\t\t\tcookie.Path = config.CookiePath\n\t\t\t}\n\t\t\tif config.CookieDomain != \"\" {\n\t\t\t\tcookie.Domain = config.CookieDomain\n\t\t\t}\n\t\t\tif config.CookieSameSite != http.SameSiteDefaultMode {\n\t\t\t\tcookie.SameSite = config.CookieSameSite\n\t\t\t}\n\t\t\tcookie.Expires = time.Now().Add(time.Duration(config.CookieMaxAge) * time.Second)\n\t\t\tcookie.Secure = config.CookieSecure\n\t\t\tcookie.HttpOnly = config.CookieHTTPOnly\n\t\t\tc.SetCookie(cookie)\n\n\t\t\t// Store token in the context\n\t\t\tc.Set(config.ContextKey, token)\n\n\t\t\t// Protect clients from caching the response\n\t\t\tc.Response().Header().Add(echo.HeaderVary, echo.HeaderCookie)\n\n\t\t\treturn next(c)\n\t\t}\n\t}, nil\n}\n\nfunc validateCSRFToken(token, clientToken string) bool {\n\treturn subtle.ConstantTimeCompare([]byte(token), []byte(clientToken)) == 1\n}\n\nvar safeMethods = []string{http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace}\n\nfunc (config CSRFConfig) checkSecFetchSiteRequest(c *echo.Context) (bool, error) {\n\t// https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#fetch-metadata-headers\n\t// Sec-Fetch-Site values are:\n\t// - `same-origin` \texact origin match - allow always\n\t// - `same-site` \t\tsame registrable domain (subdomain and/or different port) - block, unless explicitly trusted\n\t// - `cross-site`\t\trequest originates from different site - block, unless explicitly trusted\n\t// - `none`\t\t\t\t\tdirect navigation (URL bar, bookmark) - allow always\n\tsecFetchSite := c.Request().Header.Get(echo.HeaderSecFetchSite)\n\tif secFetchSite == \"\" {\n\t\treturn false, nil\n\t}\n\n\tif len(config.TrustedOrigins) > 0 {\n\t\t// trusted sites ala OAuth callbacks etc. should be let through\n\t\torigin := c.Request().Header.Get(echo.HeaderOrigin)\n\t\tif origin != \"\" {\n\t\t\tfor _, trustedOrigin := range config.TrustedOrigins {\n\t\t\t\tif strings.EqualFold(origin, trustedOrigin) {\n\t\t\t\t\treturn true, nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tisSafe := slices.Contains(safeMethods, c.Request().Method)\n\tif !isSafe { // for state-changing request check SecFetchSite value\n\t\tisSafe = secFetchSite == \"same-origin\" || secFetchSite == \"none\"\n\t}\n\n\tif isSafe {\n\t\t// This helps handlers that support older token-based CSRF protection.\n\t\t// We know that the client is using a browser that supports Sec-Fetch-Site header, so when the form is submitted in\n\t\t// the future with this dummy token value it is OK. Although the request is safe, the template rendered by the\n\t\t// handler may need this value to render CSRF token for form.\n\t\tc.Set(config.ContextKey, CSRFUsingSecFetchSite)\n\t\treturn true, nil\n\t}\n\t// we are here when request is state-changing and `cross-site` or `same-site`\n\n\t// Note: if you want to allow `same-site` use config.TrustedOrigins or `config.AllowSecFetchSiteFunc`\n\tif config.AllowSecFetchSiteFunc != nil {\n\t\treturn config.AllowSecFetchSiteFunc(c)\n\t}\n\n\tif secFetchSite == \"same-site\" {\n\t\treturn false, echo.NewHTTPError(http.StatusForbidden, \"same-site request blocked by CSRF\")\n\t}\n\treturn false, echo.NewHTTPError(http.StatusForbidden, \"cross-site request blocked by CSRF\")\n}\n"
  },
  {
    "path": "middleware/csrf_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"cmp\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCSRF_tokenExtractors(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname                    string\n\t\twhenTokenLookup         string\n\t\twhenCookieName          string\n\t\tgivenCSRFCookie         string\n\t\tgivenMethod             string\n\t\tgivenQueryTokens        map[string][]string\n\t\tgivenFormTokens         map[string][]string\n\t\tgivenHeaderTokens       map[string][]string\n\t\texpectError             string\n\t\texpectToMiddlewareError string\n\t}{\n\t\t{\n\t\t\tname:            \"ok, multiple token lookups sources, succeeds on last one\",\n\t\t\twhenTokenLookup: \"header:X-CSRF-Token,form:csrf\",\n\t\t\tgivenCSRFCookie: \"token\",\n\t\t\tgivenMethod:     http.MethodPost,\n\t\t\tgivenHeaderTokens: map[string][]string{\n\t\t\t\techo.HeaderXCSRFToken: {\"invalid_token\"},\n\t\t\t},\n\t\t\tgivenFormTokens: map[string][]string{\n\t\t\t\t\"csrf\": {\"token\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:            \"ok, token from POST form\",\n\t\t\twhenTokenLookup: \"form:csrf\",\n\t\t\tgivenCSRFCookie: \"token\",\n\t\t\tgivenMethod:     http.MethodPost,\n\t\t\tgivenFormTokens: map[string][]string{\n\t\t\t\t\"csrf\": {\"token\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:            \"ok, token from POST form, second token passes\",\n\t\t\twhenTokenLookup: \"form:csrf\",\n\t\t\tgivenCSRFCookie: \"token\",\n\t\t\tgivenMethod:     http.MethodPost,\n\t\t\tgivenFormTokens: map[string][]string{\n\t\t\t\t\"csrf\": {\"invalid\", \"token\"},\n\t\t\t},\n\t\t\texpectError: \"code=403, message=invalid csrf token\",\n\t\t},\n\t\t{\n\t\t\tname:            \"nok, invalid token from POST form\",\n\t\t\twhenTokenLookup: \"form:csrf\",\n\t\t\tgivenCSRFCookie: \"token\",\n\t\t\tgivenMethod:     http.MethodPost,\n\t\t\tgivenFormTokens: map[string][]string{\n\t\t\t\t\"csrf\": {\"invalid_token\"},\n\t\t\t},\n\t\t\texpectError: \"code=403, message=invalid csrf token\",\n\t\t},\n\t\t{\n\t\t\tname:            \"nok, missing token from POST form\",\n\t\t\twhenTokenLookup: \"form:csrf\",\n\t\t\tgivenCSRFCookie: \"token\",\n\t\t\tgivenMethod:     http.MethodPost,\n\t\t\tgivenFormTokens: map[string][]string{},\n\t\t\texpectError:     \"code=400, message=Bad Request, err=missing value in the form\",\n\t\t},\n\t\t{\n\t\t\tname:            \"ok, token from POST header\",\n\t\t\twhenTokenLookup: \"\", // will use defaults\n\t\t\tgivenCSRFCookie: \"token\",\n\t\t\tgivenMethod:     http.MethodPost,\n\t\t\tgivenHeaderTokens: map[string][]string{\n\t\t\t\techo.HeaderXCSRFToken: {\"token\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:            \"nok, token from POST header, tokens limited to 1, second token would pass\",\n\t\t\twhenTokenLookup: \"header:\" + echo.HeaderXCSRFToken,\n\t\t\tgivenCSRFCookie: \"token\",\n\t\t\tgivenMethod:     http.MethodPost,\n\t\t\tgivenHeaderTokens: map[string][]string{\n\t\t\t\techo.HeaderXCSRFToken: {\"invalid\", \"token\"},\n\t\t\t},\n\t\t\texpectError: \"code=403, message=invalid csrf token\",\n\t\t},\n\t\t{\n\t\t\tname:            \"nok, invalid token from POST header\",\n\t\t\twhenTokenLookup: \"header:\" + echo.HeaderXCSRFToken,\n\t\t\tgivenCSRFCookie: \"token\",\n\t\t\tgivenMethod:     http.MethodPost,\n\t\t\tgivenHeaderTokens: map[string][]string{\n\t\t\t\techo.HeaderXCSRFToken: {\"invalid_token\"},\n\t\t\t},\n\t\t\texpectError: \"code=403, message=invalid csrf token\",\n\t\t},\n\t\t{\n\t\t\tname:              \"nok, missing token from POST header\",\n\t\t\twhenTokenLookup:   \"header:\" + echo.HeaderXCSRFToken,\n\t\t\tgivenCSRFCookie:   \"token\",\n\t\t\tgivenMethod:       http.MethodPost,\n\t\t\tgivenHeaderTokens: map[string][]string{},\n\t\t\texpectError:       \"code=400, message=Bad Request, err=missing value in request header\",\n\t\t},\n\t\t{\n\t\t\tname:            \"ok, token from PUT query param\",\n\t\t\twhenTokenLookup: \"query:csrf-param\",\n\t\t\tgivenCSRFCookie: \"token\",\n\t\t\tgivenMethod:     http.MethodPut,\n\t\t\tgivenQueryTokens: map[string][]string{\n\t\t\t\t\"csrf-param\": {\"token\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:            \"nok, token from PUT query form, second token would pass\",\n\t\t\twhenTokenLookup: \"query:csrf\",\n\t\t\tgivenCSRFCookie: \"token\",\n\t\t\tgivenMethod:     http.MethodPut,\n\t\t\tgivenQueryTokens: map[string][]string{\n\t\t\t\t\"csrf\": {\"invalid\", \"token\"},\n\t\t\t},\n\t\t\texpectError: \"code=403, message=invalid csrf token\",\n\t\t},\n\t\t{\n\t\t\tname:            \"nok, invalid token from PUT query form\",\n\t\t\twhenTokenLookup: \"query:csrf\",\n\t\t\tgivenCSRFCookie: \"token\",\n\t\t\tgivenMethod:     http.MethodPut,\n\t\t\tgivenQueryTokens: map[string][]string{\n\t\t\t\t\"csrf\": {\"invalid_token\"},\n\t\t\t},\n\t\t\texpectError: \"code=403, message=invalid csrf token\",\n\t\t},\n\t\t{\n\t\t\tname:             \"nok, missing token from PUT query form\",\n\t\t\twhenTokenLookup:  \"query:csrf\",\n\t\t\tgivenCSRFCookie:  \"token\",\n\t\t\tgivenMethod:      http.MethodPut,\n\t\t\tgivenQueryTokens: map[string][]string{},\n\t\t\texpectError:      \"code=400, message=Bad Request, err=missing value in the query string\",\n\t\t},\n\t\t{\n\t\t\tname:                    \"nok, invalid TokenLookup\",\n\t\t\twhenTokenLookup:         \"q\",\n\t\t\tgivenCSRFCookie:         \"token\",\n\t\t\tgivenMethod:             http.MethodPut,\n\t\t\tgivenQueryTokens:        map[string][]string{},\n\t\t\texpectToMiddlewareError: \"extractor source for lookup could not be split into needed parts: q\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := echo.New()\n\n\t\t\tq := make(url.Values)\n\t\t\tfor queryParam, values := range tc.givenQueryTokens {\n\t\t\t\tfor _, v := range values {\n\t\t\t\t\tq.Add(queryParam, v)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tf := make(url.Values)\n\t\t\tfor formKey, values := range tc.givenFormTokens {\n\t\t\t\tfor _, v := range values {\n\t\t\t\t\tf.Add(formKey, v)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar req *http.Request\n\t\t\tswitch tc.givenMethod {\n\t\t\tcase http.MethodGet:\n\t\t\t\treq = httptest.NewRequest(http.MethodGet, \"/?\"+q.Encode(), nil)\n\t\t\tcase http.MethodPost, http.MethodPut:\n\t\t\t\treq = httptest.NewRequest(http.MethodPost, \"/?\"+q.Encode(), strings.NewReader(f.Encode()))\n\t\t\t\treq.Header.Add(echo.HeaderContentType, echo.MIMEApplicationForm)\n\t\t\t}\n\n\t\t\tfor header, values := range tc.givenHeaderTokens {\n\t\t\t\tfor _, v := range values {\n\t\t\t\t\treq.Header.Add(header, v)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif tc.givenCSRFCookie != \"\" {\n\t\t\t\treq.Header.Set(echo.HeaderCookie, \"_csrf=\"+tc.givenCSRFCookie)\n\t\t\t}\n\n\t\t\trec := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\tconfig := CSRFConfig{\n\t\t\t\tTokenLookup: tc.whenTokenLookup,\n\t\t\t\tCookieName:  tc.whenCookieName,\n\t\t\t}\n\t\t\tcsrf, err := config.ToMiddleware()\n\t\t\tif tc.expectToMiddlewareError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectToMiddlewareError)\n\t\t\t\treturn\n\t\t\t} else if err != nil {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\n\t\t\th := csrf(func(c *echo.Context) error {\n\t\t\t\treturn c.String(http.StatusOK, \"test\")\n\t\t\t})\n\n\t\t\terr = h(c)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCSRFWithConfig(t *testing.T) {\n\ttoken := randomString(16)\n\n\tvar testCases = []struct {\n\t\tname                 string\n\t\tgivenConfig          *CSRFConfig\n\t\twhenMethod           string\n\t\twhenHeaders          map[string]string\n\t\texpectEmptyBody      bool\n\t\texpectMWError        string\n\t\texpectCookieContains string\n\t\texpectTokenInContext string\n\t\texpectErr            string\n\t}{\n\t\t{\n\t\t\tname:                 \"ok, GET\",\n\t\t\twhenMethod:           http.MethodGet,\n\t\t\texpectCookieContains: \"_csrf\",\n\t\t\texpectTokenInContext: \"TESTTOKEN\",\n\t\t},\n\t\t{\n\t\t\tname: \"ok, POST valid token\",\n\t\t\twhenHeaders: map[string]string{\n\t\t\t\techo.HeaderCookie:     \"_csrf=\" + token,\n\t\t\t\techo.HeaderXCSRFToken: token,\n\t\t\t},\n\t\t\twhenMethod:           http.MethodPost,\n\t\t\texpectCookieContains: \"_csrf\",\n\t\t\texpectTokenInContext: token,\n\t\t},\n\t\t{\n\t\t\tname:            \"nok, POST without token\",\n\t\t\twhenMethod:      http.MethodPost,\n\t\t\texpectEmptyBody: true,\n\t\t\texpectErr:       `code=400, message=Bad Request, err=missing value in request header`,\n\t\t},\n\t\t{\n\t\t\tname:            \"nok, POST empty token\",\n\t\t\twhenHeaders:     map[string]string{echo.HeaderXCSRFToken: \"\"},\n\t\t\twhenMethod:      http.MethodPost,\n\t\t\texpectEmptyBody: true,\n\t\t\texpectErr:       `code=403, message=invalid csrf token`,\n\t\t},\n\t\t{\n\t\t\tname: \"nok, invalid trusted origin in Config\",\n\t\t\tgivenConfig: &CSRFConfig{\n\t\t\t\tTrustedOrigins: []string{\"http://example.com\", \"invalid\"},\n\t\t\t},\n\t\t\texpectMWError: `trusted origin is missing scheme or host: invalid`,\n\t\t},\n\t\t{\n\t\t\tname: \"ok, TokenLength\",\n\t\t\tgivenConfig: &CSRFConfig{\n\t\t\t\tTokenLength: 16,\n\t\t\t},\n\t\t\twhenMethod:           http.MethodGet,\n\t\t\texpectCookieContains: \"_csrf\",\n\t\t\texpectTokenInContext: \"TESTTOKEN\",\n\t\t},\n\t\t{\n\t\t\tname: \"ok, unsafe method + SecFetchSite=same-origin passes\",\n\t\t\twhenHeaders: map[string]string{\n\t\t\t\techo.HeaderSecFetchSite: \"same-origin\",\n\t\t\t},\n\t\t\twhenMethod:           http.MethodPost,\n\t\t\texpectTokenInContext: \"_echo_csrf_using_sec_fetch_site_\",\n\t\t},\n\t\t{\n\t\t\tname: \"ok, safe method + SecFetchSite=same-origin passes\",\n\t\t\twhenHeaders: map[string]string{\n\t\t\t\techo.HeaderSecFetchSite: \"same-origin\",\n\t\t\t},\n\t\t\twhenMethod:           http.MethodGet,\n\t\t\texpectTokenInContext: \"_echo_csrf_using_sec_fetch_site_\",\n\t\t},\n\t\t{\n\t\t\tname: \"nok, unsafe method + SecFetchSite=same-cross blocked\",\n\t\t\twhenHeaders: map[string]string{\n\t\t\t\techo.HeaderSecFetchSite: \"same-cross\",\n\t\t\t},\n\t\t\twhenMethod:      http.MethodPost,\n\t\t\texpectEmptyBody: true,\n\t\t\texpectErr:       `code=403, message=cross-site request blocked by CSRF`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := echo.New()\n\n\t\t\treq := httptest.NewRequest(cmp.Or(tc.whenMethod, http.MethodPost), \"/\", nil)\n\t\t\trec := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\tfor key, value := range tc.whenHeaders {\n\t\t\t\treq.Header.Set(key, value)\n\t\t\t}\n\n\t\t\tconfig := CSRFConfig{}\n\t\t\tif tc.givenConfig != nil {\n\t\t\t\tconfig = *tc.givenConfig\n\t\t\t}\n\t\t\tif config.Generator == nil {\n\t\t\t\tconfig.Generator = func() string {\n\t\t\t\t\treturn \"TESTTOKEN\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmw, err := config.ToMiddleware()\n\t\t\tif tc.expectMWError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectMWError)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.NoError(t, err)\n\n\t\t\th := mw(func(c *echo.Context) error {\n\t\t\t\tcToken := c.Get(cmp.Or(config.ContextKey, DefaultCSRFConfig.ContextKey))\n\t\t\t\tassert.Equal(t, tc.expectTokenInContext, cToken)\n\t\t\t\treturn c.String(http.StatusOK, \"test\")\n\t\t\t})\n\n\t\t\terr = h(c)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\n\t\t\texpect := \"test\"\n\t\t\tif tc.expectEmptyBody {\n\t\t\t\texpect = \"\"\n\t\t\t}\n\t\t\tassert.Equal(t, expect, rec.Body.String())\n\n\t\t\tif tc.expectCookieContains != \"\" {\n\t\t\t\tassert.Contains(t, rec.Header().Get(echo.HeaderSetCookie), tc.expectCookieContains)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCSRF(t *testing.T) {\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tcsrf := CSRF()\n\th := csrf(func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t})\n\n\t// Generate CSRF token\n\th(c)\n\tassert.Contains(t, rec.Header().Get(echo.HeaderSetCookie), \"_csrf\")\n\n}\n\nfunc TestCSRFSetSameSiteMode(t *testing.T) {\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\tcsrf := CSRFWithConfig(CSRFConfig{\n\t\tCookieSameSite: http.SameSiteStrictMode,\n\t})\n\n\th := csrf(func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t})\n\n\tr := h(c)\n\tassert.NoError(t, r)\n\tassert.Regexp(t, \"SameSite=Strict\", rec.Header()[\"Set-Cookie\"])\n}\n\nfunc TestCSRFWithoutSameSiteMode(t *testing.T) {\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\tcsrf := CSRFWithConfig(CSRFConfig{})\n\n\th := csrf(func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t})\n\n\tr := h(c)\n\tassert.NoError(t, r)\n\tassert.NotRegexp(t, \"SameSite=\", rec.Header()[\"Set-Cookie\"])\n}\n\nfunc TestCSRFWithSameSiteDefaultMode(t *testing.T) {\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\tcsrf := CSRFWithConfig(CSRFConfig{\n\t\tCookieSameSite: http.SameSiteDefaultMode,\n\t})\n\n\th := csrf(func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t})\n\n\tr := h(c)\n\tassert.NoError(t, r)\n\tassert.NotRegexp(t, \"SameSite=\", rec.Header()[\"Set-Cookie\"])\n}\n\nfunc TestCSRFWithSameSiteModeNone(t *testing.T) {\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\tcsrf, err := CSRFConfig{\n\t\tCookieSameSite: http.SameSiteNoneMode,\n\t}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\th := csrf(func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t})\n\n\tr := h(c)\n\tassert.NoError(t, r)\n\tassert.Regexp(t, \"SameSite=None\", rec.Header()[\"Set-Cookie\"])\n\tassert.Regexp(t, \"Secure\", rec.Header()[\"Set-Cookie\"])\n}\n\nfunc TestCSRFConfig_skipper(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname          string\n\t\twhenSkip      bool\n\t\texpectCookies int\n\t}{\n\t\t{\n\t\t\tname:          \"do skip\",\n\t\t\twhenSkip:      true,\n\t\t\texpectCookies: 0,\n\t\t},\n\t\t{\n\t\t\tname:          \"do not skip\",\n\t\t\twhenSkip:      false,\n\t\t\texpectCookies: 1,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := echo.New()\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\t\trec := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\tcsrf := CSRFWithConfig(CSRFConfig{\n\t\t\t\tSkipper: func(c *echo.Context) bool {\n\t\t\t\t\treturn tc.whenSkip\n\t\t\t\t},\n\t\t\t})\n\n\t\t\th := csrf(func(c *echo.Context) error {\n\t\t\t\treturn c.String(http.StatusOK, \"test\")\n\t\t\t})\n\n\t\t\tr := h(c)\n\t\t\tassert.NoError(t, r)\n\t\t\tcookie := rec.Header()[\"Set-Cookie\"]\n\t\t\tassert.Len(t, cookie, tc.expectCookies)\n\t\t})\n\t}\n}\n\nfunc TestCSRFErrorHandling(t *testing.T) {\n\tcfg := CSRFConfig{\n\t\tErrorHandler: func(c *echo.Context, err error) error {\n\t\t\treturn echo.NewHTTPError(http.StatusTeapot, \"error_handler_executed\")\n\t\t},\n\t}\n\n\te := echo.New()\n\te.POST(\"/\", func(c *echo.Context) error {\n\t\treturn c.String(http.StatusNotImplemented, \"should not end up here\")\n\t})\n\n\te.Use(CSRFWithConfig(cfg))\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", nil)\n\tres := httptest.NewRecorder()\n\te.ServeHTTP(res, req)\n\n\tassert.Equal(t, http.StatusTeapot, res.Code)\n\tassert.Equal(t, \"{\\\"message\\\":\\\"error_handler_executed\\\"}\\n\", res.Body.String())\n}\n\nfunc TestCSRFConfig_checkSecFetchSiteRequest(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname             string\n\t\tgivenConfig      CSRFConfig\n\t\twhenMethod       string\n\t\twhenSecFetchSite string\n\t\twhenOrigin       string\n\t\texpectAllow      bool\n\t\texpectErr        string\n\t}{\n\t\t{\n\t\t\tname:             \"ok, unsafe POST, no SecFetchSite is not blocked\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodPost,\n\t\t\twhenSecFetchSite: \"\",\n\t\t\texpectAllow:      false, // should fall back to token CSRF\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, safe GET + same-origin passes\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodGet,\n\t\t\twhenSecFetchSite: \"same-origin\",\n\t\t\texpectAllow:      true,\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, safe GET + none passes\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodGet,\n\t\t\twhenSecFetchSite: \"none\",\n\t\t\texpectAllow:      true,\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, safe GET + same-site passes\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodGet,\n\t\t\twhenSecFetchSite: \"same-site\",\n\t\t\texpectAllow:      true,\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, safe GET + cross-site passes\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodGet,\n\t\t\twhenSecFetchSite: \"cross-site\",\n\t\t\texpectAllow:      true,\n\t\t},\n\t\t{\n\t\t\tname:             \"nok, unsafe POST + cross-site is blocked\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodPost,\n\t\t\twhenSecFetchSite: \"cross-site\",\n\t\t\texpectAllow:      false,\n\t\t\texpectErr:        `code=403, message=cross-site request blocked by CSRF`,\n\t\t},\n\t\t{\n\t\t\tname:             \"nok, unsafe POST + same-site is blocked\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodPost,\n\t\t\twhenSecFetchSite: \"same-site\",\n\t\t\texpectAllow:      false,\n\t\t\texpectErr:        `code=403, message=same-site request blocked by CSRF`,\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, unsafe POST + same-origin passes\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodPost,\n\t\t\twhenSecFetchSite: \"same-origin\",\n\t\t\texpectAllow:      true,\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, unsafe POST + none passes\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodPost,\n\t\t\twhenSecFetchSite: \"none\",\n\t\t\texpectAllow:      true,\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, unsafe PUT + same-origin passes\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodPut,\n\t\t\twhenSecFetchSite: \"same-origin\",\n\t\t\texpectAllow:      true,\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, unsafe PUT + none passes\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodPut,\n\t\t\twhenSecFetchSite: \"none\",\n\t\t\texpectAllow:      true,\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, unsafe DELETE + same-origin passes\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodDelete,\n\t\t\twhenSecFetchSite: \"same-origin\",\n\t\t\texpectAllow:      true,\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, unsafe PATCH + same-origin passes\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodPatch,\n\t\t\twhenSecFetchSite: \"same-origin\",\n\t\t\texpectAllow:      true,\n\t\t},\n\t\t{\n\t\t\tname:             \"nok, unsafe PUT + cross-site is blocked\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodPut,\n\t\t\twhenSecFetchSite: \"cross-site\",\n\t\t\texpectAllow:      false,\n\t\t\texpectErr:        `code=403, message=cross-site request blocked by CSRF`,\n\t\t},\n\t\t{\n\t\t\tname:             \"nok, unsafe PUT + same-site is blocked\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodPut,\n\t\t\twhenSecFetchSite: \"same-site\",\n\t\t\texpectAllow:      false,\n\t\t\texpectErr:        `code=403, message=same-site request blocked by CSRF`,\n\t\t},\n\t\t{\n\t\t\tname:             \"nok, unsafe DELETE + cross-site is blocked\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodDelete,\n\t\t\twhenSecFetchSite: \"cross-site\",\n\t\t\texpectAllow:      false,\n\t\t\texpectErr:        `code=403, message=cross-site request blocked by CSRF`,\n\t\t},\n\t\t{\n\t\t\tname:             \"nok, unsafe DELETE + same-site is blocked\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodDelete,\n\t\t\twhenSecFetchSite: \"same-site\",\n\t\t\texpectAllow:      false,\n\t\t\texpectErr:        `code=403, message=same-site request blocked by CSRF`,\n\t\t},\n\t\t{\n\t\t\tname:             \"nok, unsafe PATCH + cross-site is blocked\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodPatch,\n\t\t\twhenSecFetchSite: \"cross-site\",\n\t\t\texpectAllow:      false,\n\t\t\texpectErr:        `code=403, message=cross-site request blocked by CSRF`,\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, safe HEAD + same-origin passes\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodHead,\n\t\t\twhenSecFetchSite: \"same-origin\",\n\t\t\texpectAllow:      true,\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, safe HEAD + cross-site passes\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodHead,\n\t\t\twhenSecFetchSite: \"cross-site\",\n\t\t\texpectAllow:      true,\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, safe OPTIONS + cross-site passes\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodOptions,\n\t\t\twhenSecFetchSite: \"cross-site\",\n\t\t\texpectAllow:      true,\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, safe TRACE + cross-site passes\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodTrace,\n\t\t\twhenSecFetchSite: \"cross-site\",\n\t\t\texpectAllow:      true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok, unsafe POST + cross-site + matching trusted origin passes\",\n\t\t\tgivenConfig: CSRFConfig{\n\t\t\t\tTrustedOrigins: []string{\"https://trusted.example.com\"},\n\t\t\t},\n\t\t\twhenMethod:       http.MethodPost,\n\t\t\twhenSecFetchSite: \"cross-site\",\n\t\t\twhenOrigin:       \"https://trusted.example.com\",\n\t\t\texpectAllow:      true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok, unsafe POST + same-site + matching trusted origin passes\",\n\t\t\tgivenConfig: CSRFConfig{\n\t\t\t\tTrustedOrigins: []string{\"https://trusted.example.com\"},\n\t\t\t},\n\t\t\twhenMethod:       http.MethodPost,\n\t\t\twhenSecFetchSite: \"same-site\",\n\t\t\twhenOrigin:       \"https://trusted.example.com\",\n\t\t\texpectAllow:      true,\n\t\t},\n\t\t{\n\t\t\tname: \"nok, unsafe POST + cross-site + non-matching origin is blocked\",\n\t\t\tgivenConfig: CSRFConfig{\n\t\t\t\tTrustedOrigins: []string{\"https://trusted.example.com\"},\n\t\t\t},\n\t\t\twhenMethod:       http.MethodPost,\n\t\t\twhenSecFetchSite: \"cross-site\",\n\t\t\twhenOrigin:       \"https://evil.example.com\",\n\t\t\texpectAllow:      false,\n\t\t\texpectErr:        `code=403, message=cross-site request blocked by CSRF`,\n\t\t},\n\t\t{\n\t\t\tname: \"ok, unsafe POST + cross-site + case-insensitive trusted origin match passes\",\n\t\t\tgivenConfig: CSRFConfig{\n\t\t\t\tTrustedOrigins: []string{\"https://trusted.example.com\"},\n\t\t\t},\n\t\t\twhenMethod:       http.MethodPost,\n\t\t\twhenSecFetchSite: \"cross-site\",\n\t\t\twhenOrigin:       \"https://TRUSTED.example.com\",\n\t\t\texpectAllow:      true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok, unsafe POST + same-origin + trusted origins configured but not matched passes\",\n\t\t\tgivenConfig: CSRFConfig{\n\t\t\t\tTrustedOrigins: []string{\"https://trusted.example.com\"},\n\t\t\t},\n\t\t\twhenMethod:       http.MethodPost,\n\t\t\twhenSecFetchSite: \"same-origin\",\n\t\t\twhenOrigin:       \"https://different.example.com\",\n\t\t\texpectAllow:      true,\n\t\t},\n\t\t{\n\t\t\tname: \"nok, unsafe POST + cross-site + empty origin + trusted origins configured is blocked\",\n\t\t\tgivenConfig: CSRFConfig{\n\t\t\t\tTrustedOrigins: []string{\"https://trusted.example.com\"},\n\t\t\t},\n\t\t\twhenMethod:       http.MethodPost,\n\t\t\twhenSecFetchSite: \"cross-site\",\n\t\t\twhenOrigin:       \"\",\n\t\t\texpectAllow:      false,\n\t\t\texpectErr:        `code=403, message=cross-site request blocked by CSRF`,\n\t\t},\n\t\t{\n\t\t\tname: \"ok, unsafe POST + cross-site + multiple trusted origins, second one matches\",\n\t\t\tgivenConfig: CSRFConfig{\n\t\t\t\tTrustedOrigins: []string{\"https://first.example.com\", \"https://second.example.com\"},\n\t\t\t},\n\t\t\twhenMethod:       http.MethodPost,\n\t\t\twhenSecFetchSite: \"cross-site\",\n\t\t\twhenOrigin:       \"https://second.example.com\",\n\t\t\texpectAllow:      true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok, unsafe POST + same-site + custom func allows\",\n\t\t\tgivenConfig: CSRFConfig{\n\t\t\t\tAllowSecFetchSiteFunc: func(c *echo.Context) (bool, error) {\n\t\t\t\t\treturn true, nil\n\t\t\t\t},\n\t\t\t},\n\t\t\twhenMethod:       http.MethodPost,\n\t\t\twhenSecFetchSite: \"same-site\",\n\t\t\texpectAllow:      true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok, unsafe POST + cross-site + custom func allows\",\n\t\t\tgivenConfig: CSRFConfig{\n\t\t\t\tAllowSecFetchSiteFunc: func(c *echo.Context) (bool, error) {\n\t\t\t\t\treturn true, nil\n\t\t\t\t},\n\t\t\t},\n\t\t\twhenMethod:       http.MethodPost,\n\t\t\twhenSecFetchSite: \"cross-site\",\n\t\t\texpectAllow:      true,\n\t\t},\n\t\t{\n\t\t\tname: \"nok, unsafe POST + same-site + custom func returns custom error\",\n\t\t\tgivenConfig: CSRFConfig{\n\t\t\t\tAllowSecFetchSiteFunc: func(c *echo.Context) (bool, error) {\n\t\t\t\t\treturn false, echo.NewHTTPError(http.StatusTeapot, \"custom error from func\")\n\t\t\t\t},\n\t\t\t},\n\t\t\twhenMethod:       http.MethodPost,\n\t\t\twhenSecFetchSite: \"same-site\",\n\t\t\texpectAllow:      false,\n\t\t\texpectErr:        `code=418, message=custom error from func`,\n\t\t},\n\t\t{\n\t\t\tname: \"nok, unsafe POST + cross-site + custom func returns false with nil error\",\n\t\t\tgivenConfig: CSRFConfig{\n\t\t\t\tAllowSecFetchSiteFunc: func(c *echo.Context) (bool, error) {\n\t\t\t\t\treturn false, nil\n\t\t\t\t},\n\t\t\t},\n\t\t\twhenMethod:       http.MethodPost,\n\t\t\twhenSecFetchSite: \"cross-site\",\n\t\t\texpectAllow:      false,\n\t\t\texpectErr:        \"\", // custom func returns nil error, so no error expected\n\t\t},\n\t\t{\n\t\t\tname:             \"nok, unsafe POST + invalid Sec-Fetch-Site value treated as cross-site\",\n\t\t\tgivenConfig:      CSRFConfig{},\n\t\t\twhenMethod:       http.MethodPost,\n\t\t\twhenSecFetchSite: \"invalid-value\",\n\t\t\texpectAllow:      false,\n\t\t\texpectErr:        `code=403, message=cross-site request blocked by CSRF`,\n\t\t},\n\t\t{\n\t\t\tname: \"ok, unsafe POST + cross-site + trusted origin takes precedence over custom func\",\n\t\t\tgivenConfig: CSRFConfig{\n\t\t\t\tTrustedOrigins: []string{\"https://trusted.example.com\"},\n\t\t\t\tAllowSecFetchSiteFunc: func(c *echo.Context) (bool, error) {\n\t\t\t\t\treturn false, echo.NewHTTPError(http.StatusTeapot, \"should not be called\")\n\t\t\t\t},\n\t\t\t},\n\t\t\twhenMethod:       http.MethodPost,\n\t\t\twhenSecFetchSite: \"cross-site\",\n\t\t\twhenOrigin:       \"https://trusted.example.com\",\n\t\t\texpectAllow:      true,\n\t\t},\n\t\t{\n\t\t\tname: \"nok, unsafe POST + cross-site + trusted origin not matched, custom func blocks\",\n\t\t\tgivenConfig: CSRFConfig{\n\t\t\t\tTrustedOrigins: []string{\"https://trusted.example.com\"},\n\t\t\t\tAllowSecFetchSiteFunc: func(c *echo.Context) (bool, error) {\n\t\t\t\t\treturn false, echo.NewHTTPError(http.StatusTeapot, \"custom block\")\n\t\t\t\t},\n\t\t\t},\n\t\t\twhenMethod:       http.MethodPost,\n\t\t\twhenSecFetchSite: \"cross-site\",\n\t\t\twhenOrigin:       \"https://evil.example.com\",\n\t\t\texpectAllow:      false,\n\t\t\texpectErr:        `code=418, message=custom block`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(tc.whenMethod, \"/\", nil)\n\t\t\tif tc.whenSecFetchSite != \"\" {\n\t\t\t\treq.Header.Set(echo.HeaderSecFetchSite, tc.whenSecFetchSite)\n\t\t\t}\n\t\t\tif tc.whenOrigin != \"\" {\n\t\t\t\treq.Header.Set(echo.HeaderOrigin, tc.whenOrigin)\n\t\t\t}\n\n\t\t\tres := httptest.NewRecorder()\n\t\t\tc := echo.NewContext(req, res)\n\n\t\t\tallow, err := tc.givenConfig.checkSecFetchSiteRequest(c)\n\n\t\t\tassert.Equal(t, tc.expectAllow, allow)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "middleware/decompress.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"compress/gzip\"\n\t\"io\"\n\t\"net/http\"\n\t\"sync\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\n// DecompressConfig defines the config for Decompress middleware.\ntype DecompressConfig struct {\n\t// Skipper defines a function to skip middleware.\n\tSkipper Skipper\n\n\t// GzipDecompressPool defines an interface to provide the sync.Pool used to create/store Gzip readers\n\tGzipDecompressPool Decompressor\n\n\t// MaxDecompressedSize limits the maximum size of decompressed request body in bytes.\n\t// If the decompressed body exceeds this limit, the middleware returns HTTP 413 error.\n\t// This prevents zip bomb attacks where small compressed payloads decompress to huge sizes.\n\t// Default: 100 * MB (104,857,600 bytes)\n\t// Set to -1 to disable limits (not recommended in production).\n\tMaxDecompressedSize int64\n}\n\n// GZIPEncoding content-encoding header if set to \"gzip\", decompress body contents.\nconst GZIPEncoding string = \"gzip\"\n\n// Decompressor is used to get the sync.Pool used by the middleware to get Gzip readers\ntype Decompressor interface {\n\tgzipDecompressPool() sync.Pool\n}\n\n// DefaultGzipDecompressPool is the default implementation of Decompressor interface\ntype DefaultGzipDecompressPool struct {\n}\n\nfunc (d *DefaultGzipDecompressPool) gzipDecompressPool() sync.Pool {\n\treturn sync.Pool{New: func() any { return new(gzip.Reader) }}\n}\n\n// Decompress decompresses request body based if content encoding type is set to \"gzip\" with default config\n//\n// SECURITY: By default, this limits decompressed data to 100MB to prevent zip bomb attacks.\n// To customize the limit, use DecompressWithConfig. To disable limits (not recommended in production),\n// set MaxDecompressedSize to -1.\nfunc Decompress() echo.MiddlewareFunc {\n\treturn DecompressWithConfig(DecompressConfig{})\n}\n\n// DecompressWithConfig returns a decompress middleware with config or panics on invalid configuration.\n//\n// SECURITY: If MaxDecompressedSize is not set (zero value), it defaults to 100MB to prevent\n// DoS attacks via zip bombs. Set to -1 to explicitly disable limits if needed for your use case.\nfunc DecompressWithConfig(config DecompressConfig) echo.MiddlewareFunc {\n\treturn toMiddlewareOrPanic(config)\n}\n\n// ToMiddleware converts DecompressConfig to middleware or returns an error for invalid configuration\nfunc (config DecompressConfig) ToMiddleware() (echo.MiddlewareFunc, error) {\n\tif config.Skipper == nil {\n\t\tconfig.Skipper = DefaultSkipper\n\t}\n\tif config.GzipDecompressPool == nil {\n\t\tconfig.GzipDecompressPool = &DefaultGzipDecompressPool{}\n\t}\n\t// Apply secure default for decompression limit\n\tif config.MaxDecompressedSize == 0 {\n\t\tconfig.MaxDecompressedSize = 100 * MB\n\t}\n\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\tpool := config.GzipDecompressPool.gzipDecompressPool()\n\n\t\treturn func(c *echo.Context) error {\n\t\t\tif config.Skipper(c) {\n\t\t\t\treturn next(c)\n\t\t\t}\n\n\t\t\tif c.Request().Header.Get(echo.HeaderContentEncoding) != GZIPEncoding {\n\t\t\t\treturn next(c)\n\t\t\t}\n\n\t\t\ti := pool.Get()\n\t\t\tgr, ok := i.(*gzip.Reader)\n\t\t\tif !ok || gr == nil {\n\t\t\t\tif err, isErr := i.(error); isErr {\n\t\t\t\t\treturn echo.NewHTTPError(http.StatusInternalServerError, err.Error())\n\t\t\t\t}\n\t\t\t\treturn echo.NewHTTPError(http.StatusInternalServerError, \"unexpected type from gzip decompression pool\")\n\t\t\t}\n\t\t\tdefer pool.Put(gr)\n\n\t\t\tb := c.Request().Body\n\t\t\tdefer b.Close()\n\n\t\t\tif err := gr.Reset(b); err != nil {\n\t\t\t\tif err == io.EOF { //ignore if body is empty\n\t\t\t\t\treturn next(c)\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\t// only Close gzip reader if it was set to a proper gzip source otherwise it will panic on close.\n\t\t\tdefer gr.Close()\n\n\t\t\t// Apply decompression size limit to prevent zip bombs\n\t\t\tif config.MaxDecompressedSize > 0 {\n\t\t\t\tc.Request().Body = &limitedGzipReader{\n\t\t\t\t\tReader:    gr,\n\t\t\t\t\tremaining: config.MaxDecompressedSize,\n\t\t\t\t\tlimit:     config.MaxDecompressedSize,\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// -1 means explicitly unlimited (not recommended)\n\t\t\t\tc.Request().Body = gr\n\t\t\t}\n\n\t\t\treturn next(c)\n\t\t}\n\t}, nil\n}\n\n// limitedGzipReader wraps a gzip reader with size limiting to prevent zip bombs\ntype limitedGzipReader struct {\n\t*gzip.Reader\n\tremaining int64\n\tlimit     int64\n}\n\nfunc (r *limitedGzipReader) Read(p []byte) (n int, err error) {\n\tif r.remaining <= 0 {\n\t\t// Limit exceeded - return 413 error\n\t\treturn 0, echo.ErrStatusRequestEntityTooLarge\n\t}\n\n\t// Limit the read to remaining bytes\n\tif int64(len(p)) > r.remaining {\n\t\tp = p[:r.remaining]\n\t}\n\n\tn, err = r.Reader.Read(p)\n\tr.remaining -= int64(n)\n\n\treturn n, err\n}\n\nfunc (r *limitedGzipReader) Close() error {\n\treturn r.Reader.Close()\n}\n"
  },
  {
    "path": "middleware/decompress_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestDecompress(t *testing.T) {\n\te := echo.New()\n\n\th := Decompress()(func(c *echo.Context) error {\n\t\tc.Response().Write([]byte(\"test\")) // For Content-Type sniffing\n\t\treturn nil\n\t})\n\n\t// Decompress request body\n\tbody := `{\"name\": \"echo\"}`\n\tgz, _ := gzipString(body)\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(string(gz)))\n\treq.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\terr := h(c)\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))\n\tb, err := io.ReadAll(req.Body)\n\tassert.NoError(t, err)\n\tassert.Equal(t, body, string(b))\n}\n\nfunc TestDecompress_skippedIfNoHeader(t *testing.T) {\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(\"test\"))\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\t// Skip if no Content-Encoding header\n\th := Decompress()(func(c *echo.Context) error {\n\t\tc.Response().Write([]byte(\"test\")) // For Content-Type sniffing\n\t\treturn nil\n\t})\n\n\terr := h(c)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"test\", rec.Body.String())\n\n}\n\nfunc TestDecompressWithConfig_DefaultConfig_noDecode(t *testing.T) {\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(\"test\"))\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\th, err := DecompressConfig{}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\terr = h(func(c *echo.Context) error {\n\t\tc.Response().Write([]byte(\"test\")) // For Content-Type sniffing\n\t\treturn nil\n\t})(c)\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, \"test\", rec.Body.String())\n\n}\n\nfunc TestDecompressWithConfig_DefaultConfig(t *testing.T) {\n\te := echo.New()\n\n\th := Decompress()(func(c *echo.Context) error {\n\t\tc.Response().Write([]byte(\"test\")) // For Content-Type sniffing\n\t\treturn nil\n\t})\n\n\t// Decompress\n\tbody := `{\"name\": \"echo\"}`\n\tgz, _ := gzipString(body)\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(string(gz)))\n\treq.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\terr := h(c)\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))\n\tb, err := io.ReadAll(req.Body)\n\tassert.NoError(t, err)\n\tassert.Equal(t, body, string(b))\n}\n\nfunc TestCompressRequestWithoutDecompressMiddleware(t *testing.T) {\n\te := echo.New()\n\tbody := `{\"name\":\"echo\"}`\n\tgz, _ := gzipString(body)\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(string(gz)))\n\treq.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)\n\trec := httptest.NewRecorder()\n\te.NewContext(req, rec)\n\n\te.ServeHTTP(rec, req)\n\n\tassert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))\n\tb, err := io.ReadAll(req.Body)\n\tassert.NoError(t, err)\n\tassert.NotEqual(t, b, body)\n\tassert.Equal(t, b, gz)\n}\n\nfunc TestDecompressNoContent(t *testing.T) {\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\th := Decompress()(func(c *echo.Context) error {\n\t\treturn c.NoContent(http.StatusNoContent)\n\t})\n\n\terr := h(c)\n\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))\n\t\tassert.Empty(t, rec.Header().Get(echo.HeaderContentType))\n\t\tassert.Equal(t, 0, len(rec.Body.Bytes()))\n\t}\n}\n\nfunc TestDecompressErrorReturned(t *testing.T) {\n\te := echo.New()\n\te.Use(Decompress())\n\te.GET(\"/\", func(c *echo.Context) error {\n\t\treturn echo.ErrNotFound\n\t})\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)\n\trec := httptest.NewRecorder()\n\n\te.ServeHTTP(rec, req)\n\n\tassert.Equal(t, http.StatusNotFound, rec.Code)\n\tassert.Empty(t, rec.Header().Get(echo.HeaderContentEncoding))\n}\n\nfunc TestDecompressSkipper(t *testing.T) {\n\te := echo.New()\n\te.Use(DecompressWithConfig(DecompressConfig{\n\t\tSkipper: func(c *echo.Context) bool {\n\t\t\treturn c.Request().URL.Path == \"/skip\"\n\t\t},\n\t}))\n\tbody := `{\"name\": \"echo\"}`\n\treq := httptest.NewRequest(http.MethodPost, \"/skip\", strings.NewReader(body))\n\treq.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\te.ServeHTTP(rec, req)\n\n\tassert.Equal(t, rec.Header().Get(echo.HeaderContentType), echo.MIMEApplicationJSON)\n\treqBody, err := io.ReadAll(c.Request().Body)\n\tassert.NoError(t, err)\n\tassert.Equal(t, body, string(reqBody))\n}\n\ntype TestDecompressPoolWithError struct {\n}\n\nfunc (d *TestDecompressPoolWithError) gzipDecompressPool() sync.Pool {\n\treturn sync.Pool{\n\t\tNew: func() any {\n\t\t\treturn errors.New(\"pool error\")\n\t\t},\n\t}\n}\n\nfunc TestDecompressPoolError(t *testing.T) {\n\te := echo.New()\n\te.Use(DecompressWithConfig(DecompressConfig{\n\t\tSkipper:            DefaultSkipper,\n\t\tGzipDecompressPool: &TestDecompressPoolWithError{},\n\t}))\n\tbody := `{\"name\": \"echo\"}`\n\treq := httptest.NewRequest(http.MethodPost, \"/echo\", strings.NewReader(body))\n\treq.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\te.ServeHTTP(rec, req)\n\n\tassert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))\n\treqBody, err := io.ReadAll(c.Request().Body)\n\tassert.NoError(t, err)\n\tassert.Equal(t, body, string(reqBody))\n\tassert.Equal(t, rec.Code, http.StatusInternalServerError)\n}\n\nfunc BenchmarkDecompress(b *testing.B) {\n\te := echo.New()\n\tbody := `{\"name\": \"echo\"}`\n\tgz, _ := gzipString(body)\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(string(gz)))\n\treq.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)\n\n\th := Decompress()(func(c *echo.Context) error {\n\t\tc.Response().Write([]byte(body)) // For Content-Type sniffing\n\t\treturn nil\n\t})\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tfor i := 0; i < b.N; i++ {\n\t\t// Decompress\n\t\trec := httptest.NewRecorder()\n\t\tc := e.NewContext(req, rec)\n\t\th(c)\n\t}\n}\n\nfunc gzipString(body string) ([]byte, error) {\n\tvar buf bytes.Buffer\n\tgz := gzip.NewWriter(&buf)\n\n\t_, err := gz.Write([]byte(body))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := gz.Close(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn buf.Bytes(), nil\n}\n\nfunc TestDecompress_WithinLimit(t *testing.T) {\n\te := echo.New()\n\tbody := strings.Repeat(\"test data \", 100) // Small payload ~1KB\n\tgz, _ := gzipString(body)\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", bytes.NewReader(gz))\n\treq.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\th, err := DecompressConfig{MaxDecompressedSize: 100 * MB}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\terr = h(func(c *echo.Context) error {\n\t\tb, _ := io.ReadAll(c.Request().Body)\n\t\treturn c.String(http.StatusOK, string(b))\n\t})(c)\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, body, rec.Body.String())\n}\n\nfunc TestDecompress_ExceedsLimit(t *testing.T) {\n\te := echo.New()\n\t// Create 2KB of data but limit to 1KB\n\tlargeBody := strings.Repeat(\"A\", 2*1024)\n\tgz, _ := gzipString(largeBody)\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", bytes.NewReader(gz))\n\treq.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\th, err := DecompressConfig{MaxDecompressedSize: 1024}.ToMiddleware() // 1KB limit\n\tassert.NoError(t, err)\n\n\terr = h(func(c *echo.Context) error {\n\t\t_, readErr := io.ReadAll(c.Request().Body)\n\t\treturn readErr\n\t})(c)\n\n\t// Should return 413 error\n\tassert.Error(t, err)\n\the, ok := err.(echo.HTTPStatusCoder)\n\tassert.True(t, ok)\n\tassert.Equal(t, http.StatusRequestEntityTooLarge, he.StatusCode())\n}\n\nfunc TestDecompress_AtExactLimit(t *testing.T) {\n\te := echo.New()\n\texactBody := strings.Repeat(\"B\", 1024) // Exactly 1KB\n\tgz, _ := gzipString(exactBody)\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", bytes.NewReader(gz))\n\treq.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\th, err := DecompressConfig{MaxDecompressedSize: 1024}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\terr = h(func(c *echo.Context) error {\n\t\tb, _ := io.ReadAll(c.Request().Body)\n\t\treturn c.String(http.StatusOK, string(b))\n\t})(c)\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, exactBody, rec.Body.String())\n}\n\nfunc TestDecompress_ZipBomb(t *testing.T) {\n\te := echo.New()\n\t// Create highly compressed data that expands to 2MB\n\t// but limit is 1MB\n\tlargeBody := bytes.Repeat([]byte(\"A\"), 2*1024*1024) // 2MB\n\tvar buf bytes.Buffer\n\tgzWriter := gzip.NewWriter(&buf)\n\tgzWriter.Write(largeBody)\n\tgzWriter.Close()\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", &buf)\n\treq.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\th, err := DecompressConfig{MaxDecompressedSize: 1 * MB}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\terr = h(func(c *echo.Context) error {\n\t\t_, readErr := io.ReadAll(c.Request().Body)\n\t\treturn readErr\n\t})(c)\n\n\t// Should return 413 error\n\tassert.Error(t, err)\n\the, ok := err.(echo.HTTPStatusCoder)\n\tassert.True(t, ok)\n\tassert.Equal(t, http.StatusRequestEntityTooLarge, he.StatusCode())\n}\n\nfunc TestDecompress_UnlimitedExplicit(t *testing.T) {\n\te := echo.New()\n\tlargeBody := strings.Repeat(\"X\", 10*1024) // 10KB\n\tgz, _ := gzipString(largeBody)\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", bytes.NewReader(gz))\n\treq.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\th, err := DecompressConfig{MaxDecompressedSize: -1}.ToMiddleware() // Unlimited\n\tassert.NoError(t, err)\n\n\terr = h(func(c *echo.Context) error {\n\t\tb, _ := io.ReadAll(c.Request().Body)\n\t\treturn c.String(http.StatusOK, string(b))\n\t})(c)\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, largeBody, rec.Body.String())\n}\n\nfunc TestDecompress_DefaultLimit(t *testing.T) {\n\te := echo.New()\n\tsmallBody := \"test\"\n\tgz, _ := gzipString(smallBody)\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", bytes.NewReader(gz))\n\treq.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\t// Use zero value which should apply 100MB default\n\th, err := DecompressConfig{}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\terr = h(func(c *echo.Context) error {\n\t\tb, _ := io.ReadAll(c.Request().Body)\n\t\treturn c.String(http.StatusOK, string(b))\n\t})(c)\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, smallBody, rec.Body.String())\n}\n\nfunc TestDecompress_SmallCustomLimit(t *testing.T) {\n\te := echo.New()\n\tbody := strings.Repeat(\"D\", 512) // 512 bytes\n\tgz, _ := gzipString(body)\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", bytes.NewReader(gz))\n\treq.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\th, err := DecompressConfig{MaxDecompressedSize: 1024}.ToMiddleware() // 1KB limit\n\tassert.NoError(t, err)\n\n\terr = h(func(c *echo.Context) error {\n\t\tb, _ := io.ReadAll(c.Request().Body)\n\t\treturn c.String(http.StatusOK, string(b))\n\t})(c)\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, body, rec.Body.String())\n}\n\nfunc TestDecompress_MultipleReads(t *testing.T) {\n\te := echo.New()\n\t// Test that limit is enforced across multiple Read() calls\n\tlargeBody := strings.Repeat(\"M\", 2*1024) // 2KB\n\tgz, _ := gzipString(largeBody)\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", bytes.NewReader(gz))\n\treq.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\th, err := DecompressConfig{MaxDecompressedSize: 1024}.ToMiddleware() // 1KB limit\n\tassert.NoError(t, err)\n\n\terr = h(func(c *echo.Context) error {\n\t\t// Read in small chunks\n\t\tbuf := make([]byte, 256)\n\t\ttotal := 0\n\t\tfor {\n\t\t\tn, readErr := c.Request().Body.Read(buf)\n\t\t\ttotal += n\n\t\t\tif readErr != nil {\n\t\t\t\tif readErr == io.EOF {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\treturn readErr\n\t\t\t}\n\t\t}\n\t})(c)\n\n\t// Should return 413 error from cumulative reads\n\tassert.Error(t, err)\n\the, ok := err.(echo.HTTPStatusCoder)\n\tassert.True(t, ok)\n\tassert.Equal(t, http.StatusRequestEntityTooLarge, he.StatusCode())\n}\n\nfunc TestDecompress_LargePayloadDosPrevention(t *testing.T) {\n\te := echo.New()\n\t// Simulate a DoS attack with highly compressed large payload\n\tlargeSize := 10 * 1024 * 1024 // 10MB decompressed\n\tlargeBody := bytes.Repeat([]byte(\"Z\"), largeSize)\n\tvar buf bytes.Buffer\n\tgzWriter := gzip.NewWriter(&buf)\n\tgzWriter.Write(largeBody)\n\tgzWriter.Close()\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", &buf)\n\treq.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\th, err := DecompressConfig{MaxDecompressedSize: 1 * MB}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\terr = h(func(c *echo.Context) error {\n\t\t_, readErr := io.ReadAll(c.Request().Body)\n\t\treturn readErr\n\t})(c)\n\n\t// Should prevent DoS by returning 413\n\tassert.Error(t, err)\n\the, ok := err.(echo.HTTPStatusCoder)\n\tassert.True(t, ok)\n\tassert.Equal(t, http.StatusRequestEntityTooLarge, he.StatusCode())\n}\n\nfunc BenchmarkDecompress_WithLimit(b *testing.B) {\n\te := echo.New()\n\tbody := strings.Repeat(\"benchmark data \", 1000) // ~15KB\n\tgz, _ := gzipString(body)\n\n\th, _ := DecompressConfig{MaxDecompressedSize: 100 * MB}.ToMiddleware()\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tfor i := 0; i < b.N; i++ {\n\t\treq := httptest.NewRequest(http.MethodPost, \"/\", bytes.NewReader(gz))\n\t\treq.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)\n\t\trec := httptest.NewRecorder()\n\t\tc := e.NewContext(req, rec)\n\n\t\th(func(c *echo.Context) error {\n\t\t\tio.ReadAll(c.Request().Body)\n\t\t\treturn nil\n\t\t})(c)\n\t}\n}\n"
  },
  {
    "path": "middleware/extractor.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"fmt\"\n\t\"net/textproto\"\n\t\"strings\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\nconst (\n\t// extractorLimit is arbitrary number to limit values extractor can return. this limits possible resource exhaustion\n\t// attack vector\n\textractorLimit = 20\n)\n\n// ExtractorSource is type to indicate source for extracted value\ntype ExtractorSource string\n\nconst (\n\t// ExtractorSourceHeader means value was extracted from request header\n\tExtractorSourceHeader ExtractorSource = \"header\"\n\t// ExtractorSourceQuery means value was extracted from request query parameters\n\tExtractorSourceQuery ExtractorSource = \"query\"\n\t// ExtractorSourcePathParam means value was extracted from route path parameters\n\tExtractorSourcePathParam ExtractorSource = \"param\"\n\t// ExtractorSourceCookie means value was extracted from request cookies\n\tExtractorSourceCookie ExtractorSource = \"cookie\"\n\t// ExtractorSourceForm means value was extracted from request form values\n\tExtractorSourceForm ExtractorSource = \"form\"\n)\n\n// ValueExtractorError is error type when middleware extractor is unable to extract value from lookups\ntype ValueExtractorError struct {\n\tmessage string\n}\n\n// Error returns errors text\nfunc (e *ValueExtractorError) Error() string {\n\treturn e.message\n}\n\nvar errHeaderExtractorValueMissing = &ValueExtractorError{message: \"missing value in request header\"}\nvar errHeaderExtractorValueInvalid = &ValueExtractorError{message: \"invalid value in request header\"}\nvar errQueryExtractorValueMissing = &ValueExtractorError{message: \"missing value in the query string\"}\nvar errParamExtractorValueMissing = &ValueExtractorError{message: \"missing value in path params\"}\nvar errCookieExtractorValueMissing = &ValueExtractorError{message: \"missing value in cookies\"}\nvar errFormExtractorValueMissing = &ValueExtractorError{message: \"missing value in the form\"}\n\n// ValuesExtractor defines a function for extracting values (keys/tokens) from the given context.\ntype ValuesExtractor func(c *echo.Context) ([]string, ExtractorSource, error)\n\n// CreateExtractors creates ValuesExtractors from given lookups.\n// lookups is a string in the form of \"<source>:<name>\" or \"<source>:<name>,<source>:<name>\" that is used\n// to extract key from the request.\n// Possible values:\n//   - \"header:<name>\" or \"header:<name>:<cut-prefix>\"\n//     `<cut-prefix>` is argument value to cut/trim prefix of the extracted value. This is useful if header\n//     value has static prefix like `Authorization: <auth-scheme> <authorisation-parameters>` where part that we\n//     want to cut is `<auth-scheme> ` note the space at the end.\n//     In case of basic authentication `Authorization: Basic <credentials>` prefix we want to remove is `Basic `.\n//   - \"query:<name>\"\n//   - \"param:<name>\"\n//   - \"form:<name>\"\n//   - \"cookie:<name>\"\n//\n// Multiple sources example:\n// - \"header:Authorization,header:X-Api-Key\"\n//\n// limit sets the maximum amount how many lookups can be returned.\nfunc CreateExtractors(lookups string, limit uint) ([]ValuesExtractor, error) {\n\treturn createExtractors(lookups, limit)\n}\n\nfunc createExtractors(lookups string, limit uint) ([]ValuesExtractor, error) {\n\tif lookups == \"\" {\n\t\treturn nil, nil\n\t}\n\tif limit == 0 {\n\t\tlimit = 1\n\t} else if limit > extractorLimit {\n\t\tlimit = extractorLimit\n\t}\n\n\tsources := strings.Split(lookups, \",\")\n\tvar extractors = make([]ValuesExtractor, 0)\n\tfor _, source := range sources {\n\t\tparts := strings.Split(source, \":\")\n\t\tif len(parts) < 2 {\n\t\t\treturn nil, fmt.Errorf(\"extractor source for lookup could not be split into needed parts: %v\", source)\n\t\t}\n\n\t\tswitch parts[0] {\n\t\tcase \"query\":\n\t\t\textractors = append(extractors, valuesFromQuery(parts[1], limit))\n\t\tcase \"param\":\n\t\t\textractors = append(extractors, valuesFromParam(parts[1], limit))\n\t\tcase \"cookie\":\n\t\t\textractors = append(extractors, valuesFromCookie(parts[1], limit))\n\t\tcase \"form\":\n\t\t\textractors = append(extractors, valuesFromForm(parts[1], limit))\n\t\tcase \"header\":\n\t\t\tprefix := \"\"\n\t\t\tif len(parts) > 2 {\n\t\t\t\tprefix = parts[2]\n\t\t\t}\n\t\t\textractors = append(extractors, valuesFromHeader(parts[1], prefix, limit))\n\t\t}\n\t}\n\treturn extractors, nil\n}\n\n// valuesFromHeader returns a functions that extracts values from the request header.\n// valuePrefix is parameter to remove first part (prefix) of the extracted value. This is useful if header value has static\n// prefix like `Authorization: <auth-scheme> <authorisation-parameters>` where part that we want to remove is `<auth-scheme> `\n// note the space at the end. In case of basic authentication `Authorization: Basic <credentials>` prefix we want to remove\n// is `Basic `. In case of JWT tokens `Authorization: Bearer <token>` prefix is `Bearer `.\n// If prefix is left empty the whole value is returned.\nfunc valuesFromHeader(header string, valuePrefix string, limit uint) ValuesExtractor {\n\tprefixLen := len(valuePrefix)\n\t// standard library parses http.Request header keys in canonical form but we may provide something else so fix this\n\theader = textproto.CanonicalMIMEHeaderKey(header)\n\tif limit == 0 {\n\t\tlimit = 1\n\t}\n\treturn func(c *echo.Context) ([]string, ExtractorSource, error) {\n\t\tvalues := c.Request().Header.Values(header)\n\t\tif len(values) == 0 {\n\t\t\treturn nil, ExtractorSourceHeader, errHeaderExtractorValueMissing\n\t\t}\n\n\t\ti := uint(0)\n\t\tresult := make([]string, 0)\n\t\tfor _, value := range values {\n\t\t\tif prefixLen == 0 {\n\t\t\t\tresult = append(result, value)\n\t\t\t\ti++\n\t\t\t\tif i >= limit {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t} else if len(value) > prefixLen && strings.EqualFold(value[:prefixLen], valuePrefix) {\n\t\t\t\tresult = append(result, value[prefixLen:])\n\t\t\t\ti++\n\t\t\t\tif i >= limit {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(result) == 0 {\n\t\t\tif prefixLen > 0 {\n\t\t\t\treturn nil, ExtractorSourceHeader, errHeaderExtractorValueInvalid\n\t\t\t}\n\t\t\treturn nil, ExtractorSourceHeader, errHeaderExtractorValueMissing\n\t\t}\n\t\treturn result, ExtractorSourceHeader, nil\n\t}\n}\n\n// valuesFromQuery returns a function that extracts values from the query string.\nfunc valuesFromQuery(param string, limit uint) ValuesExtractor {\n\tif limit == 0 {\n\t\tlimit = 1\n\t}\n\treturn func(c *echo.Context) ([]string, ExtractorSource, error) {\n\t\tresult := c.QueryParams()[param]\n\t\tif len(result) == 0 {\n\t\t\treturn nil, ExtractorSourceQuery, errQueryExtractorValueMissing\n\t\t} else if len(result) > int(limit)-1 {\n\t\t\tresult = result[:limit]\n\t\t}\n\t\treturn result, ExtractorSourceQuery, nil\n\t}\n}\n\n// valuesFromParam returns a function that extracts values from the url param string.\nfunc valuesFromParam(param string, limit uint) ValuesExtractor {\n\tif limit == 0 {\n\t\tlimit = 1\n\t}\n\treturn func(c *echo.Context) ([]string, ExtractorSource, error) {\n\t\tresult := make([]string, 0)\n\t\ti := uint(0)\n\t\tfor _, p := range c.PathValues() {\n\t\t\tif param != p.Name {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tresult = append(result, p.Value)\n\t\t\ti++\n\t\t\tif i >= limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif len(result) == 0 {\n\t\t\treturn nil, ExtractorSourcePathParam, errParamExtractorValueMissing\n\t\t}\n\t\treturn result, ExtractorSourcePathParam, nil\n\t}\n}\n\n// valuesFromCookie returns a function that extracts values from the named cookie.\nfunc valuesFromCookie(name string, limit uint) ValuesExtractor {\n\tif limit == 0 {\n\t\tlimit = 1\n\t}\n\treturn func(c *echo.Context) ([]string, ExtractorSource, error) {\n\t\tcookies := c.Cookies()\n\t\tif len(cookies) == 0 {\n\t\t\treturn nil, ExtractorSourceCookie, errCookieExtractorValueMissing\n\t\t}\n\n\t\ti := uint(0)\n\t\tresult := make([]string, 0)\n\t\tfor _, cookie := range cookies {\n\t\t\tif name != cookie.Name {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tresult = append(result, cookie.Value)\n\t\t\ti++\n\t\t\tif i >= limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif len(result) == 0 {\n\t\t\treturn nil, ExtractorSourceCookie, errCookieExtractorValueMissing\n\t\t}\n\t\treturn result, ExtractorSourceCookie, nil\n\t}\n}\n\n// valuesFromForm returns a function that extracts values from the form field.\nfunc valuesFromForm(name string, limit uint) ValuesExtractor {\n\tif limit == 0 {\n\t\tlimit = 1\n\t}\n\treturn func(c *echo.Context) ([]string, ExtractorSource, error) {\n\t\tif c.Request().Form == nil {\n\t\t\t_, _ = c.MultipartForm() // we want to trigger c.request.ParseMultipartForm(c.formParseMaxMemory)\n\t\t}\n\t\tvalues := c.Request().Form[name]\n\t\tif len(values) == 0 {\n\t\t\treturn nil, ExtractorSourceForm, errFormExtractorValueMissing\n\t\t}\n\t\tif len(values) > int(limit)-1 {\n\t\t\tvalues = values[:limit]\n\t\t}\n\t\tresult := append([]string{}, values...)\n\t\treturn result, ExtractorSourceForm, nil\n\t}\n}\n"
  },
  {
    "path": "middleware/extractor_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCreateExtractors(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname              string\n\t\tgivenRequest      func() *http.Request\n\t\tgivenPathValues   echo.PathValues\n\t\twhenLookups       string\n\t\twhenLimit         uint\n\t\texpectValues      []string\n\t\texpectSource      ExtractorSource\n\t\texpectCreateError string\n\t\texpectError       string\n\t}{\n\t\t{\n\t\t\tname: \"ok, header\",\n\t\t\tgivenRequest: func() *http.Request {\n\t\t\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\t\t\treq.Header.Set(echo.HeaderAuthorization, \"Bearer token\")\n\t\t\t\treturn req\n\t\t\t},\n\t\t\twhenLookups:  \"header:Authorization:Bearer \",\n\t\t\texpectValues: []string{\"token\"},\n\t\t\texpectSource: ExtractorSourceHeader,\n\t\t},\n\t\t{\n\t\t\tname: \"ok, form\",\n\t\t\tgivenRequest: func() *http.Request {\n\t\t\t\tf := make(url.Values)\n\t\t\t\tf.Set(\"name\", \"Jon Snow\")\n\n\t\t\t\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(f.Encode()))\n\t\t\t\treq.Header.Add(echo.HeaderContentType, echo.MIMEApplicationForm)\n\t\t\t\treturn req\n\t\t\t},\n\t\t\twhenLookups:  \"form:name\",\n\t\t\texpectValues: []string{\"Jon Snow\"},\n\t\t\texpectSource: ExtractorSourceForm,\n\t\t},\n\t\t{\n\t\t\tname: \"ok, cookie\",\n\t\t\tgivenRequest: func() *http.Request {\n\t\t\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\t\t\treq.Header.Set(echo.HeaderCookie, \"_csrf=token\")\n\t\t\t\treturn req\n\t\t\t},\n\t\t\twhenLookups:  \"cookie:_csrf\",\n\t\t\texpectValues: []string{\"token\"},\n\t\t\texpectSource: ExtractorSourceCookie,\n\t\t},\n\t\t{\n\t\t\tname: \"ok, param\",\n\t\t\tgivenPathValues: echo.PathValues{\n\t\t\t\t{Name: \"id\", Value: \"123\"},\n\t\t\t},\n\t\t\twhenLookups:  \"param:id\",\n\t\t\texpectValues: []string{\"123\"},\n\t\t\texpectSource: ExtractorSourcePathParam,\n\t\t},\n\t\t{\n\t\t\tname: \"ok, query\",\n\t\t\tgivenRequest: func() *http.Request {\n\t\t\t\treq := httptest.NewRequest(http.MethodGet, \"/?id=999\", nil)\n\t\t\t\treturn req\n\t\t\t},\n\t\t\twhenLookups:  \"query:id\",\n\t\t\texpectValues: []string{\"999\"},\n\t\t\texpectSource: ExtractorSourceQuery,\n\t\t},\n\t\t{\n\t\t\tname:              \"nok, invalid lookup\",\n\t\t\twhenLookups:       \"query\",\n\t\t\texpectCreateError: \"extractor source for lookup could not be split into needed parts: query\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := echo.New()\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\t\tif tc.givenRequest != nil {\n\t\t\t\treq = tc.givenRequest()\n\t\t\t}\n\t\t\trec := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, rec)\n\t\t\tif tc.givenPathValues != nil {\n\t\t\t\tc.SetPathValues(tc.givenPathValues)\n\t\t\t}\n\n\t\t\textractors, err := CreateExtractors(tc.whenLookups, tc.whenLimit)\n\t\t\tif tc.expectCreateError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectCreateError)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.NoError(t, err)\n\n\t\t\tfor _, e := range extractors {\n\t\t\t\tvalues, source, eErr := e(c)\n\t\t\t\tassert.Equal(t, tc.expectValues, values)\n\t\t\t\tassert.Equal(t, tc.expectSource, source)\n\t\t\t\tif tc.expectError != \"\" {\n\t\t\t\t\tassert.EqualError(t, eErr, tc.expectError)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tassert.NoError(t, eErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValuesFromHeader(t *testing.T) {\n\texampleRequest := func(req *http.Request) {\n\t\treq.Header.Set(echo.HeaderAuthorization, \"basic dXNlcjpwYXNzd29yZA==\")\n\t}\n\n\tvar testCases = []struct {\n\t\tname            string\n\t\tgivenRequest    func(req *http.Request)\n\t\twhenName        string\n\t\twhenValuePrefix string\n\t\twhenLimit       uint\n\t\texpectValues    []string\n\t\texpectError     string\n\t}{\n\t\t{\n\t\t\tname:            \"ok, single value\",\n\t\t\tgivenRequest:    exampleRequest,\n\t\t\twhenName:        echo.HeaderAuthorization,\n\t\t\twhenValuePrefix: \"basic \",\n\t\t\texpectValues:    []string{\"dXNlcjpwYXNzd29yZA==\"},\n\t\t},\n\t\t{\n\t\t\tname:            \"ok, single value, case insensitive\",\n\t\t\tgivenRequest:    exampleRequest,\n\t\t\twhenName:        echo.HeaderAuthorization,\n\t\t\twhenValuePrefix: \"Basic \",\n\t\t\texpectValues:    []string{\"dXNlcjpwYXNzd29yZA==\"},\n\t\t},\n\t\t{\n\t\t\tname: \"ok, multiple value\",\n\t\t\tgivenRequest: func(req *http.Request) {\n\t\t\t\treq.Header.Set(echo.HeaderAuthorization, \"basic dXNlcjpwYXNzd29yZA==\")\n\t\t\t\treq.Header.Add(echo.HeaderAuthorization, \"basic dGVzdDp0ZXN0\")\n\t\t\t},\n\t\t\twhenName:        echo.HeaderAuthorization,\n\t\t\twhenValuePrefix: \"basic \",\n\t\t\twhenLimit:       2,\n\t\t\texpectValues:    []string{\"dXNlcjpwYXNzd29yZA==\", \"dGVzdDp0ZXN0\"},\n\t\t},\n\t\t{\n\t\t\tname:            \"ok, empty prefix\",\n\t\t\tgivenRequest:    exampleRequest,\n\t\t\twhenName:        echo.HeaderAuthorization,\n\t\t\twhenValuePrefix: \"\",\n\t\t\texpectValues:    []string{\"basic dXNlcjpwYXNzd29yZA==\"},\n\t\t},\n\t\t{\n\t\t\tname: \"nok, no matching due different prefix\",\n\t\t\tgivenRequest: func(req *http.Request) {\n\t\t\t\treq.Header.Set(echo.HeaderAuthorization, \"basic dXNlcjpwYXNzd29yZA==\")\n\t\t\t\treq.Header.Add(echo.HeaderAuthorization, \"basic dGVzdDp0ZXN0\")\n\t\t\t},\n\t\t\twhenName:        echo.HeaderAuthorization,\n\t\t\twhenValuePrefix: \"Bearer \",\n\t\t\texpectError:     errHeaderExtractorValueInvalid.Error(),\n\t\t},\n\t\t{\n\t\t\tname: \"nok, no matching due different prefix\",\n\t\t\tgivenRequest: func(req *http.Request) {\n\t\t\t\treq.Header.Set(echo.HeaderAuthorization, \"basic dXNlcjpwYXNzd29yZA==\")\n\t\t\t\treq.Header.Add(echo.HeaderAuthorization, \"basic dGVzdDp0ZXN0\")\n\t\t\t},\n\t\t\twhenName:        echo.HeaderWWWAuthenticate,\n\t\t\twhenValuePrefix: \"\",\n\t\t\texpectError:     errHeaderExtractorValueMissing.Error(),\n\t\t},\n\t\t{\n\t\t\tname:            \"nok, no headers\",\n\t\t\tgivenRequest:    nil,\n\t\t\twhenName:        echo.HeaderAuthorization,\n\t\t\twhenValuePrefix: \"basic \",\n\t\t\texpectError:     errHeaderExtractorValueMissing.Error(),\n\t\t},\n\t\t{\n\t\t\tname: \"ok, prefix, cut values over extractorLimit\",\n\t\t\tgivenRequest: func(req *http.Request) {\n\t\t\t\tfor i := 1; i <= 25; i++ {\n\t\t\t\t\treq.Header.Add(echo.HeaderAuthorization, fmt.Sprintf(\"basic %v\", i))\n\t\t\t\t}\n\t\t\t},\n\t\t\twhenName:        echo.HeaderAuthorization,\n\t\t\twhenValuePrefix: \"basic \",\n\t\t\twhenLimit:       extractorLimit,\n\t\t\texpectValues: []string{\n\t\t\t\t\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\", \"10\",\n\t\t\t\t\"11\", \"12\", \"13\", \"14\", \"15\", \"16\", \"17\", \"18\", \"19\", \"20\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok, cut values over extractorLimit\",\n\t\t\tgivenRequest: func(req *http.Request) {\n\t\t\t\tfor i := 1; i <= 25; i++ {\n\t\t\t\t\treq.Header.Add(echo.HeaderAuthorization, fmt.Sprintf(\"%v\", i))\n\t\t\t\t}\n\t\t\t},\n\t\t\twhenName:        echo.HeaderAuthorization,\n\t\t\twhenValuePrefix: \"\",\n\t\t\twhenLimit:       extractorLimit,\n\t\t\texpectValues: []string{\n\t\t\t\t\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\", \"10\",\n\t\t\t\t\"11\", \"12\", \"13\", \"14\", \"15\", \"16\", \"17\", \"18\", \"19\", \"20\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := echo.New()\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\t\tif tc.givenRequest != nil {\n\t\t\t\ttc.givenRequest(req)\n\t\t\t}\n\t\t\trec := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\textractor := valuesFromHeader(tc.whenName, tc.whenValuePrefix, tc.whenLimit)\n\n\t\t\tvalues, source, err := extractor(c)\n\t\t\tassert.Equal(t, tc.expectValues, values)\n\t\t\tassert.Equal(t, ExtractorSourceHeader, source)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValuesFromQuery(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname           string\n\t\tgivenQueryPart string\n\t\twhenName       string\n\t\twhenLimit      uint\n\t\texpectValues   []string\n\t\texpectError    string\n\t}{\n\t\t{\n\t\t\tname:           \"ok, single value\",\n\t\t\tgivenQueryPart: \"?id=123&name=test\",\n\t\t\twhenName:       \"id\",\n\t\t\texpectValues:   []string{\"123\"},\n\t\t},\n\t\t{\n\t\t\tname:           \"ok, multiple value\",\n\t\t\tgivenQueryPart: \"?id=123&id=456&name=test\",\n\t\t\twhenName:       \"id\",\n\t\t\twhenLimit:      2,\n\t\t\texpectValues:   []string{\"123\", \"456\"},\n\t\t},\n\t\t{\n\t\t\tname:           \"nok, missing value\",\n\t\t\tgivenQueryPart: \"?id=123&name=test\",\n\t\t\twhenName:       \"nope\",\n\t\t\texpectError:    errQueryExtractorValueMissing.Error(),\n\t\t},\n\t\t{\n\t\t\tname: \"ok, cut values over extractorLimit\",\n\t\t\tgivenQueryPart: \"?name=test\" +\n\t\t\t\t\"&id=1&id=2&id=3&id=4&id=5&id=6&id=7&id=8&id=9&id=10\" +\n\t\t\t\t\"&id=11&id=12&id=13&id=14&id=15&id=16&id=17&id=18&id=19&id=20\" +\n\t\t\t\t\"&id=21&id=22&id=23&id=24&id=25\",\n\t\t\twhenName:  \"id\",\n\t\t\twhenLimit: extractorLimit,\n\t\t\texpectValues: []string{\n\t\t\t\t\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\", \"10\",\n\t\t\t\t\"11\", \"12\", \"13\", \"14\", \"15\", \"16\", \"17\", \"18\", \"19\", \"20\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := echo.New()\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/\"+tc.givenQueryPart, nil)\n\t\t\trec := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\textractor := valuesFromQuery(tc.whenName, tc.whenLimit)\n\n\t\t\tvalues, source, err := extractor(c)\n\t\t\tassert.Equal(t, tc.expectValues, values)\n\t\t\tassert.Equal(t, ExtractorSourceQuery, source)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValuesFromParam(t *testing.T) {\n\texamplePathValues := echo.PathValues{\n\t\t{Name: \"id\", Value: \"123\"},\n\t\t{Name: \"gid\", Value: \"456\"},\n\t\t{Name: \"gid\", Value: \"789\"},\n\t}\n\texamplePathValues20 := make(echo.PathValues, 0)\n\tfor i := 1; i < 25; i++ {\n\t\texamplePathValues20 = append(examplePathValues20, echo.PathValue{Name: \"id\", Value: fmt.Sprintf(\"%v\", i)})\n\t}\n\n\tvar testCases = []struct {\n\t\tname            string\n\t\tgivenPathValues echo.PathValues\n\t\twhenName        string\n\t\twhenLimit       uint\n\t\texpectValues    []string\n\t\texpectError     string\n\t}{\n\t\t{\n\t\t\tname:            \"ok, single value\",\n\t\t\tgivenPathValues: examplePathValues,\n\t\t\twhenName:        \"id\",\n\t\t\texpectValues:    []string{\"123\"},\n\t\t},\n\t\t{\n\t\t\tname:            \"ok, multiple value\",\n\t\t\tgivenPathValues: examplePathValues,\n\t\t\twhenName:        \"gid\",\n\t\t\twhenLimit:       2,\n\t\t\texpectValues:    []string{\"456\", \"789\"},\n\t\t},\n\t\t{\n\t\t\tname:            \"nok, no values\",\n\t\t\tgivenPathValues: nil,\n\t\t\twhenName:        \"nope\",\n\t\t\texpectValues:    nil,\n\t\t\texpectError:     errParamExtractorValueMissing.Error(),\n\t\t},\n\t\t{\n\t\t\tname:            \"nok, no matching value\",\n\t\t\tgivenPathValues: examplePathValues,\n\t\t\twhenName:        \"nope\",\n\t\t\texpectValues:    nil,\n\t\t\texpectError:     errParamExtractorValueMissing.Error(),\n\t\t},\n\t\t{\n\t\t\tname:            \"ok, cut values over extractorLimit\",\n\t\t\tgivenPathValues: examplePathValues20,\n\t\t\twhenName:        \"id\",\n\t\t\twhenLimit:       extractorLimit,\n\t\t\texpectValues: []string{\n\t\t\t\t\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\", \"10\",\n\t\t\t\t\"11\", \"12\", \"13\", \"14\", \"15\", \"16\", \"17\", \"18\", \"19\", \"20\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := echo.New()\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\t\trec := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, rec)\n\t\t\tif tc.givenPathValues != nil {\n\t\t\t\tc.SetPathValues(tc.givenPathValues)\n\t\t\t}\n\n\t\t\textractor := valuesFromParam(tc.whenName, tc.whenLimit)\n\n\t\t\tvalues, source, err := extractor(c)\n\t\t\tassert.Equal(t, tc.expectValues, values)\n\t\t\tassert.Equal(t, ExtractorSourcePathParam, source)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValuesFromCookie(t *testing.T) {\n\texampleRequest := func(req *http.Request) {\n\t\treq.Header.Set(echo.HeaderCookie, \"_csrf=token\")\n\t}\n\n\tvar testCases = []struct {\n\t\tname         string\n\t\tgivenRequest func(req *http.Request)\n\t\twhenName     string\n\t\twhenLimit    uint\n\t\texpectValues []string\n\t\texpectError  string\n\t}{\n\t\t{\n\t\t\tname:         \"ok, single value\",\n\t\t\tgivenRequest: exampleRequest,\n\t\t\twhenName:     \"_csrf\",\n\t\t\texpectValues: []string{\"token\"},\n\t\t},\n\t\t{\n\t\t\tname: \"ok, multiple value\",\n\t\t\tgivenRequest: func(req *http.Request) {\n\t\t\t\treq.Header.Add(echo.HeaderCookie, \"_csrf=token\")\n\t\t\t\treq.Header.Add(echo.HeaderCookie, \"_csrf=token2\")\n\t\t\t},\n\t\t\twhenName:     \"_csrf\",\n\t\t\twhenLimit:    2,\n\t\t\texpectValues: []string{\"token\", \"token2\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, no matching cookie\",\n\t\t\tgivenRequest: exampleRequest,\n\t\t\twhenName:     \"xxx\",\n\t\t\texpectValues: nil,\n\t\t\texpectError:  errCookieExtractorValueMissing.Error(),\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, no cookies at all\",\n\t\t\tgivenRequest: nil,\n\t\t\twhenName:     \"xxx\",\n\t\t\texpectValues: nil,\n\t\t\texpectError:  errCookieExtractorValueMissing.Error(),\n\t\t},\n\t\t{\n\t\t\tname: \"ok, cut values over extractorLimit\",\n\t\t\tgivenRequest: func(req *http.Request) {\n\t\t\t\tfor i := 1; i < 25; i++ {\n\t\t\t\t\treq.Header.Add(echo.HeaderCookie, fmt.Sprintf(\"_csrf=%v\", i))\n\t\t\t\t}\n\t\t\t},\n\t\t\twhenName:  \"_csrf\",\n\t\t\twhenLimit: extractorLimit,\n\t\t\texpectValues: []string{\n\t\t\t\t\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\", \"10\",\n\t\t\t\t\"11\", \"12\", \"13\", \"14\", \"15\", \"16\", \"17\", \"18\", \"19\", \"20\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := echo.New()\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\t\tif tc.givenRequest != nil {\n\t\t\t\ttc.givenRequest(req)\n\t\t\t}\n\t\t\trec := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\textractor := valuesFromCookie(tc.whenName, tc.whenLimit)\n\n\t\t\tvalues, source, err := extractor(c)\n\t\t\tassert.Equal(t, tc.expectValues, values)\n\t\t\tassert.Equal(t, ExtractorSourceCookie, source)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValuesFromForm(t *testing.T) {\n\texamplePostFormRequest := func(mod func(v *url.Values)) *http.Request {\n\t\tf := make(url.Values)\n\t\tf.Set(\"name\", \"Jon Snow\")\n\t\tf.Set(\"emails[]\", \"jon@labstack.com\")\n\t\tif mod != nil {\n\t\t\tmod(&f)\n\t\t}\n\n\t\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(f.Encode()))\n\t\treq.Header.Add(echo.HeaderContentType, echo.MIMEApplicationForm)\n\n\t\treturn req\n\t}\n\texampleGetFormRequest := func(mod func(v *url.Values)) *http.Request {\n\t\tf := make(url.Values)\n\t\tf.Set(\"name\", \"Jon Snow\")\n\t\tf.Set(\"emails[]\", \"jon@labstack.com\")\n\t\tif mod != nil {\n\t\t\tmod(&f)\n\t\t}\n\n\t\treq := httptest.NewRequest(http.MethodGet, \"/?\"+f.Encode(), nil)\n\t\treturn req\n\t}\n\n\texampleMultiPartFormRequest := func(mod func(w *multipart.Writer)) *http.Request {\n\t\tvar b bytes.Buffer\n\t\tw := multipart.NewWriter(&b)\n\t\tw.WriteField(\"name\", \"Jon Snow\")\n\t\tw.WriteField(\"emails[]\", \"jon@labstack.com\")\n\t\tif mod != nil {\n\t\t\tmod(w)\n\t\t}\n\n\t\tfw, _ := w.CreateFormFile(\"upload\", \"my.file\")\n\t\tfw.Write([]byte(`<div>hi</div>`))\n\t\tw.Close()\n\n\t\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(b.String()))\n\t\treq.Header.Add(echo.HeaderContentType, w.FormDataContentType())\n\n\t\treturn req\n\t}\n\n\tvar testCases = []struct {\n\t\tname         string\n\t\tgivenRequest *http.Request\n\t\twhenName     string\n\t\twhenLimit    uint\n\t\texpectValues []string\n\t\texpectError  string\n\t}{\n\t\t{\n\t\t\tname:         \"ok, POST form, single value\",\n\t\t\tgivenRequest: examplePostFormRequest(nil),\n\t\t\twhenName:     \"emails[]\",\n\t\t\texpectValues: []string{\"jon@labstack.com\"},\n\t\t},\n\t\t{\n\t\t\tname: \"ok, POST form, multiple value\",\n\t\t\tgivenRequest: examplePostFormRequest(func(v *url.Values) {\n\t\t\t\tv.Add(\"emails[]\", \"snow@labstack.com\")\n\t\t\t}),\n\t\t\twhenName:     \"emails[]\",\n\t\t\twhenLimit:    2,\n\t\t\texpectValues: []string{\"jon@labstack.com\", \"snow@labstack.com\"},\n\t\t},\n\t\t{\n\t\t\tname: \"ok, POST multipart/form, multiple value\",\n\t\t\tgivenRequest: exampleMultiPartFormRequest(func(w *multipart.Writer) {\n\t\t\t\tw.WriteField(\"emails[]\", \"snow@labstack.com\")\n\t\t\t}),\n\t\t\twhenName:     \"emails[]\",\n\t\t\twhenLimit:    2,\n\t\t\texpectValues: []string{\"jon@labstack.com\", \"snow@labstack.com\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, GET form, single value\",\n\t\t\tgivenRequest: exampleGetFormRequest(nil),\n\t\t\twhenName:     \"emails[]\",\n\t\t\texpectValues: []string{\"jon@labstack.com\"},\n\t\t},\n\t\t{\n\t\t\tname: \"ok, GET form, multiple value\",\n\t\t\tgivenRequest: examplePostFormRequest(func(v *url.Values) {\n\t\t\t\tv.Add(\"emails[]\", \"snow@labstack.com\")\n\t\t\t}),\n\t\t\twhenName:     \"emails[]\",\n\t\t\twhenLimit:    2,\n\t\t\texpectValues: []string{\"jon@labstack.com\", \"snow@labstack.com\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, POST form, value missing\",\n\t\t\tgivenRequest: examplePostFormRequest(nil),\n\t\t\twhenName:     \"nope\",\n\t\t\texpectError:  errFormExtractorValueMissing.Error(),\n\t\t},\n\t\t{\n\t\t\tname: \"ok, cut values over extractorLimit\",\n\t\t\tgivenRequest: examplePostFormRequest(func(v *url.Values) {\n\t\t\t\tfor i := 1; i < 25; i++ {\n\t\t\t\t\tv.Add(\"id[]\", fmt.Sprintf(\"%v\", i))\n\t\t\t\t}\n\t\t\t}),\n\t\t\twhenName:  \"id[]\",\n\t\t\twhenLimit: extractorLimit,\n\t\t\texpectValues: []string{\n\t\t\t\t\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\", \"10\",\n\t\t\t\t\"11\", \"12\", \"13\", \"14\", \"15\", \"16\", \"17\", \"18\", \"19\", \"20\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := echo.New()\n\n\t\t\treq := tc.givenRequest\n\t\t\trec := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\textractor := valuesFromForm(tc.whenName, tc.whenLimit)\n\n\t\t\tvalues, source, err := extractor(c)\n\t\t\tassert.Equal(t, tc.expectValues, values)\n\t\t\tassert.Equal(t, ExtractorSourceForm, source)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "middleware/key_auth.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"cmp\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\n// KeyAuthConfig defines the config for KeyAuth middleware.\n//\n// SECURITY: The Validator function is responsible for securely comparing API keys.\n// See KeyAuthValidator documentation for guidance on preventing timing attacks.\ntype KeyAuthConfig struct {\n\t// Skipper defines a function to skip middleware.\n\tSkipper Skipper\n\n\t// KeyLookup is a string in the form of \"<source>:<name>\" or \"<source>:<name>,<source>:<name>\" that is used\n\t// to extract key from the request.\n\t// Optional. Default value \"header:Authorization\".\n\t// Possible values:\n\t// - \"header:<name>\" or \"header:<name>:<cut-prefix>\"\n\t// \t\t\t`<cut-prefix>` is argument value to cut/trim prefix of the extracted value. This is useful if header\n\t//\t\t\tvalue has static prefix like `Authorization: <auth-scheme> <authorisation-parameters>` where part that we\n\t//\t\t\twant to cut is `<auth-scheme> ` note the space at the end.\n\t//\t\t\tIn case of basic authentication `Authorization: Basic <credentials>` prefix we want to remove is `Basic `.\n\t// - \"query:<name>\"\n\t// - \"form:<name>\"\n\t// - \"cookie:<name>\"\n\t// Multiple sources example:\n\t// - \"header:Authorization,header:X-Api-Key\"\n\tKeyLookup string\n\n\t// AllowedCheckLimit set how many KeyLookup values are allowed to be checked. This is\n\t// useful environments like corporate test environments with application proxies restricting\n\t// access to environment with their own auth scheme.\n\tAllowedCheckLimit uint\n\n\t// Validator is a function to validate key.\n\t// Required.\n\tValidator KeyAuthValidator\n\n\t// ErrorHandler defines a function which is executed when all lookups have been done and none of them passed Validator\n\t// function. ErrorHandler is executed with last missing (ErrExtractionValueMissing) or an invalid key.\n\t// It may be used to define a custom error.\n\t//\n\t// Note: when error handler swallows the error (returns nil) middleware continues handler chain execution towards handler.\n\t// This is useful in cases when portion of your site/api is publicly accessible and has extra features for authorized users\n\t// In that case you can use ErrorHandler to set default public auth value to request and continue with handler chain.\n\tErrorHandler KeyAuthErrorHandler\n\n\t// ContinueOnIgnoredError allows the next middleware/handler to be called when ErrorHandler decides to\n\t// ignore the error (by returning `nil`).\n\t// This is useful when parts of your site/api allow public access and some authorized routes provide extra functionality.\n\t// In that case you can use ErrorHandler to set a default public key auth value in the request context\n\t// and continue. Some logic down the remaining execution chain needs to check that (public) key auth value then.\n\tContinueOnIgnoredError bool\n}\n\n// KeyAuthValidator defines a function to validate KeyAuth credentials.\n//\n// SECURITY WARNING: To prevent timing attacks that could allow attackers to enumerate\n// valid API keys, validator implementations MUST use constant-time comparison.\n// Use crypto/subtle.ConstantTimeCompare instead of standard string equality (==)\n// or switch statements.\n//\n// Example of SECURE implementation:\n//\n//\timport \"crypto/subtle\"\n//\n//\tvalidator := func(c *echo.Context, key string, source ExtractorSource) (bool, error) {\n//\t    // Fetch valid keys from database/config\n//\t    validKeys := []string{\"key1\", \"key2\", \"key3\"}\n//\n//\t    for _, validKey := range validKeys {\n//\t        // Use constant-time comparison to prevent timing attacks\n//\t        if subtle.ConstantTimeCompare([]byte(key), []byte(validKey)) == 1 {\n//\t            return true, nil\n//\t        }\n//\t    }\n//\t    return false, nil\n//\t}\n//\n// Example of INSECURE implementation (DO NOT USE):\n//\n//\t// VULNERABLE TO TIMING ATTACKS - DO NOT USE\n//\tvalidator := func(c *echo.Context, key string, source ExtractorSource) (bool, error) {\n//\t    switch key {  // Timing leak!\n//\t    case \"valid-key\":\n//\t        return true, nil\n//\t    default:\n//\t        return false, nil\n//\t    }\n//\t}\ntype KeyAuthValidator func(c *echo.Context, key string, source ExtractorSource) (bool, error)\n\n// KeyAuthErrorHandler defines a function which is executed for an invalid key.\ntype KeyAuthErrorHandler func(c *echo.Context, err error) error\n\n// ErrKeyMissing denotes an error raised when key value could not be extracted from request\nvar ErrKeyMissing = echo.NewHTTPError(http.StatusUnauthorized, \"missing key\")\n\n// ErrInvalidKey denotes an error raised when key value is invalid by validator\nvar ErrInvalidKey = echo.NewHTTPError(http.StatusUnauthorized, \"invalid key\")\n\n// DefaultKeyAuthConfig is the default KeyAuth middleware config.\nvar DefaultKeyAuthConfig = KeyAuthConfig{\n\tSkipper:   DefaultSkipper,\n\tKeyLookup: \"header:\" + echo.HeaderAuthorization + \":Bearer \",\n}\n\n// KeyAuth returns an KeyAuth middleware.\n//\n// For valid key it calls the next handler.\n// For invalid key, it sends \"401 - Unauthorized\" response.\n// For missing key, it sends \"400 - Bad Request\" response.\nfunc KeyAuth(fn KeyAuthValidator) echo.MiddlewareFunc {\n\tc := DefaultKeyAuthConfig\n\tc.Validator = fn\n\treturn KeyAuthWithConfig(c)\n}\n\n// KeyAuthWithConfig returns an KeyAuth middleware or panics if configuration is invalid.\n//\n// For first valid key it calls the next handler.\n// For invalid key, it sends \"401 - Unauthorized\" response.\n// For missing key, it sends \"400 - Bad Request\" response.\nfunc KeyAuthWithConfig(config KeyAuthConfig) echo.MiddlewareFunc {\n\treturn toMiddlewareOrPanic(config)\n}\n\n// ToMiddleware converts KeyAuthConfig to middleware or returns an error for invalid configuration\nfunc (config KeyAuthConfig) ToMiddleware() (echo.MiddlewareFunc, error) {\n\tif config.Skipper == nil {\n\t\tconfig.Skipper = DefaultKeyAuthConfig.Skipper\n\t}\n\tif config.KeyLookup == \"\" {\n\t\tconfig.KeyLookup = DefaultKeyAuthConfig.KeyLookup\n\t}\n\tif config.Validator == nil {\n\t\treturn nil, errors.New(\"echo key-auth middleware requires a validator function\")\n\t}\n\n\tlimit := cmp.Or(config.AllowedCheckLimit, 1)\n\n\textractors, cErr := createExtractors(config.KeyLookup, limit)\n\tif cErr != nil {\n\t\treturn nil, fmt.Errorf(\"echo key-auth middleware could not create key extractor: %w\", cErr)\n\t}\n\tif len(extractors) == 0 {\n\t\treturn nil, errors.New(\"echo key-auth middleware could not create extractors from KeyLookup string\")\n\t}\n\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) error {\n\t\t\tif config.Skipper(c) {\n\t\t\t\treturn next(c)\n\t\t\t}\n\n\t\t\tvar lastExtractorErr error\n\t\t\tvar lastValidatorErr error\n\t\t\tfor _, extractor := range extractors {\n\t\t\t\tkeys, source, extrErr := extractor(c)\n\t\t\t\tif extrErr != nil {\n\t\t\t\t\tlastExtractorErr = extrErr\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tfor _, key := range keys {\n\t\t\t\t\tvalid, err := config.Validator(c, key, source)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlastValidatorErr = err\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif !valid {\n\t\t\t\t\t\tlastValidatorErr = ErrInvalidKey\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\treturn next(c)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// prioritize validator errors over extracting errors\n\t\t\terr := lastValidatorErr\n\t\t\tif err == nil {\n\t\t\t\terr = lastExtractorErr\n\t\t\t}\n\t\t\tif config.ErrorHandler != nil {\n\t\t\t\ttmpErr := config.ErrorHandler(c, err)\n\t\t\t\tif config.ContinueOnIgnoredError && tmpErr == nil {\n\t\t\t\t\treturn next(c)\n\t\t\t\t}\n\t\t\t\treturn tmpErr\n\t\t\t}\n\t\t\tif lastValidatorErr == nil {\n\t\t\t\treturn ErrKeyMissing.Wrap(err)\n\t\t\t}\n\t\t\treturn echo.ErrUnauthorized.Wrap(err)\n\t\t}\n\t}, nil\n}\n"
  },
  {
    "path": "middleware/key_auth_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"crypto/subtle\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc testKeyValidator(c *echo.Context, key string, source ExtractorSource) (bool, error) {\n\t// Use constant-time comparison to prevent timing attacks\n\tif subtle.ConstantTimeCompare([]byte(key), []byte(\"valid-key\")) == 1 {\n\t\treturn true, nil\n\t}\n\n\t// Special case for testing error handling\n\tif key == \"error-key\" { // Error path doesn't need constant-time\n\t\treturn false, errors.New(\"some user defined error\")\n\t}\n\n\treturn false, nil\n}\n\nfunc TestKeyAuth(t *testing.T) {\n\thandlerCalled := false\n\thandler := func(c *echo.Context) error {\n\t\thandlerCalled = true\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\tmiddlewareChain := KeyAuth(testKeyValidator)(handler)\n\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Set(echo.HeaderAuthorization, \"Bearer valid-key\")\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\terr := middlewareChain(c)\n\n\tassert.NoError(t, err)\n\tassert.True(t, handlerCalled)\n}\n\nfunc TestKeyAuthWithConfig(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname                string\n\t\tgivenRequestFunc    func() *http.Request\n\t\tgivenRequest        func(req *http.Request)\n\t\twhenConfig          func(conf *KeyAuthConfig)\n\t\texpectHandlerCalled bool\n\t\texpectError         string\n\t}{\n\t\t{\n\t\t\tname: \"ok, defaults, key from header\",\n\t\t\tgivenRequest: func(req *http.Request) {\n\t\t\t\treq.Header.Set(echo.HeaderAuthorization, \"Bearer valid-key\")\n\t\t\t},\n\t\t\texpectHandlerCalled: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok, custom skipper\",\n\t\t\tgivenRequest: func(req *http.Request) {\n\t\t\t\treq.Header.Set(echo.HeaderAuthorization, \"Bearer error-key\")\n\t\t\t},\n\t\t\twhenConfig: func(conf *KeyAuthConfig) {\n\t\t\t\tconf.Skipper = func(context *echo.Context) bool {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t},\n\t\t\texpectHandlerCalled: true,\n\t\t},\n\t\t{\n\t\t\tname: \"nok, defaults, invalid key from header, Authorization: Bearer\",\n\t\t\tgivenRequest: func(req *http.Request) {\n\t\t\t\treq.Header.Set(echo.HeaderAuthorization, \"Bearer invalid-key\")\n\t\t\t},\n\t\t\texpectHandlerCalled: false,\n\t\t\texpectError:         \"code=401, message=Unauthorized, err=code=401, message=invalid key\",\n\t\t},\n\t\t{\n\t\t\tname: \"nok, defaults, invalid scheme in header\",\n\t\t\tgivenRequest: func(req *http.Request) {\n\t\t\t\treq.Header.Set(echo.HeaderAuthorization, \"Bear valid-key\")\n\t\t\t},\n\t\t\texpectHandlerCalled: false,\n\t\t\texpectError:         \"code=401, message=missing key, err=invalid value in request header\",\n\t\t},\n\t\t{\n\t\t\tname:                \"nok, defaults, missing header\",\n\t\t\tgivenRequest:        func(req *http.Request) {},\n\t\t\texpectHandlerCalled: false,\n\t\t\texpectError:         \"code=401, message=missing key, err=missing value in request header\",\n\t\t},\n\t\t{\n\t\t\tname: \"ok, custom key lookup, header\",\n\t\t\tgivenRequest: func(req *http.Request) {\n\t\t\t\treq.Header.Set(\"API-Key\", \"valid-key\")\n\t\t\t},\n\t\t\twhenConfig: func(conf *KeyAuthConfig) {\n\t\t\t\tconf.KeyLookup = \"header:API-Key\"\n\t\t\t},\n\t\t\texpectHandlerCalled: true,\n\t\t},\n\t\t{\n\t\t\tname: \"nok, custom key lookup, missing header\",\n\t\t\tgivenRequest: func(req *http.Request) {\n\t\t\t},\n\t\t\twhenConfig: func(conf *KeyAuthConfig) {\n\t\t\t\tconf.KeyLookup = \"header:API-Key\"\n\t\t\t},\n\t\t\texpectHandlerCalled: false,\n\t\t\texpectError:         \"code=401, message=missing key, err=missing value in request header\",\n\t\t},\n\t\t{\n\t\t\tname: \"ok, custom key lookup, query\",\n\t\t\tgivenRequest: func(req *http.Request) {\n\t\t\t\tq := req.URL.Query()\n\t\t\t\tq.Add(\"key\", \"valid-key\")\n\t\t\t\treq.URL.RawQuery = q.Encode()\n\t\t\t},\n\t\t\twhenConfig: func(conf *KeyAuthConfig) {\n\t\t\t\tconf.KeyLookup = \"query:key\"\n\t\t\t},\n\t\t\texpectHandlerCalled: true,\n\t\t},\n\t\t{\n\t\t\tname: \"nok, custom key lookup, missing query param\",\n\t\t\twhenConfig: func(conf *KeyAuthConfig) {\n\t\t\t\tconf.KeyLookup = \"query:key\"\n\t\t\t},\n\t\t\texpectHandlerCalled: false,\n\t\t\texpectError:         \"code=401, message=missing key, err=missing value in the query string\",\n\t\t},\n\t\t{\n\t\t\tname: \"ok, custom key lookup, form\",\n\t\t\tgivenRequestFunc: func() *http.Request {\n\t\t\t\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(\"key=valid-key\"))\n\t\t\t\treq.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm)\n\t\t\t\treturn req\n\t\t\t},\n\t\t\twhenConfig: func(conf *KeyAuthConfig) {\n\t\t\t\tconf.KeyLookup = \"form:key\"\n\t\t\t},\n\t\t\texpectHandlerCalled: true,\n\t\t},\n\t\t{\n\t\t\tname: \"nok, custom key lookup, missing key in form\",\n\t\t\tgivenRequestFunc: func() *http.Request {\n\t\t\t\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(\"xxx=valid-key\"))\n\t\t\t\treq.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm)\n\t\t\t\treturn req\n\t\t\t},\n\t\t\twhenConfig: func(conf *KeyAuthConfig) {\n\t\t\t\tconf.KeyLookup = \"form:key\"\n\t\t\t},\n\t\t\texpectHandlerCalled: false,\n\t\t\texpectError:         \"code=401, message=missing key, err=missing value in the form\",\n\t\t},\n\t\t{\n\t\t\tname: \"ok, custom key lookup, cookie\",\n\t\t\tgivenRequest: func(req *http.Request) {\n\t\t\t\treq.AddCookie(&http.Cookie{\n\t\t\t\t\tName:  \"key\",\n\t\t\t\t\tValue: \"valid-key\",\n\t\t\t\t})\n\t\t\t\tq := req.URL.Query()\n\t\t\t\tq.Add(\"key\", \"valid-key\")\n\t\t\t\treq.URL.RawQuery = q.Encode()\n\t\t\t},\n\t\t\twhenConfig: func(conf *KeyAuthConfig) {\n\t\t\t\tconf.KeyLookup = \"cookie:key\"\n\t\t\t},\n\t\t\texpectHandlerCalled: true,\n\t\t},\n\t\t{\n\t\t\tname: \"nok, custom key lookup, missing cookie param\",\n\t\t\twhenConfig: func(conf *KeyAuthConfig) {\n\t\t\t\tconf.KeyLookup = \"cookie:key\"\n\t\t\t},\n\t\t\texpectHandlerCalled: false,\n\t\t\texpectError:         \"code=401, message=missing key, err=missing value in cookies\",\n\t\t},\n\t\t{\n\t\t\tname: \"nok, custom errorHandler, error from extractor\",\n\t\t\twhenConfig: func(conf *KeyAuthConfig) {\n\t\t\t\tconf.KeyLookup = \"header:token\"\n\t\t\t\tconf.ErrorHandler = func(c *echo.Context, err error) error {\n\t\t\t\t\treturn echo.NewHTTPError(http.StatusTeapot, \"custom\").Wrap(err)\n\t\t\t\t}\n\t\t\t},\n\t\t\texpectHandlerCalled: false,\n\t\t\texpectError:         \"code=418, message=custom, err=missing value in request header\",\n\t\t},\n\t\t{\n\t\t\tname: \"nok, custom errorHandler, error from validator\",\n\t\t\tgivenRequest: func(req *http.Request) {\n\t\t\t\treq.Header.Set(echo.HeaderAuthorization, \"Bearer error-key\")\n\t\t\t},\n\t\t\twhenConfig: func(conf *KeyAuthConfig) {\n\t\t\t\tconf.ErrorHandler = func(c *echo.Context, err error) error {\n\t\t\t\t\treturn echo.NewHTTPError(http.StatusTeapot, \"custom\").Wrap(err)\n\t\t\t\t}\n\t\t\t},\n\t\t\texpectHandlerCalled: false,\n\t\t\texpectError:         \"code=418, message=custom, err=some user defined error\",\n\t\t},\n\t\t{\n\t\t\tname: \"nok, defaults, error from validator\",\n\t\t\tgivenRequest: func(req *http.Request) {\n\t\t\t\treq.Header.Set(echo.HeaderAuthorization, \"Bearer error-key\")\n\t\t\t},\n\t\t\twhenConfig:          func(conf *KeyAuthConfig) {},\n\t\t\texpectHandlerCalled: false,\n\t\t\texpectError:         \"code=401, message=Unauthorized, err=some user defined error\",\n\t\t},\n\t\t{\n\t\t\tname: \"ok, custom validator checks source\",\n\t\t\tgivenRequest: func(req *http.Request) {\n\t\t\t\tq := req.URL.Query()\n\t\t\t\tq.Add(\"key\", \"valid-key\")\n\t\t\t\treq.URL.RawQuery = q.Encode()\n\t\t\t},\n\t\t\twhenConfig: func(conf *KeyAuthConfig) {\n\t\t\t\tconf.KeyLookup = \"query:key\"\n\t\t\t\tconf.Validator = func(c *echo.Context, key string, source ExtractorSource) (bool, error) {\n\t\t\t\t\tif source == ExtractorSourceQuery {\n\t\t\t\t\t\treturn true, nil\n\t\t\t\t\t}\n\t\t\t\t\treturn false, errors.New(\"invalid source\")\n\t\t\t\t}\n\n\t\t\t},\n\t\t\texpectHandlerCalled: true,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\thandlerCalled := false\n\t\t\thandler := func(c *echo.Context) error {\n\t\t\t\thandlerCalled = true\n\t\t\t\treturn c.String(http.StatusOK, \"test\")\n\t\t\t}\n\t\t\tconfig := KeyAuthConfig{\n\t\t\t\tValidator: testKeyValidator,\n\t\t\t}\n\t\t\tif tc.whenConfig != nil {\n\t\t\t\ttc.whenConfig(&config)\n\t\t\t}\n\t\t\tmiddlewareChain := KeyAuthWithConfig(config)(handler)\n\n\t\t\te := echo.New()\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\t\tif tc.givenRequestFunc != nil {\n\t\t\t\treq = tc.givenRequestFunc()\n\t\t\t}\n\t\t\tif tc.givenRequest != nil {\n\t\t\t\ttc.givenRequest(req)\n\t\t\t}\n\t\t\trec := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\terr := middlewareChain(c)\n\n\t\t\tassert.Equal(t, tc.expectHandlerCalled, handlerCalled)\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestKeyAuthWithConfig_errors(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname        string\n\t\twhenConfig  KeyAuthConfig\n\t\texpectError string\n\t}{\n\t\t{\n\t\t\tname: \"ok, no error\",\n\t\t\twhenConfig: KeyAuthConfig{\n\t\t\t\tValidator: func(c *echo.Context, key string, source ExtractorSource) (bool, error) {\n\t\t\t\t\treturn false, nil\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok, missing validator func\",\n\t\t\twhenConfig: KeyAuthConfig{\n\t\t\t\tValidator: nil,\n\t\t\t},\n\t\t\texpectError: \"echo key-auth middleware requires a validator function\",\n\t\t},\n\t\t{\n\t\t\tname: \"ok, extractor source can not be split\",\n\t\t\twhenConfig: KeyAuthConfig{\n\t\t\t\tKeyLookup: \"nope\",\n\t\t\t\tValidator: func(c *echo.Context, key string, source ExtractorSource) (bool, error) {\n\t\t\t\t\treturn false, nil\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: \"echo key-auth middleware could not create key extractor: extractor source for lookup could not be split into needed parts: nope\",\n\t\t},\n\t\t{\n\t\t\tname: \"ok, no extractors\",\n\t\t\twhenConfig: KeyAuthConfig{\n\t\t\t\tKeyLookup: \"nope:nope\",\n\t\t\t\tValidator: func(c *echo.Context, key string, source ExtractorSource) (bool, error) {\n\t\t\t\t\treturn false, nil\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectError: \"echo key-auth middleware could not create extractors from KeyLookup string\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tmw, err := tc.whenConfig.ToMiddleware()\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.Nil(t, mw)\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NotNil(t, mw)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMustKeyAuthWithConfig_panic(t *testing.T) {\n\tassert.Panics(t, func() {\n\t\tKeyAuthWithConfig(KeyAuthConfig{})\n\t})\n}\n\nfunc TestKeyAuth_errorHandlerSwallowsError(t *testing.T) {\n\thandlerCalled := false\n\tvar authValue string\n\thandler := func(c *echo.Context) error {\n\t\thandlerCalled = true\n\t\tauthValue = c.Get(\"auth\").(string)\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\tmiddlewareChain := KeyAuthWithConfig(KeyAuthConfig{\n\t\tValidator: testKeyValidator,\n\t\tErrorHandler: func(c *echo.Context, err error) error {\n\t\t\t// could check error to decide if we can swallow the error\n\t\t\tc.Set(\"auth\", \"public\")\n\t\t\treturn nil\n\t\t},\n\t\tContinueOnIgnoredError: true,\n\t})(handler)\n\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t// no auth header this time\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\terr := middlewareChain(c)\n\n\tassert.NoError(t, err)\n\tassert.True(t, handlerCalled)\n\tassert.Equal(t, \"public\", authValue)\n}\n"
  },
  {
    "path": "middleware/method_override.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\n// MethodOverrideConfig defines the config for MethodOverride middleware.\ntype MethodOverrideConfig struct {\n\t// Skipper defines a function to skip middleware.\n\tSkipper Skipper\n\n\t// Getter is a function that gets overridden method from the request.\n\t// Optional. Default values MethodFromHeader(echo.HeaderXHTTPMethodOverride).\n\tGetter MethodOverrideGetter\n}\n\n// MethodOverrideGetter is a function that gets overridden method from the request\ntype MethodOverrideGetter func(c *echo.Context) string\n\n// DefaultMethodOverrideConfig is the default MethodOverride middleware config.\nvar DefaultMethodOverrideConfig = MethodOverrideConfig{\n\tSkipper: DefaultSkipper,\n\tGetter:  MethodFromHeader(echo.HeaderXHTTPMethodOverride),\n}\n\n// MethodOverride returns a MethodOverride middleware.\n// MethodOverride  middleware checks for the overridden method from the request and\n// uses it instead of the original method.\n//\n// For security reasons, only `POST` method can be overridden.\nfunc MethodOverride() echo.MiddlewareFunc {\n\treturn MethodOverrideWithConfig(DefaultMethodOverrideConfig)\n}\n\n// MethodOverrideWithConfig returns a Method Override middleware with config or panics on invalid configuration.\nfunc MethodOverrideWithConfig(config MethodOverrideConfig) echo.MiddlewareFunc {\n\treturn toMiddlewareOrPanic(config)\n}\n\n// ToMiddleware converts MethodOverrideConfig to middleware or returns an error for invalid configuration\nfunc (config MethodOverrideConfig) ToMiddleware() (echo.MiddlewareFunc, error) {\n\t// Defaults\n\tif config.Skipper == nil {\n\t\tconfig.Skipper = DefaultMethodOverrideConfig.Skipper\n\t}\n\tif config.Getter == nil {\n\t\tconfig.Getter = DefaultMethodOverrideConfig.Getter\n\t}\n\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) error {\n\t\t\tif config.Skipper(c) {\n\t\t\t\treturn next(c)\n\t\t\t}\n\n\t\t\treq := c.Request()\n\t\t\tif req.Method == http.MethodPost {\n\t\t\t\tm := config.Getter(c)\n\t\t\t\tif m != \"\" {\n\t\t\t\t\treq.Method = m\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn next(c)\n\t\t}\n\t}, nil\n}\n\n// MethodFromHeader is a `MethodOverrideGetter` that gets overridden method from\n// the request header.\nfunc MethodFromHeader(header string) MethodOverrideGetter {\n\treturn func(c *echo.Context) string {\n\t\treturn c.Request().Header.Get(header)\n\t}\n}\n\n// MethodFromForm is a `MethodOverrideGetter` that gets overridden method from the\n// form parameter.\nfunc MethodFromForm(param string) MethodOverrideGetter {\n\treturn func(c *echo.Context) string {\n\t\treturn c.FormValue(param)\n\t}\n}\n\n// MethodFromQuery is a `MethodOverrideGetter` that gets overridden method from\n// the query parameter.\nfunc MethodFromQuery(param string) MethodOverrideGetter {\n\treturn func(c *echo.Context) string {\n\t\treturn c.QueryParam(param)\n\t}\n}\n"
  },
  {
    "path": "middleware/method_override_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestMethodOverride(t *testing.T) {\n\te := echo.New()\n\tm := MethodOverride()\n\th := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\n\t// Override with http header\n\treq := httptest.NewRequest(http.MethodPost, \"/\", nil)\n\trec := httptest.NewRecorder()\n\treq.Header.Set(echo.HeaderXHTTPMethodOverride, http.MethodDelete)\n\tc := e.NewContext(req, rec)\n\n\terr := m(h)(c)\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, http.MethodDelete, req.Method)\n\n}\n\nfunc TestMethodOverride_formParam(t *testing.T) {\n\te := echo.New()\n\th := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\n\t// Override with form parameter\n\tm, err := MethodOverrideConfig{Getter: MethodFromForm(\"_method\")}.ToMiddleware()\n\tassert.NoError(t, err)\n\treq := httptest.NewRequest(http.MethodPost, \"/\", bytes.NewReader([]byte(\"_method=\"+http.MethodDelete)))\n\trec := httptest.NewRecorder()\n\treq.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm)\n\tc := e.NewContext(req, rec)\n\n\terr = m(h)(c)\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, http.MethodDelete, req.Method)\n}\n\nfunc TestMethodOverride_queryParam(t *testing.T) {\n\te := echo.New()\n\th := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\n\t// Override with query parameter\n\tm, err := MethodOverrideConfig{Getter: MethodFromQuery(\"_method\")}.ToMiddleware()\n\tassert.NoError(t, err)\n\treq := httptest.NewRequest(http.MethodPost, \"/?_method=\"+http.MethodDelete, nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\terr = m(h)(c)\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, http.MethodDelete, req.Method)\n}\n\nfunc TestMethodOverride_ignoreGet(t *testing.T) {\n\te := echo.New()\n\tm := MethodOverride()\n\th := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\n\t// Ignore `GET`\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Set(echo.HeaderXHTTPMethodOverride, http.MethodDelete)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\terr := m(h)(c)\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, http.MethodGet, req.Method)\n}\n"
  },
  {
    "path": "middleware/middleware.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"net/http\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\n// Skipper defines a function to skip middleware. Returning true skips processing the middleware.\ntype Skipper func(c *echo.Context) bool\n\n// BeforeFunc defines a function which is executed just before the middleware.\ntype BeforeFunc func(c *echo.Context)\n\nfunc captureTokens(pattern *regexp.Regexp, input string) *strings.Replacer {\n\tgroups := pattern.FindAllStringSubmatch(input, -1)\n\tif groups == nil {\n\t\treturn nil\n\t}\n\tvalues := groups[0][1:]\n\treplace := make([]string, 2*len(values))\n\tfor i, v := range values {\n\t\tj := 2 * i\n\t\treplace[j] = \"$\" + strconv.Itoa(i+1)\n\t\treplace[j+1] = v\n\t}\n\treturn strings.NewReplacer(replace...)\n}\n\nfunc rewriteRulesRegex(rewrite map[string]string) map[*regexp.Regexp]string {\n\t// Initialize\n\trulesRegex := map[*regexp.Regexp]string{}\n\tfor k, v := range rewrite {\n\t\tk = regexp.QuoteMeta(k)\n\t\tk = strings.ReplaceAll(k, `\\*`, \"(.*?)\")\n\t\tif strings.HasPrefix(k, `\\^`) {\n\t\t\tk = strings.ReplaceAll(k, `\\^`, \"^\")\n\t\t}\n\t\tk = k + \"$\"\n\t\trulesRegex[regexp.MustCompile(k)] = v\n\t}\n\treturn rulesRegex\n}\n\nfunc rewriteURL(rewriteRegex map[*regexp.Regexp]string, req *http.Request) error {\n\tif len(rewriteRegex) == 0 {\n\t\treturn nil\n\t}\n\n\t// Depending how HTTP request is sent RequestURI could contain Scheme://Host/path or be just /path.\n\t// We only want to use path part for rewriting and therefore trim prefix if it exists\n\trawURI := req.RequestURI\n\tif rawURI != \"\" && rawURI[0] != '/' {\n\t\tprefix := \"\"\n\t\tif req.URL.Scheme != \"\" {\n\t\t\tprefix = req.URL.Scheme + \"://\"\n\t\t}\n\t\tif req.URL.Host != \"\" {\n\t\t\tprefix += req.URL.Host // host or host:port\n\t\t}\n\t\tif prefix != \"\" {\n\t\t\trawURI = strings.TrimPrefix(rawURI, prefix)\n\t\t}\n\t}\n\n\tfor k, v := range rewriteRegex {\n\t\tif replacer := captureTokens(k, rawURI); replacer != nil {\n\t\t\turl, err := req.URL.Parse(replacer.Replace(v))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treq.URL = url\n\n\t\t\treturn nil // rewrite only once\n\t\t}\n\t}\n\treturn nil\n}\n\n// DefaultSkipper returns false which processes the middleware.\nfunc DefaultSkipper(c *echo.Context) bool {\n\treturn false\n}\n\nfunc toMiddlewareOrPanic(config echo.MiddlewareConfigurator) echo.MiddlewareFunc {\n\tmw, err := config.ToMiddleware()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn mw\n}\n"
  },
  {
    "path": "middleware/middleware_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"regexp\"\n\t\"testing\"\n)\n\nfunc TestRewriteURL(t *testing.T) {\n\tvar testCases = []struct {\n\t\twhenURL       string\n\t\texpectPath    string\n\t\texpectRawPath string\n\t\texpectQuery   string\n\t\texpectErr     string\n\t}{\n\t\t{\n\t\t\twhenURL:       \"http://localhost:8080/old\",\n\t\t\texpectPath:    \"/new\",\n\t\t\texpectRawPath: \"\",\n\t\t},\n\t\t{ // encoded `ol%64` (decoded `old`) should not be rewritten to `/new`\n\t\t\twhenURL:       \"/ol%64\", // `%64` is decoded `d`\n\t\t\texpectPath:    \"/old\",\n\t\t\texpectRawPath: \"/ol%64\",\n\t\t},\n\t\t{\n\t\t\twhenURL:       \"http://localhost:8080/users/+_+/orders/___++++?test=1\",\n\t\t\texpectPath:    \"/user/+_+/order/___++++\",\n\t\t\texpectRawPath: \"\",\n\t\t\texpectQuery:   \"test=1\",\n\t\t},\n\t\t{\n\t\t\twhenURL:       \"http://localhost:8080/users/%20a/orders/%20aa\",\n\t\t\texpectPath:    \"/user/ a/order/ aa\",\n\t\t\texpectRawPath: \"\",\n\t\t},\n\t\t{\n\t\t\twhenURL:       \"http://localhost:8080/%47%6f%2f?test=1\",\n\t\t\texpectPath:    \"/Go/\",\n\t\t\texpectRawPath: \"/%47%6f%2f\",\n\t\t\texpectQuery:   \"test=1\",\n\t\t},\n\t\t{\n\t\t\twhenURL:       \"/users/jill/orders/T%2FcO4lW%2Ft%2FVp%2F\",\n\t\t\texpectPath:    \"/user/jill/order/T/cO4lW/t/Vp/\",\n\t\t\texpectRawPath: \"/user/jill/order/T%2FcO4lW%2Ft%2FVp%2F\",\n\t\t},\n\t\t{ // do nothing, replace nothing\n\t\t\twhenURL:       \"http://localhost:8080/user/jill/order/T%2FcO4lW%2Ft%2FVp%2F\",\n\t\t\texpectPath:    \"/user/jill/order/T/cO4lW/t/Vp/\",\n\t\t\texpectRawPath: \"/user/jill/order/T%2FcO4lW%2Ft%2FVp%2F\",\n\t\t},\n\t\t{\n\t\t\twhenURL:       \"http://localhost:8080/static\",\n\t\t\texpectPath:    \"/static/path\",\n\t\t\texpectRawPath: \"\",\n\t\t\texpectQuery:   \"role=AUTHOR&limit=1000\",\n\t\t},\n\t\t{\n\t\t\twhenURL:       \"/static\",\n\t\t\texpectPath:    \"/static/path\",\n\t\t\texpectRawPath: \"\",\n\t\t\texpectQuery:   \"role=AUTHOR&limit=1000\",\n\t\t},\n\t}\n\n\trules := map[*regexp.Regexp]string{\n\t\tregexp.MustCompile(\"^/old$\"):                      \"/new\",\n\t\tregexp.MustCompile(\"^/users/(.*?)/orders/(.*?)$\"): \"/user/$1/order/$2\",\n\t\tregexp.MustCompile(\"^/static$\"):                   \"/static/path?role=AUTHOR&limit=1000\",\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenURL, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)\n\n\t\t\terr := rewriteURL(rules, req)\n\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectPath, req.URL.Path)       // Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\n\t\t\tassert.Equal(t, tc.expectRawPath, req.URL.RawPath) // RawPath, an optional field which only gets set if the default encoding is different from Path.\n\t\t\tassert.Equal(t, tc.expectQuery, req.URL.RawQuery)\n\t\t})\n\t}\n}\n\ntype testResponseWriterNoFlushHijack struct {\n}\n\nfunc (w *testResponseWriterNoFlushHijack) WriteHeader(statusCode int) {\n}\nfunc (w *testResponseWriterNoFlushHijack) Write([]byte) (int, error) {\n\treturn 0, nil\n}\nfunc (w *testResponseWriterNoFlushHijack) Header() http.Header {\n\treturn nil\n}\n\ntype testResponseWriterUnwrapper struct {\n\tunwrapCalled int\n\trw           http.ResponseWriter\n}\n\nfunc (w *testResponseWriterUnwrapper) WriteHeader(statusCode int) {\n}\nfunc (w *testResponseWriterUnwrapper) Write([]byte) (int, error) {\n\treturn 0, nil\n}\nfunc (w *testResponseWriterUnwrapper) Header() http.Header {\n\treturn nil\n}\nfunc (w *testResponseWriterUnwrapper) Unwrap() http.ResponseWriter {\n\tw.unwrapCalled++\n\treturn w.rw\n}\n\ntype testResponseWriterUnwrapperHijack struct {\n\ttestResponseWriterUnwrapper\n}\n\nfunc (w *testResponseWriterUnwrapperHijack) Hijack() (net.Conn, *bufio.ReadWriter, error) {\n\treturn nil, nil, errors.New(\"can hijack\")\n}\n"
  },
  {
    "path": "middleware/proxy.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/rand\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\n// TODO: Handle TLS proxy\n\n// ProxyConfig defines the config for Proxy middleware.\ntype ProxyConfig struct {\n\t// Skipper defines a function to skip middleware.\n\tSkipper Skipper\n\n\t// Balancer defines a load balancing technique.\n\t// Required.\n\tBalancer ProxyBalancer\n\n\t// RetryCount defines the number of times a failed proxied request should be retried\n\t// using the next available ProxyTarget. Defaults to 0, meaning requests are never retried.\n\tRetryCount int\n\n\t// RetryFilter defines a function used to determine if a failed request to a\n\t// ProxyTarget should be retried. The RetryFilter will only be called when the number\n\t// of previous retries is less than RetryCount. If the function returns true, the\n\t// request will be retried. The provided error indicates the reason for the request\n\t// failure. When the ProxyTarget is unavailable, the error will be an instance of\n\t// echo.HTTPError with a code of http.StatusBadGateway. In all other cases, the error\n\t// will indicate an internal error in the Proxy middleware. When a RetryFilter is not\n\t// specified, all requests that fail with http.StatusBadGateway will be retried. A custom\n\t// RetryFilter can be provided to only retry specific requests. Note that RetryFilter is\n\t// only called when the request to the target fails, or an internal error in the Proxy\n\t// middleware has occurred. Successful requests that return a non-200 response code cannot\n\t// be retried.\n\tRetryFilter func(c *echo.Context, e error) bool\n\n\t// ErrorHandler defines a function which can be used to return custom errors from\n\t// the Proxy middleware. ErrorHandler is only invoked when there has been\n\t// either an internal error in the Proxy middleware or the ProxyTarget is\n\t// unavailable. Due to the way requests are proxied, ErrorHandler is not invoked\n\t// when a ProxyTarget returns a non-200 response. In these cases, the response\n\t// is already written so errors cannot be modified. ErrorHandler is only\n\t// invoked after all retry attempts have been exhausted.\n\tErrorHandler func(c *echo.Context, err error) error\n\n\t// Rewrite defines URL path rewrite rules. The values captured in asterisk can be\n\t// retrieved by index e.g. $1, $2 and so on.\n\t// Examples:\n\t// \"/old\":              \"/new\",\n\t// \"/api/*\":            \"/$1\",\n\t// \"/js/*\":             \"/public/javascripts/$1\",\n\t// \"/users/*/orders/*\": \"/user/$1/order/$2\",\n\tRewrite map[string]string\n\n\t// RegexRewrite defines rewrite rules using regexp.Rexexp with captures\n\t// Every capture group in the values can be retrieved by index e.g. $1, $2 and so on.\n\t// Example:\n\t// \"^/old/[0.9]+/\":     \"/new\",\n\t// \"^/api/.+?/(.*)\":    \"/v2/$1\",\n\tRegexRewrite map[*regexp.Regexp]string\n\n\t// Context key to store selected ProxyTarget into context.\n\t// Optional. Default value \"target\".\n\tContextKey string\n\n\t// To customize the transport to remote.\n\t// Examples: If custom TLS certificates are required.\n\tTransport http.RoundTripper\n\n\t// ModifyResponse defines function to modify response from ProxyTarget.\n\tModifyResponse func(*http.Response) error\n}\n\n// ProxyTarget defines the upstream target.\ntype ProxyTarget struct {\n\tName string\n\tURL  *url.URL\n\tMeta map[string]any\n}\n\n// ProxyBalancer defines an interface to implement a load balancing technique.\ntype ProxyBalancer interface {\n\tAddTarget(target *ProxyTarget) bool\n\tRemoveTarget(targetName string) bool\n\tNext(c *echo.Context) (*ProxyTarget, error)\n}\n\ntype commonBalancer struct {\n\ttargets []*ProxyTarget\n\tmutex   sync.Mutex\n}\n\n// RandomBalancer implements a random load balancing technique.\ntype randomBalancer struct {\n\tcommonBalancer\n\trandom *rand.Rand\n}\n\n// RoundRobinBalancer implements a round-robin load balancing technique.\ntype roundRobinBalancer struct {\n\tcommonBalancer\n\t// tracking the index on `targets` slice for the next `*ProxyTarget` to be used\n\ti int\n}\n\n// DefaultProxyConfig is the default Proxy middleware config.\nvar DefaultProxyConfig = ProxyConfig{\n\tSkipper:    DefaultSkipper,\n\tContextKey: \"target\",\n}\n\nfunc proxyRaw(c *echo.Context, t *ProxyTarget, config ProxyConfig) http.Handler {\n\tvar dialFunc func(ctx context.Context, network, addr string) (net.Conn, error)\n\tif transport, ok := config.Transport.(*http.Transport); ok {\n\t\tif transport.TLSClientConfig != nil {\n\t\t\td := tls.Dialer{\n\t\t\t\tConfig: transport.TLSClientConfig,\n\t\t\t}\n\t\t\tdialFunc = d.DialContext\n\t\t}\n\t}\n\tif dialFunc == nil {\n\t\tvar d net.Dialer\n\t\tdialFunc = d.DialContext\n\t}\n\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tin, _, err := http.NewResponseController(w).Hijack()\n\t\tif err != nil {\n\t\t\tc.Set(\"_error\", fmt.Errorf(\"proxy raw, hijack error=%w, url=%s\", err, t.URL))\n\t\t\treturn\n\t\t}\n\t\tdefer in.Close()\n\n\t\tout, err := dialFunc(c.Request().Context(), \"tcp\", t.URL.Host)\n\t\tif err != nil {\n\t\t\tc.Set(\"_error\", echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf(\"proxy raw, dial error=%v, url=%s\", err, t.URL)))\n\t\t\treturn\n\t\t}\n\t\tdefer out.Close()\n\n\t\t// Write header\n\t\terr = r.Write(out)\n\t\tif err != nil {\n\t\t\tc.Set(\"_error\", echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf(\"proxy raw, request header copy error=%v, url=%s\", err, t.URL)))\n\t\t\treturn\n\t\t}\n\n\t\terrCh := make(chan error, 2)\n\t\tcp := func(dst io.Writer, src io.Reader) {\n\t\t\t_, copyErr := io.Copy(dst, src)\n\t\t\terrCh <- copyErr\n\t\t}\n\n\t\tgo cp(out, in)\n\t\tgo cp(in, out)\n\n\t\t// Wait for BOTH goroutines to complete\n\t\terr1 := <-errCh\n\t\terr2 := <-errCh\n\n\t\tif err1 != nil && err1 != io.EOF {\n\t\t\tc.Set(\"_error\", fmt.Errorf(\"proxy raw, copy body error=%w, url=%s\", err1, t.URL))\n\t\t} else if err2 != nil && err2 != io.EOF {\n\t\t\tc.Set(\"_error\", fmt.Errorf(\"proxy raw, copy body error=%w, url=%s\", err2, t.URL))\n\t\t}\n\t})\n}\n\n// NewRandomBalancer returns a random proxy balancer.\nfunc NewRandomBalancer(targets []*ProxyTarget) ProxyBalancer {\n\tb := randomBalancer{}\n\tb.targets = targets\n\t// G404 (CWE-338): Use of weak random number generator (math/rand or math/rand/v2 instead of crypto/rand)\n\t// this random is used to select next target. I can not think of reason this must be cryptographically safe. If you can - please open PR.\n\tb.random = rand.New(rand.NewSource(int64(time.Now().Nanosecond()))) // #nosec G404\n\treturn &b\n}\n\n// NewRoundRobinBalancer returns a round-robin proxy balancer.\nfunc NewRoundRobinBalancer(targets []*ProxyTarget) ProxyBalancer {\n\tb := roundRobinBalancer{}\n\tb.targets = targets\n\treturn &b\n}\n\n// AddTarget adds an upstream target to the list and returns `true`.\n//\n// However, if a target with the same name already exists then the operation is aborted returning `false`.\nfunc (b *commonBalancer) AddTarget(target *ProxyTarget) bool {\n\tb.mutex.Lock()\n\tdefer b.mutex.Unlock()\n\tfor _, t := range b.targets {\n\t\tif t.Name == target.Name {\n\t\t\treturn false\n\t\t}\n\t}\n\tb.targets = append(b.targets, target)\n\treturn true\n}\n\n// RemoveTarget removes an upstream target from the list by name.\n//\n// Returns `true` on success, `false` if no target with the name is found.\nfunc (b *commonBalancer) RemoveTarget(name string) bool {\n\tb.mutex.Lock()\n\tdefer b.mutex.Unlock()\n\tfor i, t := range b.targets {\n\t\tif t.Name == name {\n\t\t\tb.targets = append(b.targets[:i], b.targets[i+1:]...)\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// Next randomly returns an upstream target.\n//\n// Note: `nil` is returned in case upstream target list is empty.\nfunc (b *randomBalancer) Next(c *echo.Context) (*ProxyTarget, error) {\n\tb.mutex.Lock()\n\tdefer b.mutex.Unlock()\n\tif len(b.targets) == 0 {\n\t\treturn nil, nil\n\t} else if len(b.targets) == 1 {\n\t\treturn b.targets[0], nil\n\t}\n\treturn b.targets[b.random.Intn(len(b.targets))], nil\n}\n\n// Next returns an upstream target using round-robin technique. In the case\n// where a previously failed request is being retried, the round-robin\n// balancer will attempt to use the next target relative to the original\n// request. If the list of targets held by the balancer is modified while a\n// failed request is being retried, it is possible that the balancer will\n// return the original failed target.\n//\n// Note: `nil` is returned in case upstream target list is empty.\nfunc (b *roundRobinBalancer) Next(c *echo.Context) (*ProxyTarget, error) {\n\tb.mutex.Lock()\n\tdefer b.mutex.Unlock()\n\tif len(b.targets) == 0 {\n\t\treturn nil, nil\n\t} else if len(b.targets) == 1 {\n\t\treturn b.targets[0], nil\n\t}\n\n\tvar i int\n\tconst lastIdxKey = \"_round_robin_last_index\"\n\t// This request is a retry, start from the index of the previous\n\t// target to ensure we don't attempt to retry the request with\n\t// the same failed target\n\tif c.Get(lastIdxKey) != nil {\n\t\ti = c.Get(lastIdxKey).(int)\n\t\ti++\n\t\tif i >= len(b.targets) {\n\t\t\ti = 0\n\t\t}\n\t} else {\n\t\t// This is a first time request, use the global index\n\t\tif b.i >= len(b.targets) {\n\t\t\tb.i = 0\n\t\t}\n\t\ti = b.i\n\t\tb.i++\n\t}\n\tc.Set(lastIdxKey, i)\n\treturn b.targets[i], nil\n}\n\n// Proxy returns a Proxy middleware.\n//\n// Proxy middleware forwards the request to upstream server using a configured load balancing technique.\nfunc Proxy(balancer ProxyBalancer) echo.MiddlewareFunc {\n\tc := DefaultProxyConfig\n\tc.Balancer = balancer\n\treturn ProxyWithConfig(c)\n}\n\n// ProxyWithConfig returns a Proxy middleware or panics if configuration is invalid.\n//\n// Proxy middleware forwards the request to upstream server using a configured load balancing technique.\nfunc ProxyWithConfig(config ProxyConfig) echo.MiddlewareFunc {\n\treturn toMiddlewareOrPanic(config)\n}\n\n// ToMiddleware converts ProxyConfig to middleware or returns an error for invalid configuration\nfunc (config ProxyConfig) ToMiddleware() (echo.MiddlewareFunc, error) {\n\tif config.Skipper == nil {\n\t\tconfig.Skipper = DefaultProxyConfig.Skipper\n\t}\n\tif config.ContextKey == \"\" {\n\t\tconfig.ContextKey = DefaultProxyConfig.ContextKey\n\t}\n\tif config.Balancer == nil {\n\t\treturn nil, errors.New(\"echo proxy middleware requires balancer\")\n\t}\n\tif config.RetryFilter == nil {\n\t\tconfig.RetryFilter = func(c *echo.Context, e error) bool {\n\t\t\tif httpErr, ok := e.(*echo.HTTPError); ok {\n\t\t\t\treturn httpErr.Code == http.StatusBadGateway\n\t\t\t}\n\t\t\treturn false\n\t\t}\n\t}\n\tif config.ErrorHandler == nil {\n\t\tconfig.ErrorHandler = func(c *echo.Context, err error) error {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif config.Rewrite != nil {\n\t\tif config.RegexRewrite == nil {\n\t\t\tconfig.RegexRewrite = make(map[*regexp.Regexp]string)\n\t\t}\n\t\tfor k, v := range rewriteRulesRegex(config.Rewrite) {\n\t\t\tconfig.RegexRewrite[k] = v\n\t\t}\n\t}\n\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) (err error) {\n\t\t\tif config.Skipper(c) {\n\t\t\t\treturn next(c)\n\t\t\t}\n\n\t\t\treq := c.Request()\n\t\t\tres := c.Response()\n\t\t\tif err := rewriteURL(config.RegexRewrite, req); err != nil {\n\t\t\t\treturn config.ErrorHandler(c, err)\n\t\t\t}\n\n\t\t\t// Fix header\n\t\t\t// Basically it's not good practice to unconditionally pass incoming x-real-ip header to upstream.\n\t\t\t// However, for backward compatibility, legacy behavior is preserved unless you configure Echo#IPExtractor.\n\t\t\tif req.Header.Get(echo.HeaderXRealIP) == \"\" || c.Echo().IPExtractor != nil {\n\t\t\t\treq.Header.Set(echo.HeaderXRealIP, c.RealIP())\n\t\t\t}\n\t\t\tif req.Header.Get(echo.HeaderXForwardedProto) == \"\" {\n\t\t\t\treq.Header.Set(echo.HeaderXForwardedProto, c.Scheme())\n\t\t\t}\n\t\t\tif c.IsWebSocket() && req.Header.Get(echo.HeaderXForwardedFor) == \"\" { // For HTTP, it is automatically set by Go HTTP reverse proxy.\n\t\t\t\treq.Header.Set(echo.HeaderXForwardedFor, c.RealIP())\n\t\t\t}\n\n\t\t\tretries := config.RetryCount\n\t\t\tfor {\n\t\t\t\ttgt, err := config.Balancer.Next(c)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn config.ErrorHandler(c, err)\n\t\t\t\t}\n\n\t\t\t\tc.Set(config.ContextKey, tgt)\n\n\t\t\t\t//If retrying a failed request, clear any previous errors from\n\t\t\t\t//context here so that balancers have the option to check for\n\t\t\t\t//errors that occurred using previous target\n\t\t\t\tif retries < config.RetryCount {\n\t\t\t\t\tc.Set(\"_error\", nil)\n\t\t\t\t}\n\n\t\t\t\t// This is needed for ProxyConfig.ModifyResponse and/or ProxyConfig.Transport to be able to process the Request\n\t\t\t\t// that Balancer may have replaced with c.SetRequest.\n\t\t\t\treq = c.Request()\n\n\t\t\t\t// Proxy\n\t\t\t\tswitch {\n\t\t\t\tcase c.IsWebSocket():\n\t\t\t\t\tproxyRaw(c, tgt, config).ServeHTTP(res, req)\n\t\t\t\tdefault: // even SSE requests\n\t\t\t\t\tproxyHTTP(c, tgt, config).ServeHTTP(res, req)\n\t\t\t\t}\n\n\t\t\t\terr, hasError := c.Get(\"_error\").(error)\n\t\t\t\tif !hasError {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tretry := retries > 0 && config.RetryFilter(c, err)\n\t\t\t\tif !retry {\n\t\t\t\t\treturn config.ErrorHandler(c, err)\n\t\t\t\t}\n\n\t\t\t\tretries--\n\t\t\t}\n\t\t}\n\t}, nil\n}\n\n// StatusCodeContextCanceled is a custom HTTP status code for situations\n// where a client unexpectedly closed the connection to the server.\n// As there is no standard error code for \"client closed connection\", but\n// various well-known HTTP clients and server implement this HTTP code we use\n// 499 too instead of the more problematic 5xx, which does not allow to detect this situation\nconst StatusCodeContextCanceled = 499\n\nfunc proxyHTTP(c *echo.Context, tgt *ProxyTarget, config ProxyConfig) http.Handler {\n\tproxy := httputil.NewSingleHostReverseProxy(tgt.URL)\n\tproxy.ErrorHandler = func(resp http.ResponseWriter, req *http.Request, err error) {\n\t\tdesc := tgt.URL.String()\n\t\tif tgt.Name != \"\" {\n\t\t\tdesc = fmt.Sprintf(\"%s(%s)\", tgt.Name, tgt.URL.String())\n\t\t}\n\t\t// If the client canceled the request (usually by closing the connection), we can report a\n\t\t// client error (4xx) instead of a server error (5xx) to correctly identify the situation.\n\t\t// The Go standard library (at of late 2020) wraps the exported, standard\n\t\t// context. Canceled error with unexported garbage value requiring a substring check, see\n\t\t// https://github.com/golang/go/blob/6965b01ea248cabb70c3749fd218b36089a21efb/src/net/net.go#L416-L430\n\t\t// From Caddy https://github.com/caddyserver/caddy/blob/afa778ae05503f563af0d1015cdf7e5e78b1eeec/modules/caddyhttp/reverseproxy/reverseproxy.go#L1352\n\t\tif errors.Is(err, context.Canceled) || strings.Contains(err.Error(), \"operation was canceled\") {\n\t\t\thttpError := echo.NewHTTPError(StatusCodeContextCanceled, \"client closed connection\").Wrap(err)\n\t\t\tc.Set(\"_error\", httpError)\n\t\t} else {\n\t\t\thttpError := echo.NewHTTPError(\n\t\t\t\thttp.StatusBadGateway,\n\t\t\t\t\"remote server unreachable, could not proxy request\",\n\t\t\t).Wrap(fmt.Errorf(\"server: %s, err: %w\", desc, err))\n\t\t\tc.Set(\"_error\", httpError)\n\t\t}\n\t}\n\tproxy.Transport = config.Transport\n\tproxy.ModifyResponse = config.ModifyResponse\n\treturn proxy\n}\n"
  },
  {
    "path": "middleware/proxy_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"golang.org/x/net/websocket\"\n)\n\n// Assert expected with url.EscapedPath method to obtain the path.\nfunc TestProxy(t *testing.T) {\n\t// Setup\n\tt1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Fprint(w, \"target 1\")\n\t}))\n\tdefer t1.Close()\n\turl1, _ := url.Parse(t1.URL)\n\tt2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Fprint(w, \"target 2\")\n\t}))\n\tdefer t2.Close()\n\turl2, _ := url.Parse(t2.URL)\n\n\ttargets := []*ProxyTarget{\n\t\t{\n\t\t\tName: \"target 1\",\n\t\t\tURL:  url1,\n\t\t},\n\t\t{\n\t\t\tName: \"target 2\",\n\t\t\tURL:  url2,\n\t\t},\n\t}\n\trb := NewRandomBalancer(nil)\n\t// must add targets:\n\tfor _, target := range targets {\n\t\tassert.True(t, rb.AddTarget(target))\n\t}\n\n\t// must ignore duplicates:\n\tfor _, target := range targets {\n\t\tassert.False(t, rb.AddTarget(target))\n\t}\n\n\t// Random\n\te := echo.New()\n\te.Use(ProxyWithConfig(ProxyConfig{Balancer: rb}))\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\tbody := rec.Body.String()\n\texpected := map[string]bool{\n\t\t\"target 1\": true,\n\t\t\"target 2\": true,\n\t}\n\tassert.Condition(t, func() bool {\n\t\treturn expected[body]\n\t})\n\n\tfor _, target := range targets {\n\t\tassert.True(t, rb.RemoveTarget(target.Name))\n\t}\n\n\tassert.False(t, rb.RemoveTarget(\"unknown target\"))\n\n\t// Round-robin\n\trrb := NewRoundRobinBalancer(targets)\n\te = echo.New()\n\te.Use(ProxyWithConfig(ProxyConfig{Balancer: rrb}))\n\n\trec = httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\tbody = rec.Body.String()\n\tassert.Equal(t, \"target 1\", body)\n\n\trec = httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\tbody = rec.Body.String()\n\tassert.Equal(t, \"target 2\", body)\n\n\t// ModifyResponse\n\te = echo.New()\n\te.Use(ProxyWithConfig(ProxyConfig{\n\t\tBalancer: rrb,\n\t\tModifyResponse: func(res *http.Response) error {\n\t\t\tres.Body = io.NopCloser(bytes.NewBuffer([]byte(\"modified\")))\n\t\t\tres.Header.Set(\"X-Modified\", \"1\")\n\t\t\treturn nil\n\t\t},\n\t}))\n\n\trec = httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\tassert.Equal(t, \"modified\", rec.Body.String())\n\tassert.Equal(t, \"1\", rec.Header().Get(\"X-Modified\"))\n\n\t// ProxyTarget is set in context\n\tcontextObserver := func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) (err error) {\n\t\t\tnext(c)\n\t\t\tassert.Contains(t, targets, c.Get(\"target\"), \"target is not set in context\")\n\t\t\treturn nil\n\t\t}\n\t}\n\n\te = echo.New()\n\te.Use(contextObserver)\n\te.Use(ProxyWithConfig(ProxyConfig{Balancer: NewRoundRobinBalancer(targets)}))\n\trec = httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n}\n\nfunc TestMustProxyWithConfig_emptyBalancerPanics(t *testing.T) {\n\tassert.Panics(t, func() {\n\t\tProxyWithConfig(ProxyConfig{Balancer: nil})\n\t})\n}\n\nfunc TestProxyRealIPHeader(t *testing.T) {\n\t// Setup\n\tupstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))\n\tdefer upstream.Close()\n\turl, _ := url.Parse(upstream.URL)\n\trrb := NewRoundRobinBalancer([]*ProxyTarget{{Name: \"upstream\", URL: url}})\n\te := echo.New()\n\te.Use(ProxyWithConfig(ProxyConfig{Balancer: rrb}))\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\n\tremoteAddrIP, _, _ := net.SplitHostPort(req.RemoteAddr)\n\trealIPHeaderIP := \"203.0.113.1\"\n\textractedRealIP := \"203.0.113.10\"\n\ttests := []*struct {\n\t\thasRealIPheader bool\n\t\thasIPExtractor  bool\n\t\texpectedXRealIP string\n\t}{\n\t\t{false, false, remoteAddrIP},\n\t\t{false, true, extractedRealIP},\n\t\t{true, false, realIPHeaderIP},\n\t\t{true, true, extractedRealIP},\n\t}\n\n\tfor _, tt := range tests {\n\t\tif tt.hasRealIPheader {\n\t\t\treq.Header.Set(echo.HeaderXRealIP, realIPHeaderIP)\n\t\t} else {\n\t\t\treq.Header.Del(echo.HeaderXRealIP)\n\t\t}\n\t\tif tt.hasIPExtractor {\n\t\t\te.IPExtractor = func(*http.Request) string {\n\t\t\t\treturn extractedRealIP\n\t\t\t}\n\t\t} else {\n\t\t\te.IPExtractor = nil\n\t\t}\n\t\te.ServeHTTP(rec, req)\n\t\tassert.Equal(t, tt.expectedXRealIP, req.Header.Get(echo.HeaderXRealIP), \"hasRealIPheader: %t / hasIPExtractor: %t\", tt.hasRealIPheader, tt.hasIPExtractor)\n\t}\n}\n\nfunc TestProxyRewrite(t *testing.T) {\n\tvar testCases = []struct {\n\t\twhenPath         string\n\t\texpectProxiedURI string\n\t\texpectStatus     int\n\t}{\n\t\t{\n\t\t\twhenPath:         \"/api/users\",\n\t\t\texpectProxiedURI: \"/users\",\n\t\t\texpectStatus:     http.StatusOK,\n\t\t},\n\t\t{\n\t\t\twhenPath:         \"/js/main.js\",\n\t\t\texpectProxiedURI: \"/public/javascripts/main.js\",\n\t\t\texpectStatus:     http.StatusOK,\n\t\t},\n\t\t{\n\t\t\twhenPath:         \"/old\",\n\t\t\texpectProxiedURI: \"/new\",\n\t\t\texpectStatus:     http.StatusOK,\n\t\t},\n\t\t{\n\t\t\twhenPath:         \"/users/jack/orders/1\",\n\t\t\texpectProxiedURI: \"/user/jack/order/1\",\n\t\t\texpectStatus:     http.StatusOK,\n\t\t},\n\t\t{\n\t\t\twhenPath:         \"/user/jill/order/T%2FcO4lW%2Ft%2FVp%2F\",\n\t\t\texpectProxiedURI: \"/user/jill/order/T%2FcO4lW%2Ft%2FVp%2F\",\n\t\t\texpectStatus:     http.StatusOK,\n\t\t},\n\t\t{ // ` ` (space) is encoded by httpClient to `%20` when doing request to Echo. `%20` should not be double escaped when proxying request\n\t\t\twhenPath:         \"/api/new users\",\n\t\t\texpectProxiedURI: \"/new%20users\",\n\t\t\texpectStatus:     http.StatusOK,\n\t\t},\n\t\t{ // query params should be proxied and not be modified\n\t\t\twhenPath:         \"/api/users?limit=10\",\n\t\t\texpectProxiedURI: \"/users?limit=10\",\n\t\t\texpectStatus:     http.StatusOK,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenPath, func(t *testing.T) {\n\t\t\treceivedRequestURI := make(chan string, 1)\n\t\t\tupstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t// RequestURI is the unmodified request-target of the Request-Line (RFC 7230, Section 3.1.1) as sent by the client to a server\n\t\t\t\t// we need unmodified target to see if we are encoding/decoding the url in addition to rewrite/replace logic\n\t\t\t\t// if original request had `%2F` we should not magically decode it to `/` as it would change what was requested\n\t\t\t\treceivedRequestURI <- r.RequestURI\n\t\t\t}))\n\t\t\tdefer upstream.Close()\n\t\t\tserverURL, _ := url.Parse(upstream.URL)\n\t\t\trrb := NewRoundRobinBalancer([]*ProxyTarget{{Name: \"upstream\", URL: serverURL}})\n\n\t\t\t// Rewrite\n\t\t\te := echo.New()\n\t\t\te.Use(ProxyWithConfig(ProxyConfig{\n\t\t\t\tBalancer: rrb,\n\t\t\t\tRewrite: map[string]string{\n\t\t\t\t\t\"/old\":              \"/new\",\n\t\t\t\t\t\"/api/*\":            \"/$1\",\n\t\t\t\t\t\"/js/*\":             \"/public/javascripts/$1\",\n\t\t\t\t\t\"/users/*/orders/*\": \"/user/$1/order/$2\",\n\t\t\t\t},\n\t\t\t}))\n\n\t\t\ttargetURL, _ := serverURL.Parse(tc.whenPath)\n\t\t\treq := httptest.NewRequest(http.MethodGet, targetURL.String(), nil)\n\t\t\trec := httptest.NewRecorder()\n\n\t\t\te.ServeHTTP(rec, req)\n\n\t\t\tassert.Equal(t, tc.expectStatus, rec.Code)\n\t\t\tactualRequestURI := <-receivedRequestURI\n\t\t\tassert.Equal(t, tc.expectProxiedURI, actualRequestURI)\n\t\t})\n\t}\n}\n\nfunc TestProxyRewriteRegex(t *testing.T) {\n\t// Setup\n\treceivedRequestURI := make(chan string, 1)\n\tupstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t// RequestURI is the unmodified request-target of the Request-Line (RFC 7230, Section 3.1.1) as sent by the client to a server\n\t\t// we need unmodified target to see if we are encoding/decoding the url in addition to rewrite/replace logic\n\t\t// if original request had `%2F` we should not magically decode it to `/` as it would change what was requested\n\t\treceivedRequestURI <- r.RequestURI\n\t}))\n\tdefer upstream.Close()\n\ttmpUrL, _ := url.Parse(upstream.URL)\n\trrb := NewRoundRobinBalancer([]*ProxyTarget{{Name: \"upstream\", URL: tmpUrL}})\n\n\t// Rewrite\n\te := echo.New()\n\te.Use(ProxyWithConfig(ProxyConfig{\n\t\tBalancer: rrb,\n\t\tRewrite: map[string]string{\n\t\t\t\"^/a/*\":     \"/v1/$1\",\n\t\t\t\"^/b/*/c/*\": \"/v2/$2/$1\",\n\t\t\t\"^/c/*/*\":   \"/v3/$2\",\n\t\t},\n\t\tRegexRewrite: map[*regexp.Regexp]string{\n\t\t\tregexp.MustCompile(\"^/x/.+?/(.*)\"):   \"/v4/$1\",\n\t\t\tregexp.MustCompile(\"^/y/(.+?)/(.*)\"): \"/v5/$2/$1\",\n\t\t},\n\t}))\n\n\ttestCases := []struct {\n\t\trequestPath string\n\t\tstatusCode  int\n\t\texpectPath  string\n\t}{\n\t\t{\"/unmatched\", http.StatusOK, \"/unmatched\"},\n\t\t{\"/a/test\", http.StatusOK, \"/v1/test\"},\n\t\t{\"/b/foo/c/bar/baz\", http.StatusOK, \"/v2/bar/baz/foo\"},\n\t\t{\"/c/ignore/test\", http.StatusOK, \"/v3/test\"},\n\t\t{\"/c/ignore1/test/this\", http.StatusOK, \"/v3/test/this\"},\n\t\t{\"/x/ignore/test\", http.StatusOK, \"/v4/test\"},\n\t\t{\"/y/foo/bar\", http.StatusOK, \"/v5/bar/foo\"},\n\t\t// NB: fragment is not added by golang httputil.NewSingleHostReverseProxy implementation\n\t\t// $2 = `bar?q=1#frag`, $1 = `foo`. replaced uri = `/v5/bar?q=1#frag/foo` but httputil.NewSingleHostReverseProxy does not send `#frag/foo` (currently)\n\t\t{\"/y/foo/bar?q=1#frag\", http.StatusOK, \"/v5/bar?q=1\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.requestPath, func(t *testing.T) {\n\t\t\ttargetURL, _ := url.Parse(tc.requestPath)\n\t\t\treq := httptest.NewRequest(http.MethodGet, targetURL.String(), nil)\n\t\t\trec := httptest.NewRecorder()\n\n\t\t\te.ServeHTTP(rec, req)\n\n\t\t\tactualRequestURI := <-receivedRequestURI\n\t\t\tassert.Equal(t, tc.expectPath, actualRequestURI)\n\t\t\tassert.Equal(t, tc.statusCode, rec.Code)\n\t\t})\n\t}\n}\n\nfunc TestProxyError(t *testing.T) {\n\t// Setup\n\turl1, _ := url.Parse(\"http://127.0.0.1:27121\")\n\turl2, _ := url.Parse(\"http://127.0.0.1:27122\")\n\n\ttargets := []*ProxyTarget{\n\t\t{\n\t\t\tName: \"target 1\",\n\t\t\tURL:  url1,\n\t\t},\n\t\t{\n\t\t\tName: \"target 2\",\n\t\t\tURL:  url2,\n\t\t},\n\t}\n\trb := NewRandomBalancer(nil)\n\t// must add targets:\n\tfor _, target := range targets {\n\t\tassert.True(t, rb.AddTarget(target))\n\t}\n\n\t// must ignore duplicates:\n\tfor _, target := range targets {\n\t\tassert.False(t, rb.AddTarget(target))\n\t}\n\n\t// Random\n\te := echo.New()\n\te.Use(ProxyWithConfig(ProxyConfig{Balancer: rb}))\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\n\t// Remote unreachable\n\trec := httptest.NewRecorder()\n\treq.URL.Path = \"/api/users\"\n\te.ServeHTTP(rec, req)\n\tassert.Equal(t, \"/api/users\", req.URL.Path)\n\tassert.Equal(t, http.StatusBadGateway, rec.Code)\n}\n\nfunc TestClientCancelConnectionResultsHTTPCode499(t *testing.T) {\n\tvar timeoutStop sync.WaitGroup\n\ttimeoutStop.Add(1)\n\tHTTPTarget := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\ttimeoutStop.Wait() // wait until we have canceled the request\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer HTTPTarget.Close()\n\ttargetURL, _ := url.Parse(HTTPTarget.URL)\n\ttarget := &ProxyTarget{\n\t\tName: \"target\",\n\t\tURL:  targetURL,\n\t}\n\trb := NewRandomBalancer(nil)\n\tassert.True(t, rb.AddTarget(target))\n\te := echo.New()\n\te.Use(ProxyWithConfig(ProxyConfig{Balancer: rb}))\n\trec := httptest.NewRecorder()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\tctx, cancel := context.WithCancel(req.Context())\n\treq = req.WithContext(ctx)\n\tgo func() {\n\t\ttime.Sleep(10 * time.Millisecond)\n\t\tcancel()\n\t}()\n\te.ServeHTTP(rec, req)\n\ttimeoutStop.Done()\n\tassert.Equal(t, 499, rec.Code)\n}\n\ntype testProvider struct {\n\tcommonBalancer\n\ttarget *ProxyTarget\n\terr    error\n}\n\nfunc (p *testProvider) Next(c *echo.Context) (*ProxyTarget, error) {\n\treturn p.target, p.err\n}\n\nfunc TestTargetProvider(t *testing.T) {\n\tt1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Fprint(w, \"target 1\")\n\t}))\n\tdefer t1.Close()\n\turl1, _ := url.Parse(t1.URL)\n\n\te := echo.New()\n\ttp := &testProvider{}\n\ttp.target = &ProxyTarget{Name: \"target 1\", URL: url1}\n\te.Use(Proxy(tp))\n\trec := httptest.NewRecorder()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\te.ServeHTTP(rec, req)\n\tbody := rec.Body.String()\n\tassert.Equal(t, \"target 1\", body)\n}\n\nfunc TestFailNextTarget(t *testing.T) {\n\turl1, err := url.Parse(\"http://dummy:8080\")\n\tassert.Nil(t, err)\n\n\te := echo.New()\n\ttp := &testProvider{}\n\ttp.target = &ProxyTarget{Name: \"target 1\", URL: url1}\n\ttp.err = echo.NewHTTPError(http.StatusInternalServerError, \"method could not select target\")\n\n\te.Use(Proxy(tp))\n\trec := httptest.NewRecorder()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\te.ServeHTTP(rec, req)\n\tbody := rec.Body.String()\n\tassert.Equal(t, \"{\\\"message\\\":\\\"method could not select target\\\"}\\n\", body)\n}\n\nfunc TestRandomBalancerWithNoTargets(t *testing.T) {\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/?id=1&name=Jon+Snow\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\t// Assert balancer with empty targets does return `nil` on `Next()`\n\trb := NewRandomBalancer(nil)\n\ttarget, err := rb.Next(c)\n\tassert.Nil(t, target)\n\tassert.NoError(t, err)\n}\n\nfunc TestRoundRobinBalancerWithNoTargets(t *testing.T) {\n\t// Assert balancer with empty targets does return `nil` on `Next()`\n\trrb := NewRoundRobinBalancer([]*ProxyTarget{})\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/?id=1&name=Jon+Snow\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\ttarget, err := rrb.Next(c)\n\tassert.Nil(t, target)\n\tassert.NoError(t, err)\n}\n\nfunc TestProxyRetries(t *testing.T) {\n\tnewServer := func(res int) (*url.URL, *httptest.Server) {\n\t\tserver := httptest.NewServer(\n\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tw.WriteHeader(res)\n\t\t\t}),\n\t\t)\n\t\ttargetURL, _ := url.Parse(server.URL)\n\t\treturn targetURL, server\n\t}\n\n\ttargetURL, server := newServer(http.StatusOK)\n\tdefer server.Close()\n\tgoodTarget := &ProxyTarget{\n\t\tName: \"Good\",\n\t\tURL:  targetURL,\n\t}\n\n\ttargetURL, server = newServer(http.StatusBadRequest)\n\tdefer server.Close()\n\tgoodTargetWith40X := &ProxyTarget{\n\t\tName: \"Good with 40X\",\n\t\tURL:  targetURL,\n\t}\n\n\ttargetURL, _ = url.Parse(\"http://127.0.0.1:27121\")\n\tbadTarget := &ProxyTarget{\n\t\tName: \"Bad\",\n\t\tURL:  targetURL,\n\t}\n\n\talwaysRetryFilter := func(c *echo.Context, e error) bool { return true }\n\tneverRetryFilter := func(c *echo.Context, e error) bool { return false }\n\n\ttestCases := []struct {\n\t\tname             string\n\t\tretryCount       int\n\t\tretryFilters     []func(c *echo.Context, e error) bool\n\t\ttargets          []*ProxyTarget\n\t\texpectedResponse int\n\t}{\n\t\t{\n\t\t\tname: \"retry count 0 does not attempt retry on fail\",\n\t\t\ttargets: []*ProxyTarget{\n\t\t\t\tbadTarget,\n\t\t\t\tgoodTarget,\n\t\t\t},\n\t\t\texpectedResponse: http.StatusBadGateway,\n\t\t},\n\t\t{\n\t\t\tname:       \"retry count 1 does not attempt retry on success\",\n\t\t\tretryCount: 1,\n\t\t\ttargets: []*ProxyTarget{\n\t\t\t\tgoodTarget,\n\t\t\t},\n\t\t\texpectedResponse: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:       \"retry count 1 does retry on handler return true\",\n\t\t\tretryCount: 1,\n\t\t\tretryFilters: []func(c *echo.Context, e error) bool{\n\t\t\t\talwaysRetryFilter,\n\t\t\t},\n\t\t\ttargets: []*ProxyTarget{\n\t\t\t\tbadTarget,\n\t\t\t\tgoodTarget,\n\t\t\t},\n\t\t\texpectedResponse: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:       \"retry count 1 does not retry on handler return false\",\n\t\t\tretryCount: 1,\n\t\t\tretryFilters: []func(c *echo.Context, e error) bool{\n\t\t\t\tneverRetryFilter,\n\t\t\t},\n\t\t\ttargets: []*ProxyTarget{\n\t\t\t\tbadTarget,\n\t\t\t\tgoodTarget,\n\t\t\t},\n\t\t\texpectedResponse: http.StatusBadGateway,\n\t\t},\n\t\t{\n\t\t\tname:       \"retry count 2 returns error when no more retries left\",\n\t\t\tretryCount: 2,\n\t\t\tretryFilters: []func(c *echo.Context, e error) bool{\n\t\t\t\talwaysRetryFilter,\n\t\t\t\talwaysRetryFilter,\n\t\t\t},\n\t\t\ttargets: []*ProxyTarget{\n\t\t\t\tbadTarget,\n\t\t\t\tbadTarget,\n\t\t\t\tbadTarget,\n\t\t\t\tgoodTarget, //Should never be reached as only 2 retries\n\t\t\t},\n\t\t\texpectedResponse: http.StatusBadGateway,\n\t\t},\n\t\t{\n\t\t\tname:       \"retry count 2 returns error when retries left but handler returns false\",\n\t\t\tretryCount: 3,\n\t\t\tretryFilters: []func(c *echo.Context, e error) bool{\n\t\t\t\talwaysRetryFilter,\n\t\t\t\talwaysRetryFilter,\n\t\t\t\tneverRetryFilter,\n\t\t\t},\n\t\t\ttargets: []*ProxyTarget{\n\t\t\t\tbadTarget,\n\t\t\t\tbadTarget,\n\t\t\t\tbadTarget,\n\t\t\t\tgoodTarget, //Should never be reached as retry handler returns false on 2nd check\n\t\t\t},\n\t\t\texpectedResponse: http.StatusBadGateway,\n\t\t},\n\t\t{\n\t\t\tname:       \"retry count 3 succeeds\",\n\t\t\tretryCount: 3,\n\t\t\tretryFilters: []func(c *echo.Context, e error) bool{\n\t\t\t\talwaysRetryFilter,\n\t\t\t\talwaysRetryFilter,\n\t\t\t\talwaysRetryFilter,\n\t\t\t},\n\t\t\ttargets: []*ProxyTarget{\n\t\t\t\tbadTarget,\n\t\t\t\tbadTarget,\n\t\t\t\tbadTarget,\n\t\t\t\tgoodTarget,\n\t\t\t},\n\t\t\texpectedResponse: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:       \"40x responses are not retried\",\n\t\t\tretryCount: 1,\n\t\t\ttargets: []*ProxyTarget{\n\t\t\t\tgoodTargetWith40X,\n\t\t\t\tgoodTarget,\n\t\t\t},\n\t\t\texpectedResponse: http.StatusBadRequest,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\n\t\t\tretryFilterCall := 0\n\t\t\tretryFilter := func(c *echo.Context, e error) bool {\n\t\t\t\tif len(tc.retryFilters) == 0 {\n\t\t\t\t\tassert.FailNow(t, fmt.Sprintf(\"unexpected calls, %d, to retry handler\", retryFilterCall))\n\t\t\t\t}\n\n\t\t\t\tretryFilterCall++\n\n\t\t\t\tnextRetryFilter := tc.retryFilters[0]\n\t\t\t\ttc.retryFilters = tc.retryFilters[1:]\n\n\t\t\t\treturn nextRetryFilter(c, e)\n\t\t\t}\n\n\t\t\te := echo.New()\n\t\t\te.Use(ProxyWithConfig(\n\t\t\t\tProxyConfig{\n\t\t\t\t\tBalancer:    NewRoundRobinBalancer(tc.targets),\n\t\t\t\t\tRetryCount:  tc.retryCount,\n\t\t\t\t\tRetryFilter: retryFilter,\n\t\t\t\t},\n\t\t\t))\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\t\trec := httptest.NewRecorder()\n\n\t\t\te.ServeHTTP(rec, req)\n\n\t\t\tassert.Equal(t, tc.expectedResponse, rec.Code)\n\t\t\tif len(tc.retryFilters) > 0 {\n\t\t\t\tassert.FailNow(t, fmt.Sprintf(\"expected %d more retry handler calls\", len(tc.retryFilters)))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestProxyRetryWithBackendTimeout(t *testing.T) {\n\n\ttransport := http.DefaultTransport.(*http.Transport).Clone()\n\ttransport.ResponseHeaderTimeout = time.Millisecond * 500\n\n\ttimeoutBackend := httptest.NewServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\ttime.Sleep(1 * time.Second)\n\t\t\tw.WriteHeader(404)\n\t\t}),\n\t)\n\tdefer timeoutBackend.Close()\n\n\ttimeoutTargetURL, _ := url.Parse(timeoutBackend.URL)\n\tgoodBackend := httptest.NewServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.WriteHeader(200)\n\t\t}),\n\t)\n\tdefer goodBackend.Close()\n\n\tgoodTargetURL, _ := url.Parse(goodBackend.URL)\n\te := echo.New()\n\te.Use(ProxyWithConfig(\n\t\tProxyConfig{\n\t\t\tTransport: transport,\n\t\t\tBalancer: NewRoundRobinBalancer([]*ProxyTarget{\n\t\t\t\t{\n\t\t\t\t\tName: \"Timeout\",\n\t\t\t\t\tURL:  timeoutTargetURL,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName: \"Good\",\n\t\t\t\t\tURL:  goodTargetURL,\n\t\t\t\t},\n\t\t\t}),\n\t\t\tRetryCount: 1,\n\t\t},\n\t))\n\n\tvar wg sync.WaitGroup\n\tfor i := 0; i < 20; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\t\trec := httptest.NewRecorder()\n\t\t\te.ServeHTTP(rec, req)\n\t\t\tassert.Equal(t, 200, rec.Code)\n\t\t}()\n\t}\n\n\twg.Wait()\n\n}\n\nfunc TestProxyErrorHandler(t *testing.T) {\n\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\n\tgoodURL, _ := url.Parse(server.URL)\n\tdefer server.Close()\n\tgoodTarget := &ProxyTarget{\n\t\tName: \"Good\",\n\t\tURL:  goodURL,\n\t}\n\n\tbadURL, _ := url.Parse(\"http://127.0.0.1:27121\")\n\tbadTarget := &ProxyTarget{\n\t\tName: \"Bad\",\n\t\tURL:  badURL,\n\t}\n\n\ttransformedError := errors.New(\"a new error\")\n\n\ttestCases := []struct {\n\t\tname             string\n\t\ttarget           *ProxyTarget\n\t\terrorHandler     func(c *echo.Context, e error) error\n\t\texpectFinalError func(t *testing.T, err error)\n\t}{\n\t\t{\n\t\t\tname:   \"Error handler not invoked when request success\",\n\t\t\ttarget: goodTarget,\n\t\t\terrorHandler: func(c *echo.Context, e error) error {\n\t\t\t\tassert.FailNow(t, \"error handler should not be invoked\")\n\t\t\t\treturn e\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"Error handler invoked when request fails\",\n\t\t\ttarget: badTarget,\n\t\t\terrorHandler: func(c *echo.Context, e error) error {\n\t\t\t\thttpErr, ok := e.(*echo.HTTPError)\n\t\t\t\tassert.True(t, ok, \"expected http error to be passed to handler\")\n\t\t\t\tassert.Equal(t, http.StatusBadGateway, httpErr.Code, \"expected http bad gateway error to be passed to handler\")\n\t\t\t\treturn transformedError\n\t\t\t},\n\t\t\texpectFinalError: func(t *testing.T, err error) {\n\t\t\t\tassert.Equal(t, transformedError, err, \"transformed error not returned from proxy\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := echo.New()\n\t\t\te.Use(ProxyWithConfig(\n\t\t\t\tProxyConfig{\n\t\t\t\t\tBalancer:     NewRoundRobinBalancer([]*ProxyTarget{tc.target}),\n\t\t\t\t\tErrorHandler: tc.errorHandler,\n\t\t\t\t},\n\t\t\t))\n\n\t\t\terrorHandlerCalled := false\n\t\t\tdheh := echo.DefaultHTTPErrorHandler(false)\n\t\t\te.HTTPErrorHandler = func(c *echo.Context, err error) {\n\t\t\t\terrorHandlerCalled = true\n\t\t\t\ttc.expectFinalError(t, err)\n\t\t\t\tdheh(c, err)\n\t\t\t}\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\t\trec := httptest.NewRecorder()\n\n\t\t\te.ServeHTTP(rec, req)\n\n\t\t\tif !errorHandlerCalled && tc.expectFinalError != nil {\n\t\t\t\tt.Fatalf(\"error handler was not called\")\n\t\t\t}\n\n\t\t})\n\t}\n}\n\ntype testContextKey string\ntype customBalancer struct {\n\ttarget *ProxyTarget\n}\n\nfunc (b *customBalancer) AddTarget(target *ProxyTarget) bool {\n\treturn false\n}\nfunc (b *customBalancer) RemoveTarget(name string) bool {\n\treturn false\n}\n\nfunc (b *customBalancer) Next(c *echo.Context) (*ProxyTarget, error) {\n\tctx := context.WithValue(c.Request().Context(), testContextKey(\"FROM_BALANCER\"), \"CUSTOM_BALANCER\")\n\tc.SetRequest(c.Request().WithContext(ctx))\n\treturn b.target, nil\n}\n\nfunc TestModifyResponseUseContext(t *testing.T) {\n\tserver := httptest.NewServer(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tw.Write([]byte(\"OK\"))\n\t\t}),\n\t)\n\tdefer server.Close()\n\ttargetURL, _ := url.Parse(server.URL)\n\te := echo.New()\n\te.Use(ProxyWithConfig(\n\t\tProxyConfig{\n\t\t\tBalancer: &customBalancer{\n\t\t\t\ttarget: &ProxyTarget{\n\t\t\t\t\tName: \"tst\",\n\t\t\t\t\tURL:  targetURL,\n\t\t\t\t},\n\t\t\t},\n\t\t\tRetryCount: 1,\n\t\t\tModifyResponse: func(res *http.Response) error {\n\t\t\t\tval := res.Request.Context().Value(testContextKey(\"FROM_BALANCER\"))\n\t\t\t\tif valStr, ok := val.(string); ok {\n\t\t\t\t\tres.Header.Set(\"FROM_BALANCER\", valStr)\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t))\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\tassert.Equal(t, http.StatusOK, rec.Code)\n\tassert.Equal(t, \"OK\", rec.Body.String())\n\tassert.Equal(t, \"CUSTOM_BALANCER\", rec.Header().Get(\"FROM_BALANCER\"))\n}\n\nfunc createSimpleWebSocketServer(serveTLS bool) *httptest.Server {\n\thandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\twsHandler := func(conn *websocket.Conn) {\n\t\t\tdefer conn.Close()\n\t\t\tfor {\n\t\t\t\tvar msg string\n\t\t\t\terr := websocket.Message.Receive(conn, &msg)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// message back to the client\n\t\t\t\twebsocket.Message.Send(conn, msg)\n\t\t\t}\n\t\t}\n\t\twebsocket.Server{Handler: wsHandler}.ServeHTTP(w, r)\n\t})\n\tif serveTLS {\n\t\treturn httptest.NewTLSServer(handler)\n\t}\n\treturn httptest.NewServer(handler)\n}\n\nfunc createSimpleProxyServer(t *testing.T, srv *httptest.Server, serveTLS bool, toTLS bool) *httptest.Server {\n\te := echo.New()\n\n\tif toTLS {\n\t\t// proxy to tls target\n\t\ttgtURL, _ := url.Parse(srv.URL)\n\t\ttgtURL.Scheme = \"wss\"\n\t\tbalancer := NewRandomBalancer([]*ProxyTarget{{URL: tgtURL}})\n\n\t\tdefaultTransport, ok := http.DefaultTransport.(*http.Transport)\n\t\tif !ok {\n\t\t\tt.Fatal(\"Default transport is not of type *http.Transport\")\n\t\t}\n\t\ttransport := defaultTransport.Clone()\n\t\ttransport.TLSClientConfig = &tls.Config{\n\t\t\tInsecureSkipVerify: true,\n\t\t}\n\t\te.Use(ProxyWithConfig(ProxyConfig{Balancer: balancer, Transport: transport}))\n\t} else {\n\t\t// proxy to non-TLS target\n\t\ttgtURL, _ := url.Parse(srv.URL)\n\t\tbalancer := NewRandomBalancer([]*ProxyTarget{{URL: tgtURL}})\n\t\te.Use(ProxyWithConfig(ProxyConfig{Balancer: balancer}))\n\t}\n\n\tif serveTLS {\n\t\t// serve proxy server with TLS\n\t\tts := httptest.NewTLSServer(e)\n\t\treturn ts\n\t}\n\t// serve proxy server without TLS\n\tts := httptest.NewServer(e)\n\treturn ts\n}\n\n// TestProxyWithConfigWebSocketNonTLS2NonTLS tests the proxy with non-TLS to non-TLS WebSocket connection.\nfunc TestProxyWithConfigWebSocketNonTLS2NonTLS(t *testing.T) {\n\t/*\n\t\tArrange\n\t*/\n\t// Create a WebSocket test server (non-TLS)\n\tsrv := createSimpleWebSocketServer(false)\n\tdefer srv.Close()\n\n\t// create proxy server (non-TLS to non-TLS)\n\tts := createSimpleProxyServer(t, srv, false, false)\n\tdefer ts.Close()\n\n\ttsURL, _ := url.Parse(ts.URL)\n\ttsURL.Scheme = \"ws\"\n\ttsURL.Path = \"/\"\n\n\t/*\n\t\tAct\n\t*/\n\n\t// Connect to the proxy WebSocket\n\twsConn, err := websocket.Dial(tsURL.String(), \"\", \"http://localhost/\")\n\tassert.NoError(t, err)\n\tdefer wsConn.Close()\n\n\t// Send message\n\tsendMsg := \"Hello, Non TLS WebSocket!\"\n\terr = websocket.Message.Send(wsConn, sendMsg)\n\tassert.NoError(t, err)\n\n\t/*\n\t\tAssert\n\t*/\n\t// Read response\n\tvar recvMsg string\n\terr = websocket.Message.Receive(wsConn, &recvMsg)\n\tassert.NoError(t, err)\n\tassert.Equal(t, sendMsg, recvMsg)\n}\n\n// TestProxyWithConfigWebSocketTLS2TLS tests the proxy with TLS to TLS WebSocket connection.\nfunc TestProxyWithConfigWebSocketTLS2TLS(t *testing.T) {\n\t/*\n\t\tArrange\n\t*/\n\t// Create a WebSocket test server (TLS)\n\tsrv := createSimpleWebSocketServer(true)\n\tdefer srv.Close()\n\n\t// create proxy server (TLS to TLS)\n\tts := createSimpleProxyServer(t, srv, true, true)\n\tdefer ts.Close()\n\n\ttsURL, _ := url.Parse(ts.URL)\n\ttsURL.Scheme = \"wss\"\n\ttsURL.Path = \"/\"\n\n\t/*\n\t\tAct\n\t*/\n\torigin, err := url.Parse(ts.URL)\n\tassert.NoError(t, err)\n\tconfig := &websocket.Config{\n\t\tLocation:  tsURL,\n\t\tOrigin:    origin,\n\t\tTlsConfig: &tls.Config{InsecureSkipVerify: true}, // skip verify for testing\n\t\tVersion:   websocket.ProtocolVersionHybi13,\n\t}\n\twsConn, err := websocket.DialConfig(config)\n\tassert.NoError(t, err)\n\tdefer wsConn.Close()\n\n\t// Send message\n\tsendMsg := \"Hello, TLS to TLS WebSocket!\"\n\terr = websocket.Message.Send(wsConn, sendMsg)\n\tassert.NoError(t, err)\n\n\t// Read response\n\tvar recvMsg string\n\terr = websocket.Message.Receive(wsConn, &recvMsg)\n\tassert.NoError(t, err)\n\tassert.Equal(t, sendMsg, recvMsg)\n}\n\n// TestProxyWithConfigWebSocketNonTLS2TLS tests the proxy with non-TLS to TLS WebSocket connection.\nfunc TestProxyWithConfigWebSocketNonTLS2TLS(t *testing.T) {\n\t/*\n\t\tArrange\n\t*/\n\n\t// Create a WebSocket test server (TLS)\n\tsrv := createSimpleWebSocketServer(true)\n\tdefer srv.Close()\n\n\t// create proxy server (Non-TLS to TLS)\n\tts := createSimpleProxyServer(t, srv, false, true)\n\tdefer ts.Close()\n\n\ttsURL, _ := url.Parse(ts.URL)\n\ttsURL.Scheme = \"ws\"\n\ttsURL.Path = \"/\"\n\n\t/*\n\t\tAct\n\t*/\n\t// Connect to the proxy WebSocket\n\twsConn, err := websocket.Dial(tsURL.String(), \"\", \"http://localhost/\")\n\tassert.NoError(t, err)\n\tdefer wsConn.Close()\n\n\t// Send message\n\tsendMsg := \"Hello, Non TLS to TLS WebSocket!\"\n\terr = websocket.Message.Send(wsConn, sendMsg)\n\tassert.NoError(t, err)\n\n\t/*\n\t\tAssert\n\t*/\n\t// Read response\n\tvar recvMsg string\n\terr = websocket.Message.Receive(wsConn, &recvMsg)\n\tassert.NoError(t, err)\n\tassert.Equal(t, sendMsg, recvMsg)\n}\n\n// TestProxyWithConfigWebSocketTLSToNoneTLS tests the proxy with TLS to non-TLS WebSocket connection. (TLS termination)\nfunc TestProxyWithConfigWebSocketTLS2NonTLS(t *testing.T) {\n\t/*\n\t\tArrange\n\t*/\n\n\t// Create a WebSocket test server (non-TLS)\n\tsrv := createSimpleWebSocketServer(false)\n\tdefer srv.Close()\n\n\t// create proxy server (TLS to non-TLS)\n\tts := createSimpleProxyServer(t, srv, true, false)\n\tdefer ts.Close()\n\n\ttsURL, _ := url.Parse(ts.URL)\n\ttsURL.Scheme = \"wss\"\n\ttsURL.Path = \"/\"\n\n\t/*\n\t\tAct\n\t*/\n\torigin, err := url.Parse(ts.URL)\n\tassert.NoError(t, err)\n\tconfig := &websocket.Config{\n\t\tLocation:  tsURL,\n\t\tOrigin:    origin,\n\t\tTlsConfig: &tls.Config{InsecureSkipVerify: true}, // skip verify for testing\n\t\tVersion:   websocket.ProtocolVersionHybi13,\n\t}\n\twsConn, err := websocket.DialConfig(config)\n\tassert.NoError(t, err)\n\tdefer wsConn.Close()\n\n\t// Send message\n\tsendMsg := \"Hello, TLS to NoneTLS WebSocket!\"\n\terr = websocket.Message.Send(wsConn, sendMsg)\n\tassert.NoError(t, err)\n\n\t// Read response\n\tvar recvMsg string\n\terr = websocket.Message.Receive(wsConn, &recvMsg)\n\tassert.NoError(t, err)\n\tassert.Equal(t, sendMsg, recvMsg)\n}\n"
  },
  {
    "path": "middleware/rate_limiter.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"errors\"\n\t\"math\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"golang.org/x/time/rate\"\n)\n\n// RateLimiterStore is the interface to be implemented by custom stores.\ntype RateLimiterStore interface {\n\tAllow(identifier string) (bool, error)\n}\n\n// RateLimiterConfig defines the configuration for the rate limiter\ntype RateLimiterConfig struct {\n\tSkipper    Skipper\n\tBeforeFunc BeforeFunc\n\t// IdentifierExtractor uses *echo.Context to extract the identifier for a visitor\n\tIdentifierExtractor Extractor\n\t// Store defines a store for the rate limiter\n\tStore RateLimiterStore\n\t// ErrorHandler provides a handler to be called when IdentifierExtractor returns an error\n\tErrorHandler func(c *echo.Context, err error) error\n\t// DenyHandler provides a handler to be called when RateLimiter denies access\n\tDenyHandler func(c *echo.Context, identifier string, err error) error\n}\n\n// Extractor is used to extract data from *echo.Context\ntype Extractor func(c *echo.Context) (string, error)\n\n// ErrRateLimitExceeded denotes an error raised when rate limit is exceeded\nvar ErrRateLimitExceeded = echo.NewHTTPError(http.StatusTooManyRequests, \"rate limit exceeded\")\n\n// ErrExtractorError denotes an error raised when extractor function is unsuccessful\nvar ErrExtractorError = echo.NewHTTPError(http.StatusForbidden, \"error while extracting identifier\")\n\n// DefaultRateLimiterConfig defines default values for RateLimiterConfig\nvar DefaultRateLimiterConfig = RateLimiterConfig{\n\tSkipper: DefaultSkipper,\n\tIdentifierExtractor: func(ctx *echo.Context) (string, error) {\n\t\tid := ctx.RealIP()\n\t\treturn id, nil\n\t},\n\tErrorHandler: func(c *echo.Context, err error) error {\n\t\treturn ErrExtractorError.Wrap(err)\n\t},\n\tDenyHandler: func(c *echo.Context, identifier string, err error) error {\n\t\treturn ErrRateLimitExceeded.Wrap(err)\n\t},\n}\n\n/*\nRateLimiter returns a rate limiting middleware\n\n\te := echo.New()\n\n\tlimiterStore := middleware.NewRateLimiterMemoryStore(20)\n\n\te.GET(\"/rate-limited\", func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}, RateLimiter(limiterStore))\n*/\nfunc RateLimiter(store RateLimiterStore) echo.MiddlewareFunc {\n\tconfig := DefaultRateLimiterConfig\n\tconfig.Store = store\n\n\treturn RateLimiterWithConfig(config)\n}\n\n/*\nRateLimiterWithConfig returns a rate limiting middleware\n\n\te := echo.New()\n\n\tconfig := middleware.RateLimiterConfig{\n\t\tSkipper: DefaultSkipper,\n\t\tStore: middleware.NewRateLimiterMemoryStore(\n\t\t\tmiddleware.RateLimiterMemoryStoreConfig{Rate: 10, Burst: 30, ExpiresIn: 3 * time.Minute}\n\t\t)\n\t\tIdentifierExtractor: func(ctx *echo.Context) (string, error) {\n\t\t\tid := ctx.RealIP()\n\t\t\treturn id, nil\n\t\t},\n\t\tErrorHandler: func(ctx *echo.Context, err error) error {\n\t\t\treturn context.JSON(http.StatusTooManyRequests, nil)\n\t\t},\n\t\tDenyHandler: func(ctx *echo.Context, identifier string, err error) error {\n\t\t\treturn context.JSON(http.StatusForbidden, nil)\n\t\t},\n\t}\n\n\te.GET(\"/rate-limited\", func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}, middleware.RateLimiterWithConfig(config))\n*/\nfunc RateLimiterWithConfig(config RateLimiterConfig) echo.MiddlewareFunc {\n\treturn toMiddlewareOrPanic(config)\n}\n\n// ToMiddleware converts RateLimiterConfig to middleware or returns an error for invalid configuration\nfunc (config RateLimiterConfig) ToMiddleware() (echo.MiddlewareFunc, error) {\n\tif config.Skipper == nil {\n\t\tconfig.Skipper = DefaultRateLimiterConfig.Skipper\n\t}\n\tif config.IdentifierExtractor == nil {\n\t\tconfig.IdentifierExtractor = DefaultRateLimiterConfig.IdentifierExtractor\n\t}\n\tif config.ErrorHandler == nil {\n\t\tconfig.ErrorHandler = DefaultRateLimiterConfig.ErrorHandler\n\t}\n\tif config.DenyHandler == nil {\n\t\tconfig.DenyHandler = DefaultRateLimiterConfig.DenyHandler\n\t}\n\tif config.Store == nil {\n\t\treturn nil, errors.New(\"echo rate limiter store configuration must be provided\")\n\t}\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) error {\n\t\t\tif config.Skipper(c) {\n\t\t\t\treturn next(c)\n\t\t\t}\n\t\t\tif config.BeforeFunc != nil {\n\t\t\t\tconfig.BeforeFunc(c)\n\t\t\t}\n\n\t\t\tidentifier, err := config.IdentifierExtractor(c)\n\t\t\tif err != nil {\n\t\t\t\treturn config.ErrorHandler(c, err)\n\t\t\t}\n\n\t\t\tif allow, allowErr := config.Store.Allow(identifier); !allow {\n\t\t\t\treturn config.DenyHandler(c, identifier, allowErr)\n\t\t\t}\n\t\t\treturn next(c)\n\t\t}\n\t}, nil\n}\n\n// RateLimiterMemoryStore is the built-in store implementation for RateLimiter\ntype RateLimiterMemoryStore struct {\n\tvisitors    map[string]*Visitor\n\tmutex       sync.Mutex\n\trate        float64 // for more info check out Limiter docs - https://pkg.go.dev/golang.org/x/time/rate#Limit\n\tburst       int\n\texpiresIn   time.Duration\n\tlastCleanup time.Time\n\n\ttimeNow func() time.Time\n}\n\n// Visitor signifies a unique user's limiter details\ntype Visitor struct {\n\t*rate.Limiter\n\tlastSeen time.Time\n}\n\n/*\nNewRateLimiterMemoryStore returns an instance of RateLimiterMemoryStore with\nthe provided rate (as req/s).\nfor more info check out Limiter docs - https://pkg.go.dev/golang.org/x/time/rate#Limit.\n\nBurst and ExpiresIn will be set to default values.\n\nNote that if the provided rate is a float number and Burst is zero, Burst will be treated as the rounded down value of the rate.\n\nExample (with 20 requests/sec):\n\n\tlimiterStore := middleware.NewRateLimiterMemoryStore(20)\n*/\nfunc NewRateLimiterMemoryStore(rateLimit float64) (store *RateLimiterMemoryStore) {\n\treturn NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{\n\t\tRate: rateLimit,\n\t})\n}\n\n/*\nNewRateLimiterMemoryStoreWithConfig returns an instance of RateLimiterMemoryStore\nwith the provided configuration. Rate must be provided. Burst will be set to the rounded down value of\nthe configured rate if not provided or set to 0.\n\nThe built-in memory store is usually capable for modest loads. For higher loads other\nstore implementations should be considered.\n\nCharacteristics:\n* Concurrency above 100 parallel requests may causes measurable lock contention\n* A high number of different IP addresses (above 16000) may be impacted by the internally used Go map\n* A high number of requests from a single IP address may cause lock contention\n\nExample:\n\n\tlimiterStore := middleware.NewRateLimiterMemoryStoreWithConfig(\n\t\tmiddleware.RateLimiterMemoryStoreConfig{Rate: 50, Burst: 200, ExpiresIn: 5 * time.Minute},\n\t)\n*/\nfunc NewRateLimiterMemoryStoreWithConfig(config RateLimiterMemoryStoreConfig) (store *RateLimiterMemoryStore) {\n\tstore = &RateLimiterMemoryStore{}\n\n\tstore.rate = config.Rate\n\tstore.burst = config.Burst\n\tstore.expiresIn = config.ExpiresIn\n\tif config.ExpiresIn == 0 {\n\t\tstore.expiresIn = DefaultRateLimiterMemoryStoreConfig.ExpiresIn\n\t}\n\tif config.Burst == 0 {\n\t\tstore.burst = int(math.Max(1, math.Ceil(float64(config.Rate))))\n\t}\n\tstore.visitors = make(map[string]*Visitor)\n\tstore.timeNow = time.Now\n\tstore.lastCleanup = store.timeNow()\n\treturn\n}\n\n// RateLimiterMemoryStoreConfig represents configuration for RateLimiterMemoryStore\ntype RateLimiterMemoryStoreConfig struct {\n\tRate      float64       // Rate of requests allowed to pass as req/s. For more info check out Limiter docs - https://pkg.go.dev/golang.org/x/time/rate#Limit.\n\tBurst     int           // Burst is maximum number of requests to pass at the same moment. It additionally allows a number of requests to pass when rate limit is reached.\n\tExpiresIn time.Duration // ExpiresIn is the duration after that a rate limiter is cleaned up\n}\n\n// DefaultRateLimiterMemoryStoreConfig provides default configuration values for RateLimiterMemoryStore\nvar DefaultRateLimiterMemoryStoreConfig = RateLimiterMemoryStoreConfig{\n\tExpiresIn: 3 * time.Minute,\n}\n\n// Allow implements RateLimiterStore.Allow\nfunc (store *RateLimiterMemoryStore) Allow(identifier string) (bool, error) {\n\tstore.mutex.Lock()\n\tlimiter, exists := store.visitors[identifier]\n\tif !exists {\n\t\tlimiter = new(Visitor)\n\t\tlimiter.Limiter = rate.NewLimiter(rate.Limit(store.rate), store.burst)\n\t\tstore.visitors[identifier] = limiter\n\t}\n\tnow := store.timeNow()\n\tlimiter.lastSeen = now\n\tif now.Sub(store.lastCleanup) > store.expiresIn {\n\t\tstore.cleanupStaleVisitors(now)\n\t}\n\tallowed := limiter.AllowN(now, 1)\n\tstore.mutex.Unlock()\n\treturn allowed, nil\n}\n\n/*\ncleanupStaleVisitors helps manage the size of the visitors map by removing stale records\nof users who haven't visited again after the configured expiry time has elapsed\n*/\nfunc (store *RateLimiterMemoryStore) cleanupStaleVisitors(now time.Time) {\n\tfor id, visitor := range store.visitors {\n\t\tif now.Sub(visitor.lastSeen) > store.expiresIn {\n\t\t\tdelete(store.visitors, id)\n\t\t}\n\t}\n\tstore.lastCleanup = now\n}\n"
  },
  {
    "path": "middleware/rate_limiter_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"errors\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"golang.org/x/time/rate\"\n)\n\nfunc TestRateLimiter(t *testing.T) {\n\te := echo.New()\n\n\thandler := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\n\tvar inMemoryStore = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 1, Burst: 3})\n\n\tmw := RateLimiterWithConfig(RateLimiterConfig{Store: inMemoryStore})\n\n\ttestCases := []struct {\n\t\tid        string\n\t\texpectErr string\n\t}{\n\t\t{id: \"127.0.0.1\"},\n\t\t{id: \"127.0.0.1\"},\n\t\t{id: \"127.0.0.1\"},\n\t\t{id: \"127.0.0.1\", expectErr: \"code=429, message=rate limit exceeded\"},\n\t\t{id: \"127.0.0.1\", expectErr: \"code=429, message=rate limit exceeded\"},\n\t\t{id: \"127.0.0.1\", expectErr: \"code=429, message=rate limit exceeded\"},\n\t\t{id: \"127.0.0.1\", expectErr: \"code=429, message=rate limit exceeded\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\treq.Header.Add(echo.HeaderXRealIP, tc.id)\n\n\t\trec := httptest.NewRecorder()\n\t\tc := e.NewContext(req, rec)\n\n\t\terr := mw(handler)(c)\n\t\tif tc.expectErr != \"\" {\n\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t} else {\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t}\n}\n\nfunc TestMustRateLimiterWithConfig_panicBehaviour(t *testing.T) {\n\tvar inMemoryStore = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 1, Burst: 3})\n\n\tassert.Panics(t, func() {\n\t\tRateLimiterWithConfig(RateLimiterConfig{})\n\t})\n\n\tassert.NotPanics(t, func() {\n\t\tRateLimiterWithConfig(RateLimiterConfig{Store: inMemoryStore})\n\t})\n}\n\nfunc TestRateLimiterWithConfig(t *testing.T) {\n\tvar inMemoryStore = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 1, Burst: 3})\n\n\te := echo.New()\n\n\thandler := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\n\tmw, err := RateLimiterConfig{\n\t\tIdentifierExtractor: func(c *echo.Context) (string, error) {\n\t\t\tid := c.Request().Header.Get(echo.HeaderXRealIP)\n\t\t\tif id == \"\" {\n\t\t\t\treturn \"\", errors.New(\"invalid identifier\")\n\t\t\t}\n\t\t\treturn id, nil\n\t\t},\n\t\tDenyHandler: func(ctx *echo.Context, identifier string, err error) error {\n\t\t\treturn ctx.JSON(http.StatusForbidden, nil)\n\t\t},\n\t\tErrorHandler: func(ctx *echo.Context, err error) error {\n\t\t\treturn ctx.JSON(http.StatusBadRequest, nil)\n\t\t},\n\t\tStore: inMemoryStore,\n\t}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tid   string\n\t\tcode int\n\t}{\n\t\t{\"127.0.0.1\", http.StatusOK},\n\t\t{\"127.0.0.1\", http.StatusOK},\n\t\t{\"127.0.0.1\", http.StatusOK},\n\t\t{\"127.0.0.1\", http.StatusForbidden},\n\t\t{\"\", http.StatusBadRequest},\n\t\t{\"127.0.0.1\", http.StatusForbidden},\n\t\t{\"127.0.0.1\", http.StatusForbidden},\n\t}\n\n\tfor _, tc := range testCases {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\treq.Header.Add(echo.HeaderXRealIP, tc.id)\n\n\t\trec := httptest.NewRecorder()\n\n\t\tc := e.NewContext(req, rec)\n\n\t\terr := mw(handler)(c)\n\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, tc.code, rec.Code)\n\t}\n}\n\nfunc TestRateLimiterWithConfig_defaultDenyHandler(t *testing.T) {\n\tvar inMemoryStore = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 1, Burst: 3})\n\n\te := echo.New()\n\n\thandler := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\n\tmw, err := RateLimiterConfig{\n\t\tIdentifierExtractor: func(c *echo.Context) (string, error) {\n\t\t\tid := c.Request().Header.Get(echo.HeaderXRealIP)\n\t\t\tif id == \"\" {\n\t\t\t\treturn \"\", errors.New(\"invalid identifier\")\n\t\t\t}\n\t\t\treturn id, nil\n\t\t},\n\t\tStore: inMemoryStore,\n\t}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tid        string\n\t\texpectErr string\n\t}{\n\t\t{id: \"127.0.0.1\"},\n\t\t{id: \"127.0.0.1\"},\n\t\t{id: \"127.0.0.1\"},\n\t\t{id: \"127.0.0.1\", expectErr: \"code=429, message=rate limit exceeded\"},\n\t\t{expectErr: \"code=403, message=error while extracting identifier, err=invalid identifier\"},\n\t\t{id: \"127.0.0.1\", expectErr: \"code=429, message=rate limit exceeded\"},\n\t\t{id: \"127.0.0.1\", expectErr: \"code=429, message=rate limit exceeded\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\treq.Header.Add(echo.HeaderXRealIP, tc.id)\n\n\t\trec := httptest.NewRecorder()\n\n\t\tc := e.NewContext(req, rec)\n\n\t\terr := mw(handler)(c)\n\t\tif tc.expectErr != \"\" {\n\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t} else {\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t}\n}\n\nfunc TestRateLimiterWithConfig_defaultConfig(t *testing.T) {\n\t{\n\t\tvar inMemoryStore = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 1, Burst: 3})\n\n\t\te := echo.New()\n\n\t\thandler := func(c *echo.Context) error {\n\t\t\treturn c.String(http.StatusOK, \"test\")\n\t\t}\n\n\t\tmw, err := RateLimiterConfig{\n\t\t\tStore: inMemoryStore,\n\t\t}.ToMiddleware()\n\t\tassert.NoError(t, err)\n\n\t\ttestCases := []struct {\n\t\t\tid        string\n\t\t\texpectErr string\n\t\t}{\n\t\t\t{id: \"127.0.0.1\"},\n\t\t\t{id: \"127.0.0.1\"},\n\t\t\t{id: \"127.0.0.1\"},\n\t\t\t{id: \"127.0.0.1\", expectErr: \"code=429, message=rate limit exceeded\"},\n\t\t\t{id: \"127.0.0.1\", expectErr: \"code=429, message=rate limit exceeded\"},\n\t\t\t{id: \"127.0.0.1\", expectErr: \"code=429, message=rate limit exceeded\"},\n\t\t\t{id: \"127.0.0.1\", expectErr: \"code=429, message=rate limit exceeded\"},\n\t\t}\n\n\t\tfor _, tc := range testCases {\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\t\treq.Header.Add(echo.HeaderXRealIP, tc.id)\n\n\t\t\trec := httptest.NewRecorder()\n\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\terr := mw(handler)(c)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\t}\n\t}\n}\n\nfunc TestRateLimiterWithConfig_skipper(t *testing.T) {\n\te := echo.New()\n\n\tvar beforeFuncRan bool\n\thandler := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\tvar inMemoryStore = NewRateLimiterMemoryStore(5)\n\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Add(echo.HeaderXRealIP, \"127.0.0.1\")\n\n\trec := httptest.NewRecorder()\n\n\tc := e.NewContext(req, rec)\n\n\tmw, err := RateLimiterConfig{\n\t\tSkipper: func(c *echo.Context) bool {\n\t\t\treturn true\n\t\t},\n\t\tBeforeFunc: func(c *echo.Context) {\n\t\t\tbeforeFuncRan = true\n\t\t},\n\t\tStore: inMemoryStore,\n\t\tIdentifierExtractor: func(ctx *echo.Context) (string, error) {\n\t\t\treturn \"127.0.0.1\", nil\n\t\t},\n\t}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\terr = mw(handler)(c)\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, false, beforeFuncRan)\n}\n\nfunc TestRateLimiterWithConfig_skipperNoSkip(t *testing.T) {\n\te := echo.New()\n\n\tvar beforeFuncRan bool\n\thandler := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\tvar inMemoryStore = NewRateLimiterMemoryStore(5)\n\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Add(echo.HeaderXRealIP, \"127.0.0.1\")\n\n\trec := httptest.NewRecorder()\n\n\tc := e.NewContext(req, rec)\n\n\tmw, err := RateLimiterConfig{\n\t\tSkipper: func(c *echo.Context) bool {\n\t\t\treturn false\n\t\t},\n\t\tBeforeFunc: func(c *echo.Context) {\n\t\t\tbeforeFuncRan = true\n\t\t},\n\t\tStore: inMemoryStore,\n\t\tIdentifierExtractor: func(ctx *echo.Context) (string, error) {\n\t\t\treturn \"127.0.0.1\", nil\n\t\t},\n\t}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\t_ = mw(handler)(c)\n\n\tassert.Equal(t, true, beforeFuncRan)\n}\n\nfunc TestRateLimiterWithConfig_beforeFunc(t *testing.T) {\n\te := echo.New()\n\n\thandler := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\n\tvar beforeRan bool\n\tvar inMemoryStore = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 1, Burst: 3})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Add(echo.HeaderXRealIP, \"127.0.0.1\")\n\n\trec := httptest.NewRecorder()\n\n\tc := e.NewContext(req, rec)\n\n\tmw, err := RateLimiterConfig{\n\t\tBeforeFunc: func(c *echo.Context) {\n\t\t\tbeforeRan = true\n\t\t},\n\t\tStore: inMemoryStore,\n\t\tIdentifierExtractor: func(ctx *echo.Context) (string, error) {\n\t\t\treturn \"127.0.0.1\", nil\n\t\t},\n\t}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\terr = mw(handler)(c)\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, true, beforeRan)\n}\n\nfunc TestRateLimiterMemoryStore_Allow(t *testing.T) {\n\tvar inMemoryStore = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 1, Burst: 3, ExpiresIn: 2 * time.Second})\n\ttestCases := []struct {\n\t\tid      string\n\t\tallowed bool\n\t}{\n\t\t{\"127.0.0.1\", true},  // 0 ms\n\t\t{\"127.0.0.1\", true},  // 220 ms burst #2\n\t\t{\"127.0.0.1\", true},  // 440 ms burst #3\n\t\t{\"127.0.0.1\", false}, // 660 ms block\n\t\t{\"127.0.0.1\", false}, // 880 ms block\n\t\t{\"127.0.0.1\", true},  // 1100 ms next second #1\n\t\t{\"127.0.0.2\", true},  // 1320 ms allow other ip\n\t\t{\"127.0.0.1\", false}, // 1540 ms no burst\n\t\t{\"127.0.0.1\", false}, // 1760 ms no burst\n\t\t{\"127.0.0.1\", false}, // 1980 ms no burst\n\t\t{\"127.0.0.1\", true},  // 2200 ms no burst\n\t\t{\"127.0.0.1\", false}, // 2420 ms no burst\n\t\t{\"127.0.0.1\", false}, // 2640 ms no burst\n\t\t{\"127.0.0.1\", false}, // 2860 ms no burst\n\t\t{\"127.0.0.1\", true},  // 3080 ms no burst\n\t\t{\"127.0.0.1\", false}, // 3300 ms no burst\n\t\t{\"127.0.0.1\", false}, // 3520 ms no burst\n\t\t{\"127.0.0.1\", false}, // 3740 ms no burst\n\t\t{\"127.0.0.1\", false}, // 3960 ms no burst\n\t\t{\"127.0.0.1\", true},  // 4180 ms no burst\n\t\t{\"127.0.0.1\", false}, // 4400 ms no burst\n\t\t{\"127.0.0.1\", false}, // 4620 ms no burst\n\t\t{\"127.0.0.1\", false}, // 4840 ms no burst\n\t\t{\"127.0.0.1\", true},  // 5060 ms no burst\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Logf(\"Running testcase #%d => %v\", i, time.Duration(i)*220*time.Millisecond)\n\t\tinMemoryStore.timeNow = func() time.Time {\n\t\t\treturn time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Add(time.Duration(i) * 220 * time.Millisecond)\n\t\t}\n\t\tallowed, _ := inMemoryStore.Allow(tc.id)\n\t\tassert.Equal(t, tc.allowed, allowed)\n\t}\n}\n\nfunc TestRateLimiterMemoryStore_cleanupStaleVisitors(t *testing.T) {\n\tvar inMemoryStore = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 1, Burst: 3})\n\tinMemoryStore.visitors = map[string]*Visitor{\n\t\t\"A\": {\n\t\t\tLimiter:  rate.NewLimiter(1, 3),\n\t\t\tlastSeen: time.Now(),\n\t\t},\n\t\t\"B\": {\n\t\t\tLimiter:  rate.NewLimiter(1, 3),\n\t\t\tlastSeen: time.Now().Add(-1 * time.Minute),\n\t\t},\n\t\t\"C\": {\n\t\t\tLimiter:  rate.NewLimiter(1, 3),\n\t\t\tlastSeen: time.Now().Add(-5 * time.Minute),\n\t\t},\n\t\t\"D\": {\n\t\t\tLimiter:  rate.NewLimiter(1, 3),\n\t\t\tlastSeen: time.Now().Add(-10 * time.Minute),\n\t\t},\n\t}\n\n\tinMemoryStore.Allow(\"D\")\n\tinMemoryStore.cleanupStaleVisitors(time.Now())\n\n\tvar exists bool\n\n\t_, exists = inMemoryStore.visitors[\"A\"]\n\tassert.Equal(t, true, exists)\n\n\t_, exists = inMemoryStore.visitors[\"B\"]\n\tassert.Equal(t, true, exists)\n\n\t_, exists = inMemoryStore.visitors[\"C\"]\n\tassert.Equal(t, false, exists)\n\n\t_, exists = inMemoryStore.visitors[\"D\"]\n\tassert.Equal(t, true, exists)\n}\n\nfunc TestNewRateLimiterMemoryStore(t *testing.T) {\n\ttestCases := []struct {\n\t\trate              float64\n\t\tburst             int\n\t\texpiresIn         time.Duration\n\t\texpectedExpiresIn time.Duration\n\t}{\n\t\t{1, 3, 5 * time.Second, 5 * time.Second},\n\t\t{2, 4, 0, 3 * time.Minute},\n\t\t{1, 5, 10 * time.Minute, 10 * time.Minute},\n\t\t{3, 7, 0, 3 * time.Minute},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tstore := NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: tc.rate, Burst: tc.burst, ExpiresIn: tc.expiresIn})\n\t\tassert.Equal(t, tc.rate, store.rate)\n\t\tassert.Equal(t, tc.burst, store.burst)\n\t\tassert.Equal(t, tc.expectedExpiresIn, store.expiresIn)\n\t}\n}\n\nfunc TestRateLimiterMemoryStore_FractionalRateDefaultBurst(t *testing.T) {\n\tstore := NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{\n\t\tRate: 0.5, // fractional rate should get a burst of at least 1\n\t})\n\n\tbase := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)\n\tstore.timeNow = func() time.Time {\n\t\treturn base\n\t}\n\n\tallowed, err := store.Allow(\"user\")\n\tassert.NoError(t, err)\n\tassert.True(t, allowed, \"first request should not be blocked\")\n\n\tallowed, err = store.Allow(\"user\")\n\tassert.NoError(t, err)\n\tassert.False(t, allowed, \"burst token should be consumed immediately\")\n\n\tstore.timeNow = func() time.Time {\n\t\treturn base.Add(2 * time.Second)\n\t}\n\n\tallowed, err = store.Allow(\"user\")\n\tassert.NoError(t, err)\n\tassert.True(t, allowed, \"token should refill for fractional rate after time passes\")\n}\n\nfunc generateAddressList(count int) []string {\n\taddrs := make([]string, count)\n\tfor i := 0; i < count; i++ {\n\t\taddrs[i] = randomString(15)\n\t}\n\treturn addrs\n}\n\nfunc run(wg *sync.WaitGroup, store RateLimiterStore, addrs []string, max int, b *testing.B) {\n\tfor i := 0; i < b.N; i++ {\n\t\tstore.Allow(addrs[rand.Intn(max)])\n\t}\n\twg.Done()\n}\n\nfunc benchmarkStore(store RateLimiterStore, parallel int, max int, b *testing.B) {\n\taddrs := generateAddressList(max)\n\twg := &sync.WaitGroup{}\n\tfor i := 0; i < parallel; i++ {\n\t\twg.Add(1)\n\t\tgo run(wg, store, addrs, max, b)\n\t}\n\twg.Wait()\n}\n\nconst (\n\ttestExpiresIn = 1000 * time.Millisecond\n)\n\nfunc BenchmarkRateLimiterMemoryStore_1000(b *testing.B) {\n\tvar store = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 100, Burst: 200, ExpiresIn: testExpiresIn})\n\tbenchmarkStore(store, 10, 1000, b)\n}\n\nfunc BenchmarkRateLimiterMemoryStore_10000(b *testing.B) {\n\tvar store = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 100, Burst: 200, ExpiresIn: testExpiresIn})\n\tbenchmarkStore(store, 10, 10000, b)\n}\n\nfunc BenchmarkRateLimiterMemoryStore_100000(b *testing.B) {\n\tvar store = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 100, Burst: 200, ExpiresIn: testExpiresIn})\n\tbenchmarkStore(store, 10, 100000, b)\n}\n\nfunc BenchmarkRateLimiterMemoryStore_conc100_10000(b *testing.B) {\n\tvar store = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 100, Burst: 200, ExpiresIn: testExpiresIn})\n\tbenchmarkStore(store, 100, 10000, b)\n}\n\n// TestRateLimiterMemoryStore_TOCTOUFix verifies that the TOCTOU race condition is fixed\n// by ensuring timeNow() is only called once per Allow() call\nfunc TestRateLimiterMemoryStore_TOCTOUFix(t *testing.T) {\n\tt.Parallel()\n\n\tstore := NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{\n\t\tRate:      1,\n\t\tBurst:     1,\n\t\tExpiresIn: 2 * time.Second,\n\t})\n\n\t// Track time calls to verify we use the same time value\n\ttimeCallCount := 0\n\tbaseTime := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)\n\n\tstore.timeNow = func() time.Time {\n\t\ttimeCallCount++\n\t\treturn baseTime\n\t}\n\n\t// First request - should succeed\n\tallowed, err := store.Allow(\"127.0.0.1\")\n\tassert.NoError(t, err)\n\tassert.True(t, allowed, \"First request should be allowed\")\n\n\t// Verify timeNow() was only called once\n\tassert.Equal(t, 1, timeCallCount, \"timeNow() should only be called once per Allow()\")\n}\n\n// TestRateLimiterMemoryStore_ConcurrentAccess verifies rate limiting correctness under concurrent load\nfunc TestRateLimiterMemoryStore_ConcurrentAccess(t *testing.T) {\n\tt.Parallel()\n\n\tstore := NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{\n\t\tRate:      10,\n\t\tBurst:     5,\n\t\tExpiresIn: 5 * time.Second,\n\t})\n\n\tconst goroutines = 50\n\tconst requestsPerGoroutine = 20\n\n\tvar wg sync.WaitGroup\n\tvar allowedCount, deniedCount int32\n\n\tfor i := 0; i < goroutines; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tfor j := 0; j < requestsPerGoroutine; j++ {\n\t\t\t\tallowed, err := store.Allow(\"test-user\")\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tif allowed {\n\t\t\t\t\tatomic.AddInt32(&allowedCount, 1)\n\t\t\t\t} else {\n\t\t\t\t\tatomic.AddInt32(&deniedCount, 1)\n\t\t\t\t}\n\t\t\t\ttime.Sleep(time.Millisecond)\n\t\t\t}\n\t\t}()\n\t}\n\n\twg.Wait()\n\n\ttotalRequests := goroutines * requestsPerGoroutine\n\tallowed := int(allowedCount)\n\tdenied := int(deniedCount)\n\n\tassert.Equal(t, totalRequests, allowed+denied, \"All requests should be processed\")\n\tassert.Greater(t, denied, 0, \"Some requests should be denied due to rate limiting\")\n\tassert.Greater(t, allowed, 0, \"Some requests should be allowed\")\n}\n\n// TestRateLimiterMemoryStore_RaceDetection verifies no data races with high concurrency\n// Run with: go test -race ./middleware -run TestRateLimiterMemoryStore_RaceDetection\nfunc TestRateLimiterMemoryStore_RaceDetection(t *testing.T) {\n\tt.Parallel()\n\n\tstore := NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{\n\t\tRate:      100,\n\t\tBurst:     200,\n\t\tExpiresIn: 1 * time.Second,\n\t})\n\n\tconst goroutines = 100\n\tconst requestsPerGoroutine = 100\n\n\tvar wg sync.WaitGroup\n\tidentifiers := []string{\"user1\", \"user2\", \"user3\", \"user4\", \"user5\"}\n\n\tfor i := 0; i < goroutines; i++ {\n\t\twg.Add(1)\n\t\tgo func(routineID int) {\n\t\t\tdefer wg.Done()\n\t\t\tfor j := 0; j < requestsPerGoroutine; j++ {\n\t\t\t\tidentifier := identifiers[routineID%len(identifiers)]\n\t\t\t\t_, err := store.Allow(identifier)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t}(i)\n\t}\n\n\twg.Wait()\n}\n\n// TestRateLimiterMemoryStore_TimeOrdering verifies time ordering consistency in rate limiting decisions\nfunc TestRateLimiterMemoryStore_TimeOrdering(t *testing.T) {\n\tt.Parallel()\n\n\tstore := NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{\n\t\tRate:      1,\n\t\tBurst:     2,\n\t\tExpiresIn: 5 * time.Second,\n\t})\n\n\tcurrentTime := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)\n\tstore.timeNow = func() time.Time {\n\t\treturn currentTime\n\t}\n\n\t// First two requests should succeed (burst=2)\n\tallowed1, _ := store.Allow(\"user1\")\n\tassert.True(t, allowed1, \"Request 1 should be allowed (burst)\")\n\n\tallowed2, _ := store.Allow(\"user1\")\n\tassert.True(t, allowed2, \"Request 2 should be allowed (burst)\")\n\n\t// Third request should be denied\n\tallowed3, _ := store.Allow(\"user1\")\n\tassert.False(t, allowed3, \"Request 3 should be denied (burst exhausted)\")\n\n\t// Advance time by 1 second\n\tcurrentTime = currentTime.Add(1 * time.Second)\n\n\t// Fourth request should succeed\n\tallowed4, _ := store.Allow(\"user1\")\n\tassert.True(t, allowed4, \"Request 4 should be allowed (1 token available)\")\n}\n"
  },
  {
    "path": "middleware/recover.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"runtime\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\n// RecoverConfig defines the config for Recover middleware.\ntype RecoverConfig struct {\n\t// Skipper defines a function to skip middleware.\n\tSkipper Skipper\n\n\t// Size of the stack to be printed.\n\t// Optional. Default value 4KB.\n\tStackSize int\n\n\t// DisableStackAll disables formatting stack traces of all other goroutines\n\t// into buffer after the trace for the current goroutine.\n\t// Optional. Default value false.\n\tDisableStackAll bool\n\n\t// DisablePrintStack disables printing stack trace.\n\t// Optional. Default value as false.\n\tDisablePrintStack bool\n}\n\n// DefaultRecoverConfig is the default Recover middleware config.\nvar DefaultRecoverConfig = RecoverConfig{\n\tSkipper:           DefaultSkipper,\n\tStackSize:         4 << 10, // 4 KB\n\tDisableStackAll:   false,\n\tDisablePrintStack: false,\n}\n\n// Recover returns a middleware which recovers from panics anywhere in the chain\n// and handles the control to the centralized HTTPErrorHandler.\nfunc Recover() echo.MiddlewareFunc {\n\treturn RecoverWithConfig(DefaultRecoverConfig)\n}\n\n// RecoverWithConfig returns a Recovery middleware with config or panics on invalid configuration.\nfunc RecoverWithConfig(config RecoverConfig) echo.MiddlewareFunc {\n\treturn toMiddlewareOrPanic(config)\n}\n\n// ToMiddleware converts RecoverConfig to middleware or returns an error for invalid configuration\nfunc (config RecoverConfig) ToMiddleware() (echo.MiddlewareFunc, error) {\n\t// Defaults\n\tif config.Skipper == nil {\n\t\tconfig.Skipper = DefaultRecoverConfig.Skipper\n\t}\n\tif config.StackSize == 0 {\n\t\tconfig.StackSize = DefaultRecoverConfig.StackSize\n\t}\n\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) (err error) {\n\t\t\tif config.Skipper(c) {\n\t\t\t\treturn next(c)\n\t\t\t}\n\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tif r == http.ErrAbortHandler {\n\t\t\t\t\t\tpanic(r)\n\t\t\t\t\t}\n\t\t\t\t\ttmpErr, ok := r.(error)\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\ttmpErr = fmt.Errorf(\"%v\", r)\n\t\t\t\t\t}\n\t\t\t\t\tif !config.DisablePrintStack {\n\t\t\t\t\t\tstack := make([]byte, config.StackSize)\n\t\t\t\t\t\tlength := runtime.Stack(stack, !config.DisableStackAll)\n\t\t\t\t\t\ttmpErr = &PanicStackError{Stack: stack[:length], Err: tmpErr}\n\t\t\t\t\t}\n\t\t\t\t\terr = tmpErr\n\t\t\t\t}\n\t\t\t}()\n\t\t\treturn next(c)\n\t\t}\n\t}, nil\n}\n\n// PanicStackError is an error type that wraps an error along with its stack trace.\n// It is returned when config.DisablePrintStack is set to false.\ntype PanicStackError struct {\n\tStack []byte\n\tErr   error\n}\n\nfunc (e *PanicStackError) Error() string {\n\treturn fmt.Sprintf(\"[PANIC RECOVER] %s %s\", e.Err.Error(), e.Stack)\n}\n\nfunc (e *PanicStackError) Unwrap() error {\n\treturn e.Err\n}\n"
  },
  {
    "path": "middleware/recover_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestRecover(t *testing.T) {\n\te := echo.New()\n\tbuf := new(bytes.Buffer)\n\te.Logger = slog.New(&discardHandler{})\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\th := Recover()(func(c *echo.Context) error {\n\t\tpanic(\"test\")\n\t})\n\terr := h(c)\n\tassert.Contains(t, err.Error(), \"[PANIC RECOVER] test goroutine\")\n\n\tvar pse *PanicStackError\n\tif errors.As(err, &pse) {\n\t\tassert.Contains(t, string(pse.Stack), \"middleware/recover.go\")\n\t} else {\n\t\tassert.Fail(t, \"not of type PanicStackError\")\n\t}\n\n\tassert.Equal(t, http.StatusOK, rec.Code) // status is still untouched. err is returned from middleware chain\n\tassert.Contains(t, buf.String(), \"\")     // nothing is logged\n}\n\nfunc TestRecover_skipper(t *testing.T) {\n\te := echo.New()\n\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\tconfig := RecoverConfig{\n\t\tSkipper: func(c *echo.Context) bool {\n\t\t\treturn true\n\t\t},\n\t}\n\th := RecoverWithConfig(config)(func(c *echo.Context) error {\n\t\tpanic(\"testPANIC\")\n\t})\n\n\tvar err error\n\tassert.Panics(t, func() {\n\t\terr = h(c)\n\t})\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, rec.Code) // status is still untouched. err is returned from middleware chain\n}\n\nfunc TestRecoverErrAbortHandler(t *testing.T) {\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\th := Recover()(func(c *echo.Context) error {\n\t\tpanic(http.ErrAbortHandler)\n\t})\n\tdefer func() {\n\t\tr := recover()\n\t\tif r == nil {\n\t\t\tassert.Fail(t, \"expecting `http.ErrAbortHandler`, got `nil`\")\n\t\t} else {\n\t\t\tif err, ok := r.(error); ok {\n\t\t\t\tassert.ErrorIs(t, err, http.ErrAbortHandler)\n\t\t\t} else {\n\t\t\t\tassert.Fail(t, \"not of error type\")\n\t\t\t}\n\t\t}\n\t}()\n\n\thErr := h(c)\n\n\tassert.Equal(t, http.StatusInternalServerError, rec.Code)\n\tassert.NotContains(t, hErr.Error(), \"PANIC RECOVER\")\n}\n\nfunc TestRecoverWithConfig(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname             string\n\t\tgivenNoPanic     bool\n\t\twhenConfig       RecoverConfig\n\t\texpectErrContain string\n\t\texpectErr        string\n\t}{\n\t\t{\n\t\t\tname:             \"ok, default config\",\n\t\t\twhenConfig:       DefaultRecoverConfig,\n\t\t\texpectErrContain: \"[PANIC RECOVER] testPANIC goroutine\",\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, no panic\",\n\t\t\tgivenNoPanic:     true,\n\t\t\twhenConfig:       DefaultRecoverConfig,\n\t\t\texpectErrContain: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"ok, DisablePrintStack\",\n\t\t\twhenConfig: RecoverConfig{\n\t\t\t\tDisablePrintStack: true,\n\t\t\t},\n\t\t\texpectErr: \"testPANIC\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := echo.New()\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\t\trec := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\tconfig := tc.whenConfig\n\t\t\th := RecoverWithConfig(config)(func(c *echo.Context) error {\n\t\t\t\tif tc.givenNoPanic {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tpanic(\"testPANIC\")\n\t\t\t})\n\n\t\t\terr := h(c)\n\n\t\t\tif tc.expectErrContain != \"\" {\n\t\t\t\tassert.Contains(t, err.Error(), tc.expectErrContain)\n\t\t\t} else if tc.expectErr != \"\" {\n\t\t\t\tassert.Contains(t, err.Error(), tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, http.StatusOK, rec.Code) // status is still untouched. err is returned from middleware chain\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "middleware/redirect.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\n// RedirectConfig defines the config for Redirect middleware.\ntype RedirectConfig struct {\n\t// Skipper defines a function to skip middleware.\n\tSkipper\n\n\t// Status code to be used when redirecting the request.\n\t// Optional. Default value http.StatusMovedPermanently.\n\tCode int\n\n\tredirect redirectLogic\n}\n\n// redirectLogic represents a function that given a scheme, host and uri\n// can both: 1) determine if redirect is needed (will set ok accordingly) and\n// 2) return the appropriate redirect url.\ntype redirectLogic func(scheme, host, uri string) (ok bool, url string)\n\nconst www = \"www.\"\n\n// RedirectHTTPSConfig is the HTTPS Redirect middleware config.\nvar RedirectHTTPSConfig = RedirectConfig{redirect: redirectHTTPS}\n\n// RedirectHTTPSWWWConfig is the HTTPS WWW Redirect middleware config.\nvar RedirectHTTPSWWWConfig = RedirectConfig{redirect: redirectHTTPSWWW}\n\n// RedirectNonHTTPSWWWConfig is the non HTTPS WWW Redirect middleware config.\nvar RedirectNonHTTPSWWWConfig = RedirectConfig{redirect: redirectNonHTTPSWWW}\n\n// RedirectWWWConfig is the WWW Redirect middleware config.\nvar RedirectWWWConfig = RedirectConfig{redirect: redirectWWW}\n\n// RedirectNonWWWConfig is the non WWW Redirect middleware config.\nvar RedirectNonWWWConfig = RedirectConfig{redirect: redirectNonWWW}\n\n// HTTPSRedirect redirects http requests to https.\n// For example, http://labstack.com will be redirect to https://labstack.com.\n//\n// Usage `Echo#Pre(HTTPSRedirect())`\nfunc HTTPSRedirect() echo.MiddlewareFunc {\n\treturn HTTPSRedirectWithConfig(RedirectHTTPSConfig)\n}\n\n// HTTPSRedirectWithConfig returns a HTTPS redirect middleware with config or panics on invalid configuration.\nfunc HTTPSRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {\n\tconfig.redirect = redirectHTTPS\n\treturn toMiddlewareOrPanic(config)\n}\n\n// HTTPSWWWRedirect redirects http requests to https www.\n// For example, http://labstack.com will be redirect to https://www.labstack.com.\n//\n// Usage `Echo#Pre(HTTPSWWWRedirect())`\nfunc HTTPSWWWRedirect() echo.MiddlewareFunc {\n\treturn HTTPSWWWRedirectWithConfig(RedirectHTTPSWWWConfig)\n}\n\n// HTTPSWWWRedirectWithConfig returns a HTTPS WWW redirect middleware with config or panics on invalid configuration.\nfunc HTTPSWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {\n\tconfig.redirect = redirectHTTPSWWW\n\treturn toMiddlewareOrPanic(config)\n}\n\n// HTTPSNonWWWRedirect redirects http requests to https non www.\n// For example, http://www.labstack.com will be redirect to https://labstack.com.\n//\n// Usage `Echo#Pre(HTTPSNonWWWRedirect())`\nfunc HTTPSNonWWWRedirect() echo.MiddlewareFunc {\n\treturn HTTPSNonWWWRedirectWithConfig(RedirectNonHTTPSWWWConfig)\n}\n\n// HTTPSNonWWWRedirectWithConfig returns a HTTPS Non-WWW redirect middleware with config or panics on invalid configuration.\nfunc HTTPSNonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {\n\tconfig.redirect = redirectNonHTTPSWWW\n\treturn toMiddlewareOrPanic(config)\n}\n\n// WWWRedirect redirects non www requests to www.\n// For example, http://labstack.com will be redirect to http://www.labstack.com.\n//\n// Usage `Echo#Pre(WWWRedirect())`\nfunc WWWRedirect() echo.MiddlewareFunc {\n\treturn WWWRedirectWithConfig(RedirectWWWConfig)\n}\n\n// WWWRedirectWithConfig returns a WWW redirect middleware with config or panics on invalid configuration.\nfunc WWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {\n\tconfig.redirect = redirectWWW\n\treturn toMiddlewareOrPanic(config)\n}\n\n// NonWWWRedirect redirects www requests to non www.\n// For example, http://www.labstack.com will be redirect to http://labstack.com.\n//\n// Usage `Echo#Pre(NonWWWRedirect())`\nfunc NonWWWRedirect() echo.MiddlewareFunc {\n\treturn NonWWWRedirectWithConfig(RedirectNonWWWConfig)\n}\n\n// NonWWWRedirectWithConfig returns a Non-WWW redirect middleware with config or panics on invalid configuration.\nfunc NonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {\n\tconfig.redirect = redirectNonWWW\n\treturn toMiddlewareOrPanic(config)\n}\n\n// ToMiddleware converts RedirectConfig to middleware or returns an error for invalid configuration\nfunc (config RedirectConfig) ToMiddleware() (echo.MiddlewareFunc, error) {\n\tif config.Skipper == nil {\n\t\tconfig.Skipper = DefaultSkipper\n\t}\n\tif config.Code == 0 {\n\t\tconfig.Code = http.StatusMovedPermanently\n\t}\n\tif config.redirect == nil {\n\t\treturn nil, errors.New(\"redirectConfig is missing redirect function\")\n\t}\n\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) error {\n\t\t\tif config.Skipper(c) {\n\t\t\t\treturn next(c)\n\t\t\t}\n\n\t\t\treq, scheme := c.Request(), c.Scheme()\n\t\t\thost := req.Host\n\t\t\tif ok, url := config.redirect(scheme, host, req.RequestURI); ok {\n\t\t\t\treturn c.Redirect(config.Code, url)\n\t\t\t}\n\n\t\t\treturn next(c)\n\t\t}\n\t}, nil\n}\n\nvar redirectHTTPS = func(scheme, host, uri string) (bool, string) {\n\tif scheme != \"https\" {\n\t\treturn true, \"https://\" + host + uri\n\t}\n\treturn false, \"\"\n}\n\nvar redirectHTTPSWWW = func(scheme, host, uri string) (bool, string) {\n\t// Redirect if not HTTPS OR missing www prefix (needs either fix)\n\tif scheme != \"https\" || !strings.HasPrefix(host, www) {\n\t\thost = strings.TrimPrefix(host, www) // Remove www if present to avoid duplication\n\t\treturn true, \"https://www.\" + host + uri\n\t}\n\treturn false, \"\"\n}\n\nvar redirectNonHTTPSWWW = func(scheme, host, uri string) (ok bool, url string) {\n\t// Redirect if not HTTPS OR has www prefix (needs either fix)\n\tif scheme != \"https\" || strings.HasPrefix(host, www) {\n\t\thost = strings.TrimPrefix(host, www)\n\t\treturn true, \"https://\" + host + uri\n\t}\n\treturn false, \"\"\n}\n\nvar redirectWWW = func(scheme, host, uri string) (bool, string) {\n\tif !strings.HasPrefix(host, www) {\n\t\treturn true, scheme + \"://www.\" + host + uri\n\t}\n\treturn false, \"\"\n}\n\nvar redirectNonWWW = func(scheme, host, uri string) (bool, string) {\n\tif strings.HasPrefix(host, www) {\n\t\treturn true, scheme + \"://\" + host[4:] + uri\n\t}\n\treturn false, \"\"\n}\n"
  },
  {
    "path": "middleware/redirect_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype middlewareGenerator func() echo.MiddlewareFunc\n\nfunc TestRedirectHTTPSRedirect(t *testing.T) {\n\tvar testCases = []struct {\n\t\twhenHost         string\n\t\twhenHeader       http.Header\n\t\texpectLocation   string\n\t\texpectStatusCode int\n\t}{\n\t\t{\n\t\t\twhenHost:         \"labstack.com\",\n\t\t\texpectLocation:   \"https://labstack.com/\",\n\t\t\texpectStatusCode: http.StatusMovedPermanently,\n\t\t},\n\t\t{\n\t\t\twhenHost:         \"labstack.com\",\n\t\t\twhenHeader:       map[string][]string{echo.HeaderXForwardedProto: {\"https\"}},\n\t\t\texpectLocation:   \"\",\n\t\t\texpectStatusCode: http.StatusOK,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenHost, func(t *testing.T) {\n\t\t\tres := redirectTest(HTTPSRedirect, tc.whenHost, tc.whenHeader)\n\n\t\t\tassert.Equal(t, tc.expectStatusCode, res.Code)\n\t\t\tassert.Equal(t, tc.expectLocation, res.Header().Get(echo.HeaderLocation))\n\t\t})\n\t}\n}\n\nfunc TestRedirectHTTPSWWWRedirect(t *testing.T) {\n\tvar testCases = []struct {\n\t\twhenHost         string\n\t\twhenHeader       http.Header\n\t\texpectLocation   string\n\t\texpectStatusCode int\n\t}{\n\t\t{\n\t\t\twhenHost:         \"labstack.com\",\n\t\t\texpectLocation:   \"https://www.labstack.com/\",\n\t\t\texpectStatusCode: http.StatusMovedPermanently,\n\t\t},\n\t\t{\n\t\t\twhenHost:         \"www.labstack.com\",\n\t\t\texpectLocation:   \"https://www.labstack.com/\",\n\t\t\texpectStatusCode: http.StatusMovedPermanently,\n\t\t},\n\t\t{\n\t\t\twhenHost:         \"a.com\",\n\t\t\texpectLocation:   \"https://www.a.com/\",\n\t\t\texpectStatusCode: http.StatusMovedPermanently,\n\t\t},\n\t\t{\n\t\t\twhenHost:         \"ip\",\n\t\t\texpectLocation:   \"https://www.ip/\",\n\t\t\texpectStatusCode: http.StatusMovedPermanently,\n\t\t},\n\t\t{\n\t\t\twhenHost:         \"labstack.com\",\n\t\t\twhenHeader:       map[string][]string{echo.HeaderXForwardedProto: {\"https\"}},\n\t\t\texpectLocation:   \"https://www.labstack.com/\",\n\t\t\texpectStatusCode: http.StatusMovedPermanently,\n\t\t},\n\t\t{\n\t\t\twhenHost:         \"www.labstack.com\",\n\t\t\twhenHeader:       map[string][]string{echo.HeaderXForwardedProto: {\"https\"}},\n\t\t\texpectLocation:   \"\",\n\t\t\texpectStatusCode: http.StatusOK,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenHost, func(t *testing.T) {\n\t\t\tres := redirectTest(HTTPSWWWRedirect, tc.whenHost, tc.whenHeader)\n\n\t\t\tassert.Equal(t, tc.expectStatusCode, res.Code)\n\t\t\tassert.Equal(t, tc.expectLocation, res.Header().Get(echo.HeaderLocation))\n\t\t})\n\t}\n}\n\nfunc TestRedirectHTTPSNonWWWRedirect(t *testing.T) {\n\tvar testCases = []struct {\n\t\twhenHost         string\n\t\twhenHeader       http.Header\n\t\texpectLocation   string\n\t\texpectStatusCode int\n\t}{\n\t\t{\n\t\t\twhenHost:         \"www.labstack.com\",\n\t\t\texpectLocation:   \"https://labstack.com/\",\n\t\t\texpectStatusCode: http.StatusMovedPermanently,\n\t\t},\n\t\t{\n\t\t\twhenHost:         \"a.com\",\n\t\t\texpectLocation:   \"https://a.com/\",\n\t\t\texpectStatusCode: http.StatusMovedPermanently,\n\t\t},\n\t\t{\n\t\t\twhenHost:         \"ip\",\n\t\t\texpectLocation:   \"https://ip/\",\n\t\t\texpectStatusCode: http.StatusMovedPermanently,\n\t\t},\n\t\t{\n\t\t\twhenHost:         \"www.labstack.com\",\n\t\t\twhenHeader:       map[string][]string{echo.HeaderXForwardedProto: {\"https\"}},\n\t\t\texpectLocation:   \"https://labstack.com/\",\n\t\t\texpectStatusCode: http.StatusMovedPermanently,\n\t\t},\n\t\t{\n\t\t\twhenHost:         \"labstack.com\",\n\t\t\twhenHeader:       map[string][]string{echo.HeaderXForwardedProto: {\"https\"}},\n\t\t\texpectLocation:   \"\",\n\t\t\texpectStatusCode: http.StatusOK,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenHost, func(t *testing.T) {\n\t\t\tres := redirectTest(HTTPSNonWWWRedirect, tc.whenHost, tc.whenHeader)\n\n\t\t\tassert.Equal(t, tc.expectStatusCode, res.Code)\n\t\t\tassert.Equal(t, tc.expectLocation, res.Header().Get(echo.HeaderLocation))\n\t\t})\n\t}\n}\n\nfunc TestRedirectWWWRedirect(t *testing.T) {\n\tvar testCases = []struct {\n\t\twhenHost         string\n\t\twhenHeader       http.Header\n\t\texpectLocation   string\n\t\texpectStatusCode int\n\t}{\n\t\t{\n\t\t\twhenHost:         \"labstack.com\",\n\t\t\texpectLocation:   \"http://www.labstack.com/\",\n\t\t\texpectStatusCode: http.StatusMovedPermanently,\n\t\t},\n\t\t{\n\t\t\twhenHost:         \"a.com\",\n\t\t\texpectLocation:   \"http://www.a.com/\",\n\t\t\texpectStatusCode: http.StatusMovedPermanently,\n\t\t},\n\t\t{\n\t\t\twhenHost:         \"ip\",\n\t\t\texpectLocation:   \"http://www.ip/\",\n\t\t\texpectStatusCode: http.StatusMovedPermanently,\n\t\t},\n\t\t{\n\t\t\twhenHost:         \"a.com\",\n\t\t\twhenHeader:       map[string][]string{echo.HeaderXForwardedProto: {\"https\"}},\n\t\t\texpectLocation:   \"https://www.a.com/\",\n\t\t\texpectStatusCode: http.StatusMovedPermanently,\n\t\t},\n\t\t{\n\t\t\twhenHost:         \"www.ip\",\n\t\t\texpectLocation:   \"\",\n\t\t\texpectStatusCode: http.StatusOK,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenHost, func(t *testing.T) {\n\t\t\tres := redirectTest(WWWRedirect, tc.whenHost, tc.whenHeader)\n\n\t\t\tassert.Equal(t, tc.expectStatusCode, res.Code)\n\t\t\tassert.Equal(t, tc.expectLocation, res.Header().Get(echo.HeaderLocation))\n\t\t})\n\t}\n}\n\nfunc TestRedirectNonWWWRedirect(t *testing.T) {\n\tvar testCases = []struct {\n\t\twhenHost         string\n\t\twhenHeader       http.Header\n\t\texpectLocation   string\n\t\texpectStatusCode int\n\t}{\n\t\t{\n\t\t\twhenHost:         \"www.labstack.com\",\n\t\t\texpectLocation:   \"http://labstack.com/\",\n\t\t\texpectStatusCode: http.StatusMovedPermanently,\n\t\t},\n\t\t{\n\t\t\twhenHost:         \"www.a.com\",\n\t\t\texpectLocation:   \"http://a.com/\",\n\t\t\texpectStatusCode: http.StatusMovedPermanently,\n\t\t},\n\t\t{\n\t\t\twhenHost:         \"www.a.com\",\n\t\t\twhenHeader:       map[string][]string{echo.HeaderXForwardedProto: {\"https\"}},\n\t\t\texpectLocation:   \"https://a.com/\",\n\t\t\texpectStatusCode: http.StatusMovedPermanently,\n\t\t},\n\t\t{\n\t\t\twhenHost:         \"ip\",\n\t\t\texpectLocation:   \"\",\n\t\t\texpectStatusCode: http.StatusOK,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenHost, func(t *testing.T) {\n\t\t\tres := redirectTest(NonWWWRedirect, tc.whenHost, tc.whenHeader)\n\n\t\t\tassert.Equal(t, tc.expectStatusCode, res.Code)\n\t\t\tassert.Equal(t, tc.expectLocation, res.Header().Get(echo.HeaderLocation))\n\t\t})\n\t}\n}\n\nfunc TestNonWWWRedirectWithConfig(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname             string\n\t\tgivenCode        int\n\t\tgivenSkipFunc    func(c *echo.Context) bool\n\t\twhenHost         string\n\t\twhenHeader       http.Header\n\t\texpectLocation   string\n\t\texpectStatusCode int\n\t}{\n\t\t{\n\t\t\tname:             \"usual redirect\",\n\t\t\twhenHost:         \"www.labstack.com\",\n\t\t\texpectLocation:   \"http://labstack.com/\",\n\t\t\texpectStatusCode: http.StatusMovedPermanently,\n\t\t},\n\t\t{\n\t\t\tname: \"redirect is skipped\",\n\t\t\tgivenSkipFunc: func(c *echo.Context) bool {\n\t\t\t\treturn true // skip always\n\t\t\t},\n\t\t\twhenHost:         \"www.labstack.com\",\n\t\t\texpectLocation:   \"\",\n\t\t\texpectStatusCode: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:             \"redirect with custom status code\",\n\t\t\tgivenCode:        http.StatusSeeOther,\n\t\t\twhenHost:         \"www.labstack.com\",\n\t\t\texpectLocation:   \"http://labstack.com/\",\n\t\t\texpectStatusCode: http.StatusSeeOther,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenHost, func(t *testing.T) {\n\t\t\tmiddleware := func() echo.MiddlewareFunc {\n\t\t\t\treturn NonWWWRedirectWithConfig(RedirectConfig{\n\t\t\t\t\tSkipper: tc.givenSkipFunc,\n\t\t\t\t\tCode:    tc.givenCode,\n\t\t\t\t})\n\t\t\t}\n\t\t\tres := redirectTest(middleware, tc.whenHost, tc.whenHeader)\n\n\t\t\tassert.Equal(t, tc.expectStatusCode, res.Code)\n\t\t\tassert.Equal(t, tc.expectLocation, res.Header().Get(echo.HeaderLocation))\n\t\t})\n\t}\n}\n\nfunc redirectTest(fn middlewareGenerator, host string, header http.Header) *httptest.ResponseRecorder {\n\te := echo.New()\n\tnext := func(c *echo.Context) (err error) {\n\t\treturn c.NoContent(http.StatusOK)\n\t}\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Host = host\n\tif header != nil {\n\t\treq.Header = header\n\t}\n\tres := httptest.NewRecorder()\n\tc := e.NewContext(req, res)\n\n\tfn()(next)(c)\n\n\treturn res\n}\n"
  },
  {
    "path": "middleware/request_id.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"github.com/labstack/echo/v5\"\n)\n\n// RequestIDConfig defines the config for RequestID middleware.\ntype RequestIDConfig struct {\n\t// Skipper defines a function to skip middleware.\n\tSkipper Skipper\n\n\t// Generator defines a function to generate an ID.\n\t// Optional. Default value random.String(32).\n\tGenerator func() string\n\n\t// RequestIDHandler defines a function which is executed for a request id.\n\tRequestIDHandler func(c *echo.Context, requestID string)\n\n\t// TargetHeader defines what header to look for to populate the id.\n\t// Optional. Default value is `X-Request-Id`\n\tTargetHeader string\n}\n\n// RequestID returns a middleware that reads RequestIDConfig.TargetHeader (`X-Request-ID`) header value or when\n// the header value is empty, generates that value and sets request ID to response\n// as RequestIDConfig.TargetHeader (`X-Request-Id`) value.\nfunc RequestID() echo.MiddlewareFunc {\n\treturn RequestIDWithConfig(RequestIDConfig{})\n}\n\n// RequestIDWithConfig returns a middleware with given valid config or panics on invalid configuration.\n// The middleware reads RequestIDConfig.TargetHeader (`X-Request-ID`) header value or when the header value is empty,\n// generates that value and sets request ID to response as RequestIDConfig.TargetHeader (`X-Request-Id`) value.\nfunc RequestIDWithConfig(config RequestIDConfig) echo.MiddlewareFunc {\n\treturn toMiddlewareOrPanic(config)\n}\n\n// ToMiddleware converts RequestIDConfig to middleware or returns an error for invalid configuration\nfunc (config RequestIDConfig) ToMiddleware() (echo.MiddlewareFunc, error) {\n\tif config.Skipper == nil {\n\t\tconfig.Skipper = DefaultSkipper\n\t}\n\tif config.Generator == nil {\n\t\tconfig.Generator = createRandomStringGenerator(32)\n\t}\n\tif config.TargetHeader == \"\" {\n\t\tconfig.TargetHeader = echo.HeaderXRequestID\n\t}\n\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) error {\n\t\t\tif config.Skipper(c) {\n\t\t\t\treturn next(c)\n\t\t\t}\n\n\t\t\treq := c.Request()\n\t\t\tres := c.Response()\n\t\t\trid := req.Header.Get(config.TargetHeader)\n\t\t\tif rid == \"\" {\n\t\t\t\trid = config.Generator()\n\t\t\t}\n\t\t\tres.Header().Set(config.TargetHeader, rid)\n\t\t\tif config.RequestIDHandler != nil {\n\t\t\t\tconfig.RequestIDHandler(c, rid)\n\t\t\t}\n\n\t\t\treturn next(c)\n\t\t}\n\t}, nil\n}\n"
  },
  {
    "path": "middleware/request_id_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestRequestID(t *testing.T) {\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\thandler := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\n\trid := RequestID()\n\th := rid(handler)\n\terr := h(c)\n\tassert.NoError(t, err)\n\tassert.Len(t, rec.Header().Get(echo.HeaderXRequestID), 32)\n}\n\nfunc TestMustRequestIDWithConfig_skipper(t *testing.T) {\n\te := echo.New()\n\te.GET(\"/\", func(c *echo.Context) error {\n\t\treturn c.String(http.StatusTeapot, \"test\")\n\t})\n\n\tgeneratorCalled := false\n\te.Use(RequestIDWithConfig(RequestIDConfig{\n\t\tSkipper: func(c *echo.Context) bool {\n\t\t\treturn true\n\t\t},\n\t\tGenerator: func() string {\n\t\t\tgeneratorCalled = true\n\t\t\treturn \"customGenerator\"\n\t\t},\n\t}))\n\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\tres := httptest.NewRecorder()\n\te.ServeHTTP(res, req)\n\n\tassert.Equal(t, http.StatusTeapot, res.Code)\n\tassert.Equal(t, \"test\", res.Body.String())\n\n\tassert.Equal(t, res.Header().Get(echo.HeaderXRequestID), \"\")\n\tassert.False(t, generatorCalled)\n}\n\nfunc TestMustRequestIDWithConfig_customGenerator(t *testing.T) {\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\thandler := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\n\trid := RequestIDWithConfig(RequestIDConfig{\n\t\tGenerator: func() string { return \"customGenerator\" },\n\t})\n\th := rid(handler)\n\terr := h(c)\n\tassert.NoError(t, err)\n\tassert.Equal(t, rec.Header().Get(echo.HeaderXRequestID), \"customGenerator\")\n}\n\nfunc TestMustRequestIDWithConfig_RequestIDHandler(t *testing.T) {\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\thandler := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\n\tcalled := false\n\trid := RequestIDWithConfig(RequestIDConfig{\n\t\tGenerator: func() string { return \"customGenerator\" },\n\t\tRequestIDHandler: func(c *echo.Context, s string) {\n\t\t\tcalled = true\n\t\t},\n\t})\n\th := rid(handler)\n\terr := h(c)\n\tassert.NoError(t, err)\n\tassert.Equal(t, rec.Header().Get(echo.HeaderXRequestID), \"customGenerator\")\n\tassert.True(t, called)\n}\n\nfunc TestRequestIDWithConfig(t *testing.T) {\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\thandler := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\n\trid, err := RequestIDConfig{}.ToMiddleware()\n\tassert.NoError(t, err)\n\th := rid(handler)\n\th(c)\n\tassert.Len(t, rec.Header().Get(echo.HeaderXRequestID), 32)\n\n\t// Custom generator\n\trid = RequestIDWithConfig(RequestIDConfig{\n\t\tGenerator: func() string { return \"customGenerator\" },\n\t})\n\th = rid(handler)\n\th(c)\n\tassert.Equal(t, rec.Header().Get(echo.HeaderXRequestID), \"customGenerator\")\n}\n\nfunc TestRequestID_IDNotAltered(t *testing.T) {\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Add(echo.HeaderXRequestID, \"<sample-request-id>\")\n\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\thandler := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\n\trid := RequestIDWithConfig(RequestIDConfig{})\n\th := rid(handler)\n\t_ = h(c)\n\tassert.Equal(t, rec.Header().Get(echo.HeaderXRequestID), \"<sample-request-id>\")\n}\n\nfunc TestRequestIDConfigDifferentHeader(t *testing.T) {\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\thandler := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\n\trid := RequestIDWithConfig(RequestIDConfig{TargetHeader: echo.HeaderXCorrelationID})\n\th := rid(handler)\n\th(c)\n\tassert.Len(t, rec.Header().Get(echo.HeaderXCorrelationID), 32)\n\n\t// Custom generator and handler\n\tcustomID := \"customGenerator\"\n\tcalledHandler := false\n\trid = RequestIDWithConfig(RequestIDConfig{\n\t\tGenerator:    func() string { return customID },\n\t\tTargetHeader: echo.HeaderXCorrelationID,\n\t\tRequestIDHandler: func(_ *echo.Context, id string) {\n\t\t\tcalledHandler = true\n\t\t\tassert.Equal(t, customID, id)\n\t\t},\n\t})\n\th = rid(handler)\n\th(c)\n\tassert.Equal(t, rec.Header().Get(echo.HeaderXCorrelationID), \"customGenerator\")\n\tassert.True(t, calledHandler)\n}\n"
  },
  {
    "path": "middleware/request_logger.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\n// Example for `slog` https://pkg.go.dev/log/slog\n// \tlogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))\n//\te.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{\n//\t\tLogStatus:   true,\n//\t\tLogURI:      true,\n//\t\tHandleError: true, // forwards error to the global error handler, so it can decide appropriate status code\n//\t\tLogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error {\n//\t\t\tif v.Error == nil {\n//\t\t\t\tlogger.LogAttrs(context.Background(), slog.LevelInfo, \"REQUEST\",\n//\t\t\t\t\tslog.String(\"uri\", v.URI),\n//\t\t\t\t\tslog.Int(\"status\", v.Status),\n//\t\t\t\t)\n//\t\t\t} else {\n//\t\t\t\tlogger.LogAttrs(context.Background(), slog.LevelError, \"REQUEST_ERROR\",\n//\t\t\t\t\tslog.String(\"uri\", v.URI),\n//\t\t\t\t\tslog.Int(\"status\", v.Status),\n//\t\t\t\t\tslog.String(\"err\", v.Error.Error()),\n//\t\t\t\t)\n//\t\t\t}\n//\t\t\treturn nil\n//\t\t},\n//\t}))\n//\n// Example for `fmt.Printf`\n// \te.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{\n//\t\tLogStatus:   true,\n//\t\tLogURI:      true,\n//\t\tHandleError: true, // forwards error to the global error handler, so it can decide appropriate status code\n//\t\tLogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error {\n//\t\t\tif v.Error == nil {\n//\t\t\t\tfmt.Printf(\"REQUEST: uri: %v, status: %v\\n\", v.URI, v.Status)\n//\t\t\t} else {\n//\t\t\t\tfmt.Printf(\"REQUEST_ERROR: uri: %v, status: %v, err: %v\\n\", v.URI, v.Status, v.Error)\n//\t\t\t}\n//\t\t\treturn nil\n//\t\t},\n//\t}))\n//\n// Example for Zerolog (https://github.com/rs/zerolog)\n// \tlogger := zerolog.New(os.Stdout)\n//\te.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{\n//\t\tLogURI:      true,\n//\t\tLogStatus:   true,\n//\t\tHandleError: true, // forwards error to the global error handler, so it can decide appropriate status code\n//\t\tLogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error {\n//\t\t\tif v.Error == nil {\n//\t\t\t\tlogger.Info().\n//\t\t\t\t\tStr(\"URI\", v.URI).\n//\t\t\t\t\tInt(\"status\", v.Status).\n//\t\t\t\t\tMsg(\"request\")\n//\t\t\t} else {\n//\t\t\t\tlogger.Error().\n//\t\t\t\t\tErr(v.Error).\n//\t\t\t\t\tStr(\"URI\", v.URI).\n//\t\t\t\t\tInt(\"status\", v.Status).\n//\t\t\t\t\tMsg(\"request error\")\n//\t\t\t}\n//\t\t\treturn nil\n//\t\t},\n//\t}))\n//\n// Example for Zap (https://github.com/uber-go/zap)\n// \tlogger, _ := zap.NewProduction()\n//\te.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{\n//\t\tLogURI:      true,\n//\t\tLogStatus:   true,\n//\t\tHandleError: true, // forwards error to the global error handler, so it can decide appropriate status code\n//\t\tLogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error {\n//\t\t\tif v.Error == nil {\n//\t\t\t\tlogger.Info(\"request\",\n//\t\t\t\t\tzap.String(\"URI\", v.URI),\n//\t\t\t\t\tzap.Int(\"status\", v.Status),\n//\t\t\t\t)\n//\t\t\t} else {\n//\t\t\t\tlogger.Error(\"request error\",\n//\t\t\t\t\tzap.String(\"URI\", v.URI),\n//\t\t\t\t\tzap.Int(\"status\", v.Status),\n//\t\t\t\t\tzap.Error(v.Error),\n//\t\t\t\t)\n//\t\t\t}\n//\t\t\treturn nil\n//\t\t},\n//\t}))\n//\n// Example for Logrus (https://github.com/sirupsen/logrus)\n//  log := logrus.New()\n//\te.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{\n//\t\tLogURI:      true,\n//\t\tLogStatus:   true,\n//\t\tHandleError: true, // forwards error to the global error handler, so it can decide appropriate status code\n//\t\tLogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error {\n//\t\t\tif v.Error == nil {\n//\t\t\t\tlog.WithFields(logrus.Fields{\n//\t\t\t\t\t\"URI\":    v.URI,\n//\t\t\t\t\t\"status\": v.Status,\n//\t\t\t\t}).Info(\"request\")\n//\t\t\t} else {\n//\t\t\t\tlog.WithFields(logrus.Fields{\n//\t\t\t\t\t\"URI\":    v.URI,\n//\t\t\t\t\t\"status\": v.Status,\n//\t\t\t\t\t\"error\":  v.Error,\n//\t\t\t\t}).Error(\"request error\")\n//\t\t\t}\n//\t\t\treturn nil\n//\t\t},\n//\t}))\n\n// RequestLoggerConfig is configuration for Request Logger middleware.\ntype RequestLoggerConfig struct {\n\t// Skipper defines a function to skip middleware.\n\tSkipper Skipper\n\n\t// BeforeNextFunc defines a function that is called before next middleware or handler is called in chain.\n\tBeforeNextFunc func(c *echo.Context)\n\t// LogValuesFunc defines a function that is called with values extracted by logger from request/response.\n\t// Mandatory.\n\tLogValuesFunc func(c *echo.Context, v RequestLoggerValues) error\n\n\t// HandleError instructs logger to call global error handler when next middleware/handler returns an error.\n\t// This is useful when you have custom error handler that can decide to use different status codes.\n\t//\n\t// A side-effect of calling global error handler is that now Response has been committed and sent to the client\n\t// and middlewares up in chain can not change Response status code or response body.\n\tHandleError bool\n\n\t// LogLatency instructs logger to record duration it took to execute rest of the handler chain (next(c) call).\n\tLogLatency bool\n\t// LogProtocol instructs logger to extract request protocol (i.e. `HTTP/1.1` or `HTTP/2`)\n\tLogProtocol bool\n\t// LogRemoteIP instructs logger to extract request remote IP. See `echo.Context.RealIP()` for implementation details.\n\tLogRemoteIP bool\n\t// LogHost instructs logger to extract request host value (i.e. `example.com`)\n\tLogHost bool\n\t// LogMethod instructs logger to extract request method value (i.e. `GET` etc)\n\tLogMethod bool\n\t// LogURI instructs logger to extract request URI (i.e. `/list?lang=en&page=1`)\n\tLogURI bool\n\t// LogURIPath instructs logger to extract request URI path part (i.e. `/list`)\n\tLogURIPath bool\n\t// LogRoutePath instructs logger to extract route path part to which request was matched to (i.e. `/user/:id`)\n\tLogRoutePath bool\n\t// LogRequestID instructs logger to extract request ID from request `X-Request-ID` header or response if request did not have value.\n\tLogRequestID bool\n\t// LogReferer instructs logger to extract request referer values.\n\tLogReferer bool\n\t// LogUserAgent instructs logger to extract request user agent values.\n\tLogUserAgent bool\n\t// LogStatus instructs logger to extract response status code. If handler chain returns an error,\n\t// the status code is extracted from the error satisfying echo.StatusCoder interface.\n\tLogStatus bool\n\t// LogContentLength instructs logger to extract content length header value. Note: this value could be different from\n\t// actual request body size as it could be spoofed etc.\n\tLogContentLength bool\n\t// LogResponseSize instructs logger to extract response content length value. Note: when used with Gzip middleware\n\t// this value may not be always correct.\n\tLogResponseSize bool\n\t// LogHeaders instructs logger to extract given list of headers from request. Note: request can contain more than\n\t// one header with same value so slice of values is been logger for each given header.\n\t//\n\t// Note: header values are converted to canonical form with http.CanonicalHeaderKey as this how request parser converts header\n\t// names to. For example, the canonical key for \"accept-encoding\" is \"Accept-Encoding\".\n\tLogHeaders []string\n\t// LogQueryParams instructs logger to extract given list of query parameters from request URI. Note: request can\n\t// contain more than one query parameter with same name so slice of values is been logger for each given query param name.\n\tLogQueryParams []string\n\t// LogFormValues instructs logger to extract given list of form values from request body+URI. Note: request can\n\t// contain more than one form value with same name so slice of values is been logger for each given form value name.\n\tLogFormValues []string\n\n\ttimeNow func() time.Time\n}\n\n// RequestLoggerValues contains extracted values from logger.\ntype RequestLoggerValues struct {\n\t// StartTime is time recorded before next middleware/handler is executed.\n\tStartTime time.Time\n\t// Latency is duration it took to execute rest of the handler chain (next(c) call).\n\tLatency time.Duration\n\t// Protocol is request protocol (i.e. `HTTP/1.1` or `HTTP/2`)\n\tProtocol string\n\t// RemoteIP is request remote IP. See `echo.Context.RealIP()` for implementation details.\n\tRemoteIP string\n\t// Host is request host value (i.e. `example.com`)\n\tHost string\n\t// Method is request method value (i.e. `GET` etc)\n\tMethod string\n\t// URI is request URI (i.e. `/list?lang=en&page=1`)\n\tURI string\n\t// URIPath is request URI path part (i.e. `/list`)\n\tURIPath string\n\t// RoutePath is route path part to which request was matched to (i.e. `/user/:id`)\n\tRoutePath string\n\t// RequestID is request ID from request `X-Request-ID` header or response if request did not have value.\n\tRequestID string\n\t// Referer is request referer values.\n\tReferer string\n\t// UserAgent is request user agent values.\n\tUserAgent string\n\t// Status is a response status code. When the handler returns an error satisfying echo.StatusCoder interface, then code from it.\n\tStatus int\n\t// Error is error returned from executed handler chain.\n\tError error\n\t// ContentLength is content length header value. Note: this value could be different from actual request body size\n\t// as it could be spoofed etc.\n\tContentLength string\n\t// ResponseSize is response content length value. Note: when used with Gzip middleware this value may not be always correct.\n\tResponseSize int64\n\t// Headers are list of headers from request. Note: request can contain more than one header with same value so slice\n\t// of values is what will be returned/logged for each given header.\n\t// Note: header values are converted to canonical form with http.CanonicalHeaderKey as this how request parser converts header\n\t// names to. For example, the canonical key for \"accept-encoding\" is \"Accept-Encoding\".\n\tHeaders map[string][]string\n\t// QueryParams are list of query parameters from request URI. Note: request can contain more than one query parameter\n\t// with same name so slice of values is what will be returned/logged for each given query param name.\n\tQueryParams map[string][]string\n\t// FormValues are list of form values from request body+URI. Note: request can contain more than one form value with\n\t// same name so slice of values is what will be returned/logged for each given form value name.\n\tFormValues map[string][]string\n}\n\n// RequestLoggerWithConfig returns a RequestLogger middleware with config.\nfunc RequestLoggerWithConfig(config RequestLoggerConfig) echo.MiddlewareFunc {\n\tmw, err := config.ToMiddleware()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn mw\n}\n\n// ToMiddleware converts RequestLoggerConfig into middleware or returns an error for invalid configuration.\nfunc (config RequestLoggerConfig) ToMiddleware() (echo.MiddlewareFunc, error) {\n\tif config.Skipper == nil {\n\t\tconfig.Skipper = DefaultSkipper\n\t}\n\tnow := time.Now\n\tif config.timeNow != nil {\n\t\tnow = config.timeNow\n\t}\n\n\tif config.LogValuesFunc == nil {\n\t\treturn nil, errors.New(\"missing LogValuesFunc callback function for request logger middleware\")\n\t}\n\n\tlogHeaders := len(config.LogHeaders) > 0\n\theaders := append([]string(nil), config.LogHeaders...)\n\tfor i, v := range headers {\n\t\theaders[i] = http.CanonicalHeaderKey(v)\n\t}\n\n\tlogQueryParams := len(config.LogQueryParams) > 0\n\tlogFormValues := len(config.LogFormValues) > 0\n\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) error {\n\t\t\tif config.Skipper(c) {\n\t\t\t\treturn next(c)\n\t\t\t}\n\n\t\t\treq := c.Request()\n\t\t\tstart := now()\n\n\t\t\tif config.BeforeNextFunc != nil {\n\t\t\t\tconfig.BeforeNextFunc(c)\n\t\t\t}\n\t\t\terr := next(c)\n\t\t\tif err != nil && config.HandleError {\n\t\t\t\t// When global error handler writes the error to the client the Response gets \"committed\". This state can be\n\t\t\t\t// checked with `c.Response().Committed` field.\n\t\t\t\tc.Echo().HTTPErrorHandler(c, err)\n\t\t\t}\n\t\t\tres := c.Response()\n\n\t\t\tv := RequestLoggerValues{\n\t\t\t\tStartTime: start,\n\t\t\t}\n\t\t\tif config.LogLatency {\n\t\t\t\tv.Latency = now().Sub(start)\n\t\t\t}\n\t\t\tif config.LogProtocol {\n\t\t\t\tv.Protocol = req.Proto\n\t\t\t}\n\t\t\tif config.LogRemoteIP {\n\t\t\t\tv.RemoteIP = c.RealIP()\n\t\t\t}\n\t\t\tif config.LogHost {\n\t\t\t\tv.Host = req.Host\n\t\t\t}\n\t\t\tif config.LogMethod {\n\t\t\t\tv.Method = req.Method\n\t\t\t}\n\t\t\tif config.LogURI {\n\t\t\t\tv.URI = req.RequestURI\n\t\t\t}\n\t\t\tif config.LogURIPath {\n\t\t\t\tp := req.URL.Path\n\t\t\t\tif p == \"\" {\n\t\t\t\t\tp = \"/\"\n\t\t\t\t}\n\t\t\t\tv.URIPath = p\n\t\t\t}\n\t\t\tif config.LogRoutePath {\n\t\t\t\tv.RoutePath = c.Path()\n\t\t\t}\n\t\t\tif config.LogRequestID {\n\t\t\t\tid := req.Header.Get(echo.HeaderXRequestID)\n\t\t\t\tif id == \"\" {\n\t\t\t\t\tid = res.Header().Get(echo.HeaderXRequestID)\n\t\t\t\t}\n\t\t\t\tv.RequestID = id\n\t\t\t}\n\t\t\tif config.LogReferer {\n\t\t\t\tv.Referer = req.Referer()\n\t\t\t}\n\t\t\tif config.LogUserAgent {\n\t\t\t\tv.UserAgent = req.UserAgent()\n\t\t\t}\n\n\t\t\tif config.LogStatus || config.LogResponseSize {\n\t\t\t\tresp, status := echo.ResolveResponseStatus(res, err)\n\n\t\t\t\tif config.LogStatus {\n\t\t\t\t\tv.Status = status\n\t\t\t\t}\n\t\t\t\tif config.LogResponseSize {\n\t\t\t\t\tv.ResponseSize = -1\n\t\t\t\t\tif resp != nil {\n\t\t\t\t\t\tv.ResponseSize = resp.Size\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tv.Error = err\n\t\t\t}\n\t\t\tif config.LogContentLength {\n\t\t\t\tv.ContentLength = req.Header.Get(echo.HeaderContentLength)\n\t\t\t}\n\t\t\tif logHeaders {\n\t\t\t\tv.Headers = map[string][]string{}\n\t\t\t\tfor _, header := range headers {\n\t\t\t\t\tif values, ok := req.Header[header]; ok {\n\t\t\t\t\t\tv.Headers[header] = values\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif logQueryParams {\n\t\t\t\tqueryParams := c.QueryParams()\n\t\t\t\tv.QueryParams = map[string][]string{}\n\t\t\t\tfor _, param := range config.LogQueryParams {\n\t\t\t\t\tif values, ok := queryParams[param]; ok {\n\t\t\t\t\t\tv.QueryParams[param] = values\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif logFormValues {\n\t\t\t\tv.FormValues = map[string][]string{}\n\t\t\t\tfor _, formValue := range config.LogFormValues {\n\t\t\t\t\tif values, ok := req.Form[formValue]; ok {\n\t\t\t\t\t\tv.FormValues[formValue] = values\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif errOnLog := config.LogValuesFunc(c, v); errOnLog != nil {\n\t\t\t\treturn errOnLog\n\t\t\t}\n\t\t\t// in case of HandleError=true we are returning the error that we already have handled with global error handler\n\t\t\t// this is deliberate as this error could be useful for upstream middlewares and default global error handler\n\t\t\t// will ignore that error when it bubbles up in middleware chain.\n\t\t\t// Committed response can be checked in custom error handler with following logic\n\t\t\t//\n\t\t\t// if r, _ := echo.UnwrapResponse(c.Response()); r != nil && r.Committed {\n\t\t\t//\t return\n\t\t\t// }\n\t\t\treturn err\n\t\t}\n\t}, nil\n}\n\n// RequestLogger creates Request Logger middleware with Echo default settings that uses Context.Logger() as logger.\nfunc RequestLogger() echo.MiddlewareFunc {\n\treturn RequestLoggerWithConfig(RequestLoggerConfig{\n\t\tLogLatency:       true,\n\t\tLogRemoteIP:      true,\n\t\tLogHost:          true,\n\t\tLogMethod:        true,\n\t\tLogURI:           true,\n\t\tLogRequestID:     true,\n\t\tLogUserAgent:     true,\n\t\tLogStatus:        true,\n\t\tLogContentLength: true,\n\t\tLogResponseSize:  true,\n\t\t// forwards error to the global error handler, so it can decide appropriate status code.\n\t\t// NB: side-effect of that is - request is now \"committed\" written to the client. Middlewares up in chain can not\n\t\t// change Response status code or response body.\n\t\tHandleError: true,\n\t\tLogValuesFunc: func(c *echo.Context, v RequestLoggerValues) error {\n\t\t\tlogger := c.Logger()\n\t\t\tif v.Error == nil {\n\t\t\t\tlogger.LogAttrs(context.Background(), slog.LevelInfo, \"REQUEST\",\n\t\t\t\t\tslog.String(\"method\", v.Method),\n\t\t\t\t\tslog.String(\"uri\", v.URI),\n\t\t\t\t\tslog.Int(\"status\", v.Status),\n\t\t\t\t\tslog.Duration(\"latency\", v.Latency),\n\t\t\t\t\tslog.String(\"host\", v.Host),\n\t\t\t\t\tslog.String(\"bytes_in\", v.ContentLength),\n\t\t\t\t\tslog.Int64(\"bytes_out\", v.ResponseSize),\n\t\t\t\t\tslog.String(\"user_agent\", v.UserAgent),\n\t\t\t\t\tslog.String(\"remote_ip\", v.RemoteIP),\n\t\t\t\t\tslog.String(\"request_id\", v.RequestID),\n\t\t\t\t)\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tlogger.LogAttrs(context.Background(), slog.LevelError, \"REQUEST_ERROR\",\n\t\t\t\tslog.String(\"method\", v.Method),\n\t\t\t\tslog.String(\"uri\", v.URI),\n\t\t\t\tslog.Int(\"status\", v.Status),\n\t\t\t\tslog.Duration(\"latency\", v.Latency),\n\t\t\t\tslog.String(\"host\", v.Host),\n\t\t\t\tslog.String(\"bytes_in\", v.ContentLength),\n\t\t\t\tslog.Int64(\"bytes_out\", v.ResponseSize),\n\t\t\t\tslog.String(\"user_agent\", v.UserAgent),\n\t\t\t\tslog.String(\"remote_ip\", v.RemoteIP),\n\t\t\t\tslog.String(\"request_id\", v.RequestID),\n\n\t\t\t\tslog.String(\"error\", v.Error.Error()),\n\t\t\t)\n\t\t\treturn nil\n\t\t},\n\t})\n}\n"
  },
  {
    "path": "middleware/request_logger_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestRequestLoggerOK(t *testing.T) {\n\told := slog.Default()\n\tt.Cleanup(func() {\n\t\tslog.SetDefault(old)\n\t})\n\n\te := echo.New()\n\tbuf := new(bytes.Buffer)\n\te.Logger = slog.New(slog.NewJSONHandler(buf, nil))\n\te.Use(RequestLogger())\n\n\te.POST(\"/test\", func(c *echo.Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\treader := strings.NewReader(`{\"foo\":\"bar\"}`)\n\treq := httptest.NewRequest(http.MethodPost, \"/test\", reader)\n\treq.Header.Set(echo.HeaderContentLength, strconv.Itoa(int(reader.Size())))\n\treq.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)\n\treq.Header.Set(echo.HeaderXRealIP, \"8.8.8.8\")\n\treq.Header.Set(\"User-Agent\", \"curl/7.68.0\")\n\n\trec := httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\n\tlogAttrs := map[string]interface{}{}\n\tassert.NoError(t, json.Unmarshal(buf.Bytes(), &logAttrs))\n\tlogAttrs[\"latency\"] = 123\n\tlogAttrs[\"time\"] = \"x\"\n\n\texpect := map[string]interface{}{\n\t\t\"level\":      \"INFO\",\n\t\t\"msg\":        \"REQUEST\",\n\t\t\"method\":     \"POST\",\n\t\t\"uri\":        \"/test\",\n\t\t\"status\":     float64(418),\n\t\t\"bytes_in\":   \"13\",\n\t\t\"host\":       \"example.com\",\n\t\t\"bytes_out\":  float64(2),\n\t\t\"user_agent\": \"curl/7.68.0\",\n\t\t\"remote_ip\":  \"8.8.8.8\",\n\t\t\"request_id\": \"\",\n\n\t\t\"time\":    \"x\",\n\t\t\"latency\": 123,\n\t}\n\tassert.Equal(t, expect, logAttrs)\n}\n\nfunc TestRequestLoggerError(t *testing.T) {\n\told := slog.Default()\n\tt.Cleanup(func() {\n\t\tslog.SetDefault(old)\n\t})\n\n\te := echo.New()\n\tbuf := new(bytes.Buffer)\n\te.Logger = slog.New(slog.NewJSONHandler(buf, nil))\n\te.Use(RequestLogger())\n\n\te.GET(\"/test\", func(c *echo.Context) error {\n\t\treturn errors.New(\"nope\")\n\t})\n\treq := httptest.NewRequest(http.MethodGet, \"/test\", nil)\n\trec := httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\n\tlogAttrs := map[string]interface{}{}\n\tassert.NoError(t, json.Unmarshal(buf.Bytes(), &logAttrs))\n\tlogAttrs[\"latency\"] = 123\n\tlogAttrs[\"time\"] = \"x\"\n\n\texpect := map[string]interface{}{\n\t\t\"level\":      \"ERROR\",\n\t\t\"msg\":        \"REQUEST_ERROR\",\n\t\t\"method\":     \"GET\",\n\t\t\"uri\":        \"/test\",\n\t\t\"status\":     float64(500),\n\t\t\"bytes_in\":   \"\",\n\t\t\"host\":       \"example.com\",\n\t\t\"bytes_out\":  float64(36.0),\n\t\t\"user_agent\": \"\",\n\t\t\"remote_ip\":  \"192.0.2.1\",\n\t\t\"request_id\": \"\",\n\t\t\"error\":      \"nope\",\n\n\t\t\"latency\": 123,\n\t\t\"time\":    \"x\",\n\t}\n\tassert.Equal(t, expect, logAttrs)\n}\n\nfunc TestRequestLoggerWithConfig(t *testing.T) {\n\te := echo.New()\n\n\tvar expect RequestLoggerValues\n\te.Use(RequestLoggerWithConfig(RequestLoggerConfig{\n\t\tLogRoutePath: true,\n\t\tLogURI:       true,\n\t\tLogValuesFunc: func(c *echo.Context, values RequestLoggerValues) error {\n\t\t\texpect = values\n\t\t\treturn nil\n\t\t},\n\t}))\n\n\te.GET(\"/test\", func(c *echo.Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/test\", nil)\n\trec := httptest.NewRecorder()\n\n\te.ServeHTTP(rec, req)\n\n\tassert.Equal(t, http.StatusTeapot, rec.Code)\n\tassert.Equal(t, \"/test\", expect.RoutePath)\n}\n\nfunc TestRequestLoggerWithConfig_missingOnLogValuesPanics(t *testing.T) {\n\tassert.Panics(t, func() {\n\t\tRequestLoggerWithConfig(RequestLoggerConfig{\n\t\t\tLogValuesFunc: nil,\n\t\t})\n\t})\n}\n\nfunc TestRequestLogger_skipper(t *testing.T) {\n\te := echo.New()\n\n\tloggerCalled := false\n\te.Use(RequestLoggerWithConfig(RequestLoggerConfig{\n\t\tSkipper: func(c *echo.Context) bool {\n\t\t\treturn true\n\t\t},\n\t\tLogValuesFunc: func(c *echo.Context, values RequestLoggerValues) error {\n\t\t\tloggerCalled = true\n\t\t\treturn nil\n\t\t},\n\t}))\n\n\te.GET(\"/test\", func(c *echo.Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/test\", nil)\n\trec := httptest.NewRecorder()\n\n\te.ServeHTTP(rec, req)\n\n\tassert.Equal(t, http.StatusTeapot, rec.Code)\n\tassert.False(t, loggerCalled)\n}\n\nfunc TestRequestLogger_beforeNextFunc(t *testing.T) {\n\te := echo.New()\n\n\tvar myLoggerInstance int\n\te.Use(RequestLoggerWithConfig(RequestLoggerConfig{\n\t\tBeforeNextFunc: func(c *echo.Context) {\n\t\t\tc.Set(\"myLoggerInstance\", 42)\n\t\t},\n\t\tLogValuesFunc: func(c *echo.Context, values RequestLoggerValues) error {\n\t\t\tmyLoggerInstance = c.Get(\"myLoggerInstance\").(int)\n\t\t\treturn nil\n\t\t},\n\t}))\n\n\te.GET(\"/test\", func(c *echo.Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/test\", nil)\n\trec := httptest.NewRecorder()\n\n\te.ServeHTTP(rec, req)\n\n\tassert.Equal(t, http.StatusTeapot, rec.Code)\n\tassert.Equal(t, 42, myLoggerInstance)\n}\n\nfunc TestRequestLogger_logError(t *testing.T) {\n\te := echo.New()\n\n\tvar actual RequestLoggerValues\n\te.Use(RequestLoggerWithConfig(RequestLoggerConfig{\n\t\tLogStatus: true,\n\t\tLogValuesFunc: func(c *echo.Context, values RequestLoggerValues) error {\n\t\t\tactual = values\n\t\t\treturn nil\n\t\t},\n\t}))\n\n\te.GET(\"/test\", func(c *echo.Context) error {\n\t\treturn echo.NewHTTPError(http.StatusNotAcceptable, \"nope\")\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/test\", nil)\n\trec := httptest.NewRecorder()\n\n\te.ServeHTTP(rec, req)\n\n\tassert.Equal(t, http.StatusNotAcceptable, rec.Code)\n\tassert.Equal(t, http.StatusNotAcceptable, actual.Status)\n\tassert.EqualError(t, actual.Error, \"code=406, message=nope\")\n}\n\nfunc TestRequestLogger_HandleError(t *testing.T) {\n\te := echo.New()\n\n\tvar actual RequestLoggerValues\n\te.Use(RequestLoggerWithConfig(RequestLoggerConfig{\n\t\ttimeNow: func() time.Time {\n\t\t\treturn time.Unix(1631045377, 0).UTC()\n\t\t},\n\t\tHandleError: true,\n\t\tLogStatus:   true,\n\t\tLogValuesFunc: func(c *echo.Context, values RequestLoggerValues) error {\n\t\t\tactual = values\n\t\t\treturn nil\n\t\t},\n\t}))\n\n\t// to see if \"HandleError\" works we create custom error handler that uses its own status codes\n\te.HTTPErrorHandler = func(c *echo.Context, err error) {\n\t\tif r, _ := echo.UnwrapResponse(c.Response()); r != nil && r.Committed {\n\t\t\treturn\n\t\t}\n\t\tc.JSON(http.StatusTeapot, \"custom error handler\")\n\t}\n\n\te.GET(\"/test\", func(c *echo.Context) error {\n\t\treturn echo.NewHTTPError(http.StatusForbidden, \"nope\")\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/test\", nil)\n\trec := httptest.NewRecorder()\n\n\te.ServeHTTP(rec, req)\n\n\tassert.Equal(t, http.StatusTeapot, rec.Code)\n\n\texpect := RequestLoggerValues{\n\t\tStartTime: time.Unix(1631045377, 0).UTC(),\n\t\tStatus:    http.StatusTeapot,\n\t\tError:     echo.NewHTTPError(http.StatusForbidden, \"nope\"),\n\t}\n\tassert.Equal(t, expect, actual)\n}\n\nfunc TestRequestLogger_LogValuesFuncError(t *testing.T) {\n\te := echo.New()\n\n\tvar expect RequestLoggerValues\n\te.Use(RequestLoggerWithConfig(RequestLoggerConfig{\n\t\tLogStatus: true,\n\t\tLogValuesFunc: func(c *echo.Context, values RequestLoggerValues) error {\n\t\t\texpect = values\n\t\t\treturn echo.NewHTTPError(http.StatusNotAcceptable, \"LogValuesFuncError\")\n\t\t},\n\t}))\n\n\te.GET(\"/test\", func(c *echo.Context) error {\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/test\", nil)\n\trec := httptest.NewRecorder()\n\n\te.ServeHTTP(rec, req)\n\n\t// NOTE: when global error handler received error returned from middleware the status has already\n\t// been written to the client and response has been \"committed\" therefore global error handler does not do anything\n\t// and error that bubbled up in middleware chain will not be reflected in response code.\n\tassert.Equal(t, http.StatusTeapot, rec.Code)\n\tassert.Equal(t, http.StatusTeapot, expect.Status)\n}\n\nfunc TestRequestLogger_ID(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenFromRequest bool\n\t\texpect          string\n\t}{\n\t\t{\n\t\t\tname:            \"ok, ID is provided from request headers\",\n\t\t\twhenFromRequest: true,\n\t\t\texpect:          \"123\",\n\t\t},\n\t\t{\n\t\t\tname:            \"ok, ID is from response headers\",\n\t\t\twhenFromRequest: false,\n\t\t\texpect:          \"321\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := echo.New()\n\n\t\t\tvar expect RequestLoggerValues\n\t\t\te.Use(RequestLoggerWithConfig(RequestLoggerConfig{\n\t\t\t\tLogRequestID: true,\n\t\t\t\tLogValuesFunc: func(c *echo.Context, values RequestLoggerValues) error {\n\t\t\t\t\texpect = values\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}))\n\n\t\t\te.GET(\"/test\", func(c *echo.Context) error {\n\t\t\t\tc.Response().Header().Set(echo.HeaderXRequestID, \"321\")\n\t\t\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t\t\t})\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/test\", nil)\n\t\t\tif tc.whenFromRequest {\n\t\t\t\treq.Header.Set(echo.HeaderXRequestID, \"123\")\n\t\t\t}\n\t\t\trec := httptest.NewRecorder()\n\n\t\t\te.ServeHTTP(rec, req)\n\n\t\t\tassert.Equal(t, http.StatusTeapot, rec.Code)\n\t\t\tassert.Equal(t, tc.expect, expect.RequestID)\n\t\t})\n\t}\n}\n\nfunc TestRequestLogger_headerIsCaseInsensitive(t *testing.T) {\n\te := echo.New()\n\n\tvar expect RequestLoggerValues\n\tmw := RequestLoggerWithConfig(RequestLoggerConfig{\n\t\tLogValuesFunc: func(c *echo.Context, values RequestLoggerValues) error {\n\t\t\texpect = values\n\t\t\treturn nil\n\t\t},\n\t\tLogHeaders: []string{\"referer\", \"User-Agent\"},\n\t})(func(c *echo.Context) error {\n\t\tc.Request().Header.Set(echo.HeaderXRequestID, \"123\")\n\t\tc.FormValue(\"to force parse form\")\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/test?lang=en&checked=1&checked=2\", nil)\n\treq.Header.Set(\"referer\", \"https://echo.labstack.com/\")\n\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\terr := mw(c)\n\n\tassert.NoError(t, err)\n\tassert.Len(t, expect.Headers, 1)\n\tassert.Equal(t, []string{\"https://echo.labstack.com/\"}, expect.Headers[\"Referer\"])\n}\n\nfunc TestRequestLogger_allFields(t *testing.T) {\n\te := echo.New()\n\n\tisFirstNowCall := true\n\tvar expect RequestLoggerValues\n\tmw := RequestLoggerWithConfig(RequestLoggerConfig{\n\t\tLogValuesFunc: func(c *echo.Context, values RequestLoggerValues) error {\n\t\t\texpect = values\n\t\t\treturn nil\n\t\t},\n\t\tLogLatency:       true,\n\t\tLogProtocol:      true,\n\t\tLogRemoteIP:      true,\n\t\tLogHost:          true,\n\t\tLogMethod:        true,\n\t\tLogURI:           true,\n\t\tLogURIPath:       true,\n\t\tLogRoutePath:     true,\n\t\tLogRequestID:     true,\n\t\tLogReferer:       true,\n\t\tLogUserAgent:     true,\n\t\tLogStatus:        true,\n\t\tLogContentLength: true,\n\t\tLogResponseSize:  true,\n\t\tLogHeaders:       []string{\"accept-encoding\", \"User-Agent\"},\n\t\tLogQueryParams:   []string{\"lang\", \"checked\"},\n\t\tLogFormValues:    []string{\"csrf\", \"multiple\"},\n\t\ttimeNow: func() time.Time {\n\t\t\tif isFirstNowCall {\n\t\t\t\tisFirstNowCall = false\n\t\t\t\treturn time.Unix(1631045377, 0)\n\t\t\t}\n\t\t\treturn time.Unix(1631045377+10, 0)\n\t\t},\n\t})(func(c *echo.Context) error {\n\t\tc.Request().Header.Set(echo.HeaderXRequestID, \"123\")\n\t\tc.FormValue(\"to force parse form\")\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\tf := make(url.Values)\n\tf.Set(\"csrf\", \"token\")\n\tf.Set(\"multiple\", \"1\")\n\tf.Add(\"multiple\", \"2\")\n\treader := strings.NewReader(f.Encode())\n\treq := httptest.NewRequest(http.MethodPost, \"/test?lang=en&checked=1&checked=2\", reader)\n\treq.Header.Set(\"Referer\", \"https://echo.labstack.com/\")\n\treq.Header.Set(\"User-Agent\", \"curl/7.68.0\")\n\treq.Header.Set(echo.HeaderContentLength, strconv.Itoa(int(reader.Size())))\n\treq.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm)\n\treq.Header.Set(echo.HeaderXRealIP, \"8.8.8.8\")\n\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\tc.SetPath(\"/test*\")\n\n\terr := mw(c)\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, time.Unix(1631045377, 0), expect.StartTime)\n\tassert.Equal(t, 10*time.Second, expect.Latency)\n\tassert.Equal(t, \"HTTP/1.1\", expect.Protocol)\n\tassert.Equal(t, \"8.8.8.8\", expect.RemoteIP)\n\tassert.Equal(t, \"example.com\", expect.Host)\n\tassert.Equal(t, http.MethodPost, expect.Method)\n\tassert.Equal(t, \"/test?lang=en&checked=1&checked=2\", expect.URI)\n\tassert.Equal(t, \"/test\", expect.URIPath)\n\tassert.Equal(t, \"/test*\", expect.RoutePath)\n\tassert.Equal(t, \"123\", expect.RequestID)\n\tassert.Equal(t, \"https://echo.labstack.com/\", expect.Referer)\n\tassert.Equal(t, \"curl/7.68.0\", expect.UserAgent)\n\tassert.Equal(t, 418, expect.Status)\n\tassert.Equal(t, nil, expect.Error)\n\tassert.Equal(t, \"32\", expect.ContentLength)\n\tassert.Equal(t, int64(2), expect.ResponseSize)\n\n\tassert.Len(t, expect.Headers, 1)\n\tassert.Equal(t, []string{\"curl/7.68.0\"}, expect.Headers[\"User-Agent\"])\n\n\tassert.Len(t, expect.QueryParams, 2)\n\tassert.Equal(t, []string{\"en\"}, expect.QueryParams[\"lang\"])\n\tassert.Equal(t, []string{\"1\", \"2\"}, expect.QueryParams[\"checked\"])\n\n\tassert.Len(t, expect.FormValues, 2)\n\tassert.Equal(t, []string{\"token\"}, expect.FormValues[\"csrf\"])\n\tassert.Equal(t, []string{\"1\", \"2\"}, expect.FormValues[\"multiple\"])\n}\n\nfunc TestTestRequestLogger(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname         string\n\t\twhenStatus   int\n\t\twhenError    error\n\t\texpectStatus string\n\t\texpectError  string\n\t}{\n\t\t{\n\t\t\tname:         \"ok\",\n\t\t\twhenStatus:   http.StatusTeapot,\n\t\t\texpectStatus: \"418\",\n\t\t},\n\t\t{\n\t\t\tname:         \"error\",\n\t\t\twhenError:    echo.NewHTTPError(http.StatusBadGateway, \"bad gw\"),\n\t\t\texpectStatus: \"502\",\n\t\t\texpectError:  `\"error\":\"code=502, message=bad gw\"`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := echo.New()\n\t\t\tbuf := new(bytes.Buffer)\n\t\t\te.Logger = slog.New(slog.NewJSONHandler(buf, nil))\n\n\t\t\te.Use(RequestLogger())\n\t\t\te.POST(\"/test\", func(c *echo.Context) error {\n\t\t\t\tif tc.whenError != nil {\n\t\t\t\t\treturn tc.whenError\n\t\t\t\t}\n\t\t\t\treturn c.String(tc.whenStatus, \"OK\")\n\t\t\t})\n\n\t\t\tf := make(url.Values)\n\t\t\tf.Set(\"csrf\", \"token\")\n\t\t\tf.Set(\"multiple\", \"1\")\n\t\t\tf.Add(\"multiple\", \"2\")\n\t\t\treader := strings.NewReader(f.Encode())\n\t\t\treq := httptest.NewRequest(http.MethodPost, \"/test?lang=en&checked=1&checked=2\", reader)\n\t\t\treq.Header.Set(\"Referer\", \"https://echo.labstack.com/\")\n\t\t\treq.Header.Set(\"User-Agent\", \"curl/7.68.0\")\n\t\t\treq.Header.Set(echo.HeaderContentLength, strconv.Itoa(int(reader.Size())))\n\t\t\treq.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm)\n\t\t\treq.Header.Set(echo.HeaderXRealIP, \"8.8.8.8\")\n\t\t\treq.Header.Set(echo.HeaderXRequestID, \"MY_ID\")\n\n\t\t\trec := httptest.NewRecorder()\n\n\t\t\te.ServeHTTP(rec, req)\n\n\t\t\trawlog := buf.Bytes()\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.Contains(t, string(rawlog), `\"level\":\"ERROR\"`)\n\t\t\t\tassert.Contains(t, string(rawlog), `\"msg\":\"REQUEST_ERROR\"`)\n\t\t\t\tassert.Contains(t, string(rawlog), tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.Contains(t, string(rawlog), `\"level\":\"INFO\"`)\n\t\t\t\tassert.Contains(t, string(rawlog), `\"msg\":\"REQUEST\"`)\n\t\t\t}\n\t\t\tassert.Contains(t, string(rawlog), `\"status\":`+tc.expectStatus)\n\t\t\tassert.Contains(t, string(rawlog), `\"method\":\"POST\"`)\n\t\t\tassert.Contains(t, string(rawlog), `\"uri\":\"/test?lang=en&checked=1&checked=2\"`)\n\t\t\tassert.Contains(t, string(rawlog), `\"latency\":`) // this value varies\n\t\t\tassert.Contains(t, string(rawlog), `\"request_id\":\"MY_ID\"`)\n\t\t\tassert.Contains(t, string(rawlog), `\"remote_ip\":\"8.8.8.8\"`)\n\t\t\tassert.Contains(t, string(rawlog), `\"host\":\"example.com\"`)\n\t\t\tassert.Contains(t, string(rawlog), `\"user_agent\":\"curl/7.68.0\"`)\n\t\t\tassert.Contains(t, string(rawlog), `\"bytes_in\":\"32\"`)\n\t\t\tassert.Contains(t, string(rawlog), `\"bytes_out\":2`)\n\t\t})\n\t}\n}\n\nfunc BenchmarkRequestLogger_withoutMapFields(b *testing.B) {\n\te := echo.New()\n\n\tmw := RequestLoggerWithConfig(RequestLoggerConfig{\n\t\tSkipper: nil,\n\t\tLogValuesFunc: func(c *echo.Context, values RequestLoggerValues) error {\n\t\t\treturn nil\n\t\t},\n\t\tLogLatency:       true,\n\t\tLogProtocol:      true,\n\t\tLogRemoteIP:      true,\n\t\tLogHost:          true,\n\t\tLogMethod:        true,\n\t\tLogURI:           true,\n\t\tLogURIPath:       true,\n\t\tLogRoutePath:     true,\n\t\tLogRequestID:     true,\n\t\tLogReferer:       true,\n\t\tLogUserAgent:     true,\n\t\tLogStatus:        true,\n\t\tLogContentLength: true,\n\t\tLogResponseSize:  true,\n\t})(func(c *echo.Context) error {\n\t\tc.Request().Header.Set(echo.HeaderXRequestID, \"123\")\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/test?lang=en\", nil)\n\treq.Header.Set(\"Referer\", \"https://echo.labstack.com/\")\n\treq.Header.Set(\"User-Agent\", \"curl/7.68.0\")\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tfor i := 0; i < b.N; i++ {\n\t\trec := httptest.NewRecorder()\n\t\tc := e.NewContext(req, rec)\n\t\tmw(c)\n\t}\n}\n\nfunc BenchmarkRequestLogger_withMapFields(b *testing.B) {\n\te := echo.New()\n\n\tmw := RequestLoggerWithConfig(RequestLoggerConfig{\n\t\tLogValuesFunc: func(c *echo.Context, values RequestLoggerValues) error {\n\t\t\treturn nil\n\t\t},\n\t\tLogLatency:       true,\n\t\tLogProtocol:      true,\n\t\tLogRemoteIP:      true,\n\t\tLogHost:          true,\n\t\tLogMethod:        true,\n\t\tLogURI:           true,\n\t\tLogURIPath:       true,\n\t\tLogRoutePath:     true,\n\t\tLogRequestID:     true,\n\t\tLogReferer:       true,\n\t\tLogUserAgent:     true,\n\t\tLogStatus:        true,\n\t\tLogContentLength: true,\n\t\tLogResponseSize:  true,\n\t\tLogHeaders:       []string{\"accept-encoding\", \"User-Agent\"},\n\t\tLogQueryParams:   []string{\"lang\", \"checked\"},\n\t\tLogFormValues:    []string{\"csrf\", \"multiple\"},\n\t})(func(c *echo.Context) error {\n\t\tc.Request().Header.Set(echo.HeaderXRequestID, \"123\")\n\t\tc.FormValue(\"to force parse form\")\n\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t})\n\n\tf := make(url.Values)\n\tf.Set(\"csrf\", \"token\")\n\tf.Add(\"multiple\", \"1\")\n\tf.Add(\"multiple\", \"2\")\n\treq := httptest.NewRequest(http.MethodPost, \"/test?lang=en&checked=1&checked=2\", strings.NewReader(f.Encode()))\n\treq.Header.Set(\"Referer\", \"https://echo.labstack.com/\")\n\treq.Header.Set(\"User-Agent\", \"curl/7.68.0\")\n\treq.Header.Add(echo.HeaderContentType, echo.MIMEApplicationForm)\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tfor i := 0; i < b.N; i++ {\n\t\trec := httptest.NewRecorder()\n\t\tc := e.NewContext(req, rec)\n\t\tmw(c)\n\t}\n}\n"
  },
  {
    "path": "middleware/rewrite.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"errors\"\n\t\"regexp\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\n// RewriteConfig defines the config for Rewrite middleware.\ntype RewriteConfig struct {\n\t// Skipper defines a function to skip middleware.\n\tSkipper Skipper\n\n\t// Rules defines the URL path rewrite rules. The values captured in asterisk can be\n\t// retrieved by index e.g. $1, $2 and so on.\n\t// Example:\n\t// \"/old\":              \"/new\",\n\t// \"/api/*\":            \"/$1\",\n\t// \"/js/*\":             \"/public/javascripts/$1\",\n\t// \"/users/*/orders/*\": \"/user/$1/order/$2\",\n\t// Required.\n\tRules map[string]string\n\n\t// RegexRules defines the URL path rewrite rules using regexp.Rexexp with captures\n\t// Every capture group in the values can be retrieved by index e.g. $1, $2 and so on.\n\t// Example:\n\t// \"^/old/[0.9]+/\":     \"/new\",\n\t// \"^/api/.+?/(.*)\":     \"/v2/$1\",\n\tRegexRules map[*regexp.Regexp]string\n}\n\n// Rewrite returns a Rewrite middleware.\n//\n// Rewrite middleware rewrites the URL path based on the provided rules.\nfunc Rewrite(rules map[string]string) echo.MiddlewareFunc {\n\tc := RewriteConfig{}\n\tc.Rules = rules\n\treturn RewriteWithConfig(c)\n}\n\n// RewriteWithConfig returns a Rewrite middleware or panics on invalid configuration.\n//\n// Rewrite middleware rewrites the URL path based on the provided rules.\nfunc RewriteWithConfig(config RewriteConfig) echo.MiddlewareFunc {\n\treturn toMiddlewareOrPanic(config)\n}\n\n// ToMiddleware converts RewriteConfig to middleware or returns an error for invalid configuration\nfunc (config RewriteConfig) ToMiddleware() (echo.MiddlewareFunc, error) {\n\tif config.Skipper == nil {\n\t\tconfig.Skipper = DefaultSkipper\n\t}\n\tif config.Rules == nil && config.RegexRules == nil {\n\t\treturn nil, errors.New(\"echo rewrite middleware requires url path rewrite rules or regex rules\")\n\t}\n\n\tif config.RegexRules == nil {\n\t\tconfig.RegexRules = make(map[*regexp.Regexp]string)\n\t}\n\tfor k, v := range rewriteRulesRegex(config.Rules) {\n\t\tconfig.RegexRules[k] = v\n\t}\n\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) (err error) {\n\t\t\tif config.Skipper(c) {\n\t\t\t\treturn next(c)\n\t\t\t}\n\n\t\t\tif err := rewriteURL(config.RegexRules, c.Request()); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn next(c)\n\t\t}\n\t}, nil\n}\n"
  },
  {
    "path": "middleware/rewrite_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestRewriteAfterRouting(t *testing.T) {\n\te := echo.New()\n\t// middlewares added with `Use()` are executed after routing is done and do not affect which route handler is matched\n\te.Use(RewriteWithConfig(RewriteConfig{\n\t\tRules: map[string]string{\n\t\t\t\"/old\":              \"/new\",\n\t\t\t\"/api/*\":            \"/$1\",\n\t\t\t\"/js/*\":             \"/public/javascripts/$1\",\n\t\t\t\"/users/*/orders/*\": \"/user/$1/order/$2\",\n\t\t},\n\t}))\n\te.GET(\"/public/*\", func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, c.Param(\"*\"))\n\t})\n\te.GET(\"/*\", func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, c.Param(\"*\"))\n\t})\n\n\tvar testCases = []struct {\n\t\twhenPath             string\n\t\texpectRoutePath      string\n\t\texpectRequestPath    string\n\t\texpectRequestRawPath string\n\t}{\n\t\t{\n\t\t\twhenPath:             \"/api/users\",\n\t\t\texpectRoutePath:      \"api/users\",\n\t\t\texpectRequestPath:    \"/users\",\n\t\t\texpectRequestRawPath: \"\",\n\t\t},\n\t\t{\n\t\t\twhenPath:             \"/js/main.js\",\n\t\t\texpectRoutePath:      \"js/main.js\",\n\t\t\texpectRequestPath:    \"/public/javascripts/main.js\",\n\t\t\texpectRequestRawPath: \"\",\n\t\t},\n\t\t{\n\t\t\twhenPath:             \"/users/jack/orders/1\",\n\t\t\texpectRoutePath:      \"users/jack/orders/1\",\n\t\t\texpectRequestPath:    \"/user/jack/order/1\",\n\t\t\texpectRequestRawPath: \"\",\n\t\t},\n\t\t{ // no rewrite rule matched. already encoded URL should not be double encoded or changed in any way\n\t\t\twhenPath:             \"/user/jill/order/T%2FcO4lW%2Ft%2FVp%2F\",\n\t\t\texpectRoutePath:      \"user/jill/order/T%2FcO4lW%2Ft%2FVp%2F\",\n\t\t\texpectRequestPath:    \"/user/jill/order/T/cO4lW/t/Vp/\", // this is equal to `url.Parse(tc.whenPath)` result\n\t\t\texpectRequestRawPath: \"/user/jill/order/T%2FcO4lW%2Ft%2FVp%2F\",\n\t\t},\n\t\t{ // just rewrite but do not touch encoding. already encoded URL should not be double encoded\n\t\t\twhenPath:             \"/users/jill/orders/T%2FcO4lW%2Ft%2FVp%2F\",\n\t\t\texpectRoutePath:      \"users/jill/orders/T%2FcO4lW%2Ft%2FVp%2F\",\n\t\t\texpectRequestPath:    \"/user/jill/order/T/cO4lW/t/Vp/\", // this is equal to `url.Parse(tc.whenPath)` result\n\t\t\texpectRequestRawPath: \"/user/jill/order/T%2FcO4lW%2Ft%2FVp%2F\",\n\t\t},\n\t\t{ // ` ` (space) is encoded by httpClient to `%20` when doing request to Echo. `%20` should not be double escaped or changed in any way when rewriting request\n\t\t\twhenPath:             \"/api/new users\",\n\t\t\texpectRoutePath:      \"api/new users\",\n\t\t\texpectRequestPath:    \"/new users\",\n\t\t\texpectRequestRawPath: \"\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenPath, func(t *testing.T) {\n\t\t\ttarget, _ := url.Parse(tc.whenPath)\n\t\t\treq := httptest.NewRequest(http.MethodGet, target.String(), nil)\n\t\t\trec := httptest.NewRecorder()\n\n\t\t\te.ServeHTTP(rec, req)\n\n\t\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\t\tassert.Equal(t, tc.expectRoutePath, rec.Body.String())\n\t\t\tassert.Equal(t, tc.expectRequestPath, req.URL.Path)\n\t\t\tassert.Equal(t, tc.expectRequestRawPath, req.URL.RawPath)\n\t\t})\n\t}\n}\n\nfunc TestMustRewriteWithConfig_emptyRulesPanics(t *testing.T) {\n\tassert.Panics(t, func() {\n\t\tRewriteWithConfig(RewriteConfig{})\n\t})\n}\n\nfunc TestMustRewriteWithConfig_skipper(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname         string\n\t\tgivenSkipper func(c *echo.Context) bool\n\t\twhenURL      string\n\t\texpectURL    string\n\t\texpectStatus int\n\t}{\n\t\t{\n\t\t\tname:         \"not skipped\",\n\t\t\twhenURL:      \"/old\",\n\t\t\texpectURL:    \"/new\",\n\t\t\texpectStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname: \"skipped\",\n\t\t\tgivenSkipper: func(c *echo.Context) bool {\n\t\t\t\treturn true\n\t\t\t},\n\t\t\twhenURL:      \"/old\",\n\t\t\texpectURL:    \"/old\",\n\t\t\texpectStatus: http.StatusNotFound,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := echo.New()\n\n\t\t\te.Pre(RewriteWithConfig(\n\t\t\t\tRewriteConfig{\n\t\t\t\t\tSkipper: tc.givenSkipper,\n\t\t\t\t\tRules:   map[string]string{\"/old\": \"/new\"}},\n\t\t\t))\n\n\t\t\te.GET(\"/new\", func(c *echo.Context) error {\n\t\t\t\treturn c.NoContent(http.StatusOK)\n\t\t\t})\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)\n\t\t\trec := httptest.NewRecorder()\n\n\t\t\te.ServeHTTP(rec, req)\n\n\t\t\tassert.Equal(t, tc.expectURL, req.URL.EscapedPath())\n\t\t\tassert.Equal(t, tc.expectStatus, rec.Code)\n\t\t})\n\t}\n}\n\n// Issue #1086\nfunc TestEchoRewritePreMiddleware(t *testing.T) {\n\te := echo.New()\n\n\t// Rewrite old url to new one\n\t// middlewares added with `Pre()` are executed before routing is done and therefore change which handler matches\n\te.Pre(RewriteWithConfig(RewriteConfig{\n\t\tRules: map[string]string{\"/old\": \"/new\"}}),\n\t)\n\n\t// Route\n\te.Add(http.MethodGet, \"/new\", func(c *echo.Context) error {\n\t\treturn c.NoContent(http.StatusOK)\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/old\", nil)\n\trec := httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\tassert.Equal(t, \"/new\", req.URL.EscapedPath())\n\tassert.Equal(t, http.StatusOK, rec.Code)\n}\n\n// Issue #1143\nfunc TestRewriteWithConfigPreMiddleware_Issue1143(t *testing.T) {\n\te := echo.New()\n\n\t// middlewares added with `Pre()` are executed before routing is done and therefore change which handler matches\n\te.Pre(RewriteWithConfig(RewriteConfig{\n\t\tRules: map[string]string{\n\t\t\t\"/api/*/mgmt/proj/*/agt\": \"/api/$1/hosts/$2\",\n\t\t\t\"/api/*/mgmt/proj\":       \"/api/$1/eng\",\n\t\t},\n\t}))\n\n\te.Add(http.MethodGet, \"/api/:version/hosts/:name\", func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"hosts\")\n\t})\n\te.Add(http.MethodGet, \"/api/:version/eng\", func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"eng\")\n\t})\n\n\tfor i := 0; i < 100; i++ {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/api/v1/mgmt/proj/test/agt\", nil)\n\t\trec := httptest.NewRecorder()\n\t\te.ServeHTTP(rec, req)\n\t\tassert.Equal(t, \"/api/v1/hosts/test\", req.URL.EscapedPath())\n\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\n\t\tdefer rec.Result().Body.Close()\n\t\tbodyBytes, _ := io.ReadAll(rec.Result().Body)\n\t\tassert.Equal(t, \"hosts\", string(bodyBytes))\n\t}\n}\n\n// Issue #1573\nfunc TestEchoRewriteWithCaret(t *testing.T) {\n\te := echo.New()\n\n\te.Pre(RewriteWithConfig(RewriteConfig{\n\t\tRules: map[string]string{\n\t\t\t\"^/abc/*\": \"/v1/abc/$1\",\n\t\t},\n\t}))\n\n\trec := httptest.NewRecorder()\n\n\tvar req *http.Request\n\n\treq = httptest.NewRequest(http.MethodGet, \"/abc/test\", nil)\n\te.ServeHTTP(rec, req)\n\tassert.Equal(t, \"/v1/abc/test\", req.URL.Path)\n\n\treq = httptest.NewRequest(http.MethodGet, \"/v1/abc/test\", nil)\n\te.ServeHTTP(rec, req)\n\tassert.Equal(t, \"/v1/abc/test\", req.URL.Path)\n\n\treq = httptest.NewRequest(http.MethodGet, \"/v2/abc/test\", nil)\n\te.ServeHTTP(rec, req)\n\tassert.Equal(t, \"/v2/abc/test\", req.URL.Path)\n}\n\n// Verify regex used with rewrite\nfunc TestEchoRewriteWithRegexRules(t *testing.T) {\n\te := echo.New()\n\n\te.Pre(RewriteWithConfig(RewriteConfig{\n\t\tRules: map[string]string{\n\t\t\t\"^/a/*\":     \"/v1/$1\",\n\t\t\t\"^/b/*/c/*\": \"/v2/$2/$1\",\n\t\t\t\"^/c/*/*\":   \"/v3/$2\",\n\t\t},\n\t\tRegexRules: map[*regexp.Regexp]string{\n\t\t\tregexp.MustCompile(\"^/x/.+?/(.*)\"):   \"/v4/$1\",\n\t\t\tregexp.MustCompile(\"^/y/(.+?)/(.*)\"): \"/v5/$2/$1\",\n\t\t},\n\t}))\n\n\tvar rec *httptest.ResponseRecorder\n\tvar req *http.Request\n\n\ttestCases := []struct {\n\t\trequestPath string\n\t\texpectPath  string\n\t}{\n\t\t{\"/unmatched\", \"/unmatched\"},\n\t\t{\"/a/test\", \"/v1/test\"},\n\t\t{\"/b/foo/c/bar/baz\", \"/v2/bar/baz/foo\"},\n\t\t{\"/c/ignore/test\", \"/v3/test\"},\n\t\t{\"/c/ignore1/test/this\", \"/v3/test/this\"},\n\t\t{\"/x/ignore/test\", \"/v4/test\"},\n\t\t{\"/y/foo/bar\", \"/v5/bar/foo\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.requestPath, func(t *testing.T) {\n\t\t\treq = httptest.NewRequest(http.MethodGet, tc.requestPath, nil)\n\t\t\trec = httptest.NewRecorder()\n\t\t\te.ServeHTTP(rec, req)\n\t\t\tassert.Equal(t, tc.expectPath, req.URL.EscapedPath())\n\t\t})\n\t}\n}\n\n// Ensure correct escaping as defined in replacement (issue #1798)\nfunc TestEchoRewriteReplacementEscaping(t *testing.T) {\n\te := echo.New()\n\n\t// NOTE: these are incorrect regexps as they do not factor in that URI we are replacing could contain ? (query) and # (fragment) parts\n\t// so in reality they append query and fragment part as `$1` matches everything after that prefix\n\te.Pre(RewriteWithConfig(RewriteConfig{\n\t\tRules: map[string]string{\n\t\t\t\"^/a/*\": \"/$1?query=param\",\n\t\t\t\"^/b/*\": \"/$1;part#one\",\n\t\t},\n\t\tRegexRules: map[*regexp.Regexp]string{\n\t\t\tregexp.MustCompile(\"^/x/(.*)\"): \"/$1?query=param\",\n\t\t\tregexp.MustCompile(\"^/y/(.*)\"): \"/$1;part#one\",\n\t\t\tregexp.MustCompile(\"^/z/(.*)\"): \"/$1?test=1#escaped%20test\",\n\t\t},\n\t}))\n\n\tvar rec *httptest.ResponseRecorder\n\tvar req *http.Request\n\n\ttestCases := []struct {\n\t\trequestPath string\n\t\texpect      string\n\t}{\n\t\t{\"/unmatched\", \"/unmatched\"},\n\t\t{\"/a/test\", \"/test?query=param\"},\n\t\t{\"/b/foo/bar\", \"/foo/bar;part#one\"},\n\t\t{\"/x/test\", \"/test?query=param\"},\n\t\t{\"/y/foo/bar\", \"/foo/bar;part#one\"},\n\t\t{\"/z/foo/b%20ar\", \"/foo/b%20ar?test=1#escaped%20test\"},\n\t\t{\"/z/foo/b%20ar?nope=1#yes\", \"/foo/b%20ar?nope=1#yes?test=1%23escaped%20test\"}, // example of appending\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.requestPath, func(t *testing.T) {\n\t\t\treq = httptest.NewRequest(http.MethodGet, tc.requestPath, nil)\n\t\t\trec = httptest.NewRecorder()\n\t\t\te.ServeHTTP(rec, req)\n\t\t\tassert.Equal(t, tc.expect, req.URL.String())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "middleware/secure.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\n// SecureConfig defines the config for Secure middleware.\ntype SecureConfig struct {\n\t// Skipper defines a function to skip middleware.\n\tSkipper Skipper\n\n\t// XSSProtection provides protection against cross-site scripting attack (XSS)\n\t// by setting the `X-XSS-Protection` header.\n\t// Optional. Default value \"1; mode=block\".\n\tXSSProtection string\n\n\t// ContentTypeNosniff provides protection against overriding Content-Type\n\t// header by setting the `X-Content-Type-Options` header.\n\t// Optional. Default value \"nosniff\".\n\tContentTypeNosniff string\n\n\t// XFrameOptions can be used to indicate whether or not a browser should\n\t// be allowed to render a page in a <frame>, <iframe> or <object> .\n\t// Sites can use this to avoid clickjacking attacks, by ensuring that their\n\t// content is not embedded into other sites.provides protection against\n\t// clickjacking.\n\t// Optional. Default value \"SAMEORIGIN\".\n\t// Possible values:\n\t// - \"SAMEORIGIN\" - The page can only be displayed in a frame on the same origin as the page itself.\n\t// - \"DENY\" - The page cannot be displayed in a frame, regardless of the site attempting to do so.\n\t// - \"ALLOW-FROM uri\" - The page can only be displayed in a frame on the specified origin.\n\tXFrameOptions string\n\n\t// HSTSMaxAge sets the `Strict-Transport-Security` header to indicate how\n\t// long (in seconds) browsers should remember that this site is only to\n\t// be accessed using HTTPS. This reduces your exposure to some SSL-stripping\n\t// man-in-the-middle (MITM) attacks.\n\t// Optional. Default value 0.\n\tHSTSMaxAge int\n\n\t// HSTSExcludeSubdomains won't include subdomains tag in the `Strict Transport Security`\n\t// header, excluding all subdomains from security policy. It has no effect\n\t// unless HSTSMaxAge is set to a non-zero value.\n\t// Optional. Default value false.\n\tHSTSExcludeSubdomains bool\n\n\t// ContentSecurityPolicy sets the `Content-Security-Policy` header providing\n\t// security against cross-site scripting (XSS), clickjacking and other code\n\t// injection attacks resulting from execution of malicious content in the\n\t// trusted web page context.\n\t// Optional. Default value \"\".\n\tContentSecurityPolicy string\n\n\t// CSPReportOnly would use the `Content-Security-Policy-Report-Only` header instead\n\t// of the `Content-Security-Policy` header. This allows iterative updates of the\n\t// content security policy by only reporting the violations that would\n\t// have occurred instead of blocking the resource.\n\t// Optional. Default value false.\n\tCSPReportOnly bool\n\n\t// HSTSPreloadEnabled will add the preload tag in the `Strict Transport Security`\n\t// header, which enables the domain to be included in the HSTS preload list\n\t// maintained by Chrome (and used by Firefox and Safari): https://hstspreload.org/\n\t// Optional.  Default value false.\n\tHSTSPreloadEnabled bool\n\n\t// ReferrerPolicy sets the `Referrer-Policy` header providing security against\n\t// leaking potentially sensitive request paths to third parties.\n\t// Optional. Default value \"\".\n\tReferrerPolicy string\n}\n\n// DefaultSecureConfig is the default Secure middleware config.\nvar DefaultSecureConfig = SecureConfig{\n\tSkipper:            DefaultSkipper,\n\tXSSProtection:      \"1; mode=block\",\n\tContentTypeNosniff: \"nosniff\",\n\tXFrameOptions:      \"SAMEORIGIN\",\n\tHSTSPreloadEnabled: false,\n}\n\n// Secure returns a Secure middleware.\n// Secure middleware provides protection against cross-site scripting (XSS) attack,\n// content type sniffing, clickjacking, insecure connection and other code injection\n// attacks.\nfunc Secure() echo.MiddlewareFunc {\n\treturn SecureWithConfig(DefaultSecureConfig)\n}\n\n// SecureWithConfig returns a Secure middleware with config or panics on invalid configuration.\nfunc SecureWithConfig(config SecureConfig) echo.MiddlewareFunc {\n\treturn toMiddlewareOrPanic(config)\n}\n\n// ToMiddleware converts SecureConfig to middleware or returns an error for invalid configuration\nfunc (config SecureConfig) ToMiddleware() (echo.MiddlewareFunc, error) {\n\t// Defaults\n\tif config.Skipper == nil {\n\t\tconfig.Skipper = DefaultSecureConfig.Skipper\n\t}\n\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) error {\n\t\t\tif config.Skipper(c) {\n\t\t\t\treturn next(c)\n\t\t\t}\n\n\t\t\treq := c.Request()\n\t\t\tres := c.Response()\n\n\t\t\tif config.XSSProtection != \"\" {\n\t\t\t\tres.Header().Set(echo.HeaderXXSSProtection, config.XSSProtection)\n\t\t\t}\n\t\t\tif config.ContentTypeNosniff != \"\" {\n\t\t\t\tres.Header().Set(echo.HeaderXContentTypeOptions, config.ContentTypeNosniff)\n\t\t\t}\n\t\t\tif config.XFrameOptions != \"\" {\n\t\t\t\tres.Header().Set(echo.HeaderXFrameOptions, config.XFrameOptions)\n\t\t\t}\n\t\t\tif (c.IsTLS() || (req.Header.Get(echo.HeaderXForwardedProto) == \"https\")) && config.HSTSMaxAge != 0 {\n\t\t\t\tsubdomains := \"\"\n\t\t\t\tif !config.HSTSExcludeSubdomains {\n\t\t\t\t\tsubdomains = \"; includeSubdomains\"\n\t\t\t\t}\n\t\t\t\tif config.HSTSPreloadEnabled {\n\t\t\t\t\tsubdomains = fmt.Sprintf(\"%s; preload\", subdomains)\n\t\t\t\t}\n\t\t\t\tres.Header().Set(echo.HeaderStrictTransportSecurity, fmt.Sprintf(\"max-age=%d%s\", config.HSTSMaxAge, subdomains))\n\t\t\t}\n\t\t\tif config.ContentSecurityPolicy != \"\" {\n\t\t\t\tif config.CSPReportOnly {\n\t\t\t\t\tres.Header().Set(echo.HeaderContentSecurityPolicyReportOnly, config.ContentSecurityPolicy)\n\t\t\t\t} else {\n\t\t\t\t\tres.Header().Set(echo.HeaderContentSecurityPolicy, config.ContentSecurityPolicy)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif config.ReferrerPolicy != \"\" {\n\t\t\t\tres.Header().Set(echo.HeaderReferrerPolicy, config.ReferrerPolicy)\n\t\t\t}\n\t\t\treturn next(c)\n\t\t}\n\t}, nil\n}\n"
  },
  {
    "path": "middleware/secure_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSecure(t *testing.T) {\n\te := echo.New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\th := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\n\t// Default\n\terr := Secure()(h)(c)\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, \"1; mode=block\", rec.Header().Get(echo.HeaderXXSSProtection))\n\tassert.Equal(t, \"nosniff\", rec.Header().Get(echo.HeaderXContentTypeOptions))\n\tassert.Equal(t, \"SAMEORIGIN\", rec.Header().Get(echo.HeaderXFrameOptions))\n\tassert.Equal(t, \"\", rec.Header().Get(echo.HeaderStrictTransportSecurity))\n\tassert.Equal(t, \"\", rec.Header().Get(echo.HeaderContentSecurityPolicy))\n\tassert.Equal(t, \"\", rec.Header().Get(echo.HeaderReferrerPolicy))\n}\n\nfunc TestSecureWithConfig(t *testing.T) {\n\te := echo.New()\n\th := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Set(echo.HeaderXForwardedProto, \"https\")\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tmw, err := SecureConfig{\n\t\tXSSProtection:         \"\",\n\t\tContentTypeNosniff:    \"\",\n\t\tXFrameOptions:         \"\",\n\t\tHSTSMaxAge:            3600,\n\t\tContentSecurityPolicy: \"default-src 'self'\",\n\t\tReferrerPolicy:        \"origin\",\n\t}.ToMiddleware()\n\tassert.NoError(t, err)\n\n\terr = mw(h)(c)\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, \"\", rec.Header().Get(echo.HeaderXXSSProtection))\n\tassert.Equal(t, \"\", rec.Header().Get(echo.HeaderXContentTypeOptions))\n\tassert.Equal(t, \"\", rec.Header().Get(echo.HeaderXFrameOptions))\n\tassert.Equal(t, \"max-age=3600; includeSubdomains\", rec.Header().Get(echo.HeaderStrictTransportSecurity))\n\tassert.Equal(t, \"default-src 'self'\", rec.Header().Get(echo.HeaderContentSecurityPolicy))\n\tassert.Equal(t, \"\", rec.Header().Get(echo.HeaderContentSecurityPolicyReportOnly))\n\tassert.Equal(t, \"origin\", rec.Header().Get(echo.HeaderReferrerPolicy))\n\n}\n\nfunc TestSecureWithConfig_CSPReportOnly(t *testing.T) {\n\t// Custom with CSPReportOnly flag\n\te := echo.New()\n\th := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\treq.Header.Set(echo.HeaderXForwardedProto, \"https\")\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\terr := SecureWithConfig(SecureConfig{\n\t\tXSSProtection:         \"\",\n\t\tContentTypeNosniff:    \"\",\n\t\tXFrameOptions:         \"\",\n\t\tHSTSMaxAge:            3600,\n\t\tContentSecurityPolicy: \"default-src 'self'\",\n\t\tCSPReportOnly:         true,\n\t\tReferrerPolicy:        \"origin\",\n\t})(h)(c)\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, \"\", rec.Header().Get(echo.HeaderXXSSProtection))\n\tassert.Equal(t, \"\", rec.Header().Get(echo.HeaderXContentTypeOptions))\n\tassert.Equal(t, \"\", rec.Header().Get(echo.HeaderXFrameOptions))\n\tassert.Equal(t, \"max-age=3600; includeSubdomains\", rec.Header().Get(echo.HeaderStrictTransportSecurity))\n\tassert.Equal(t, \"default-src 'self'\", rec.Header().Get(echo.HeaderContentSecurityPolicyReportOnly))\n\tassert.Equal(t, \"\", rec.Header().Get(echo.HeaderContentSecurityPolicy))\n\tassert.Equal(t, \"origin\", rec.Header().Get(echo.HeaderReferrerPolicy))\n}\n\nfunc TestSecureWithConfig_HSTSPreloadEnabled(t *testing.T) {\n\t// Custom with CSPReportOnly flag\n\te := echo.New()\n\th := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t// Custom, with preload option enabled\n\treq.Header.Set(echo.HeaderXForwardedProto, \"https\")\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\terr := SecureWithConfig(SecureConfig{\n\t\tHSTSMaxAge:         3600,\n\t\tHSTSPreloadEnabled: true,\n\t})(h)(c)\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, \"max-age=3600; includeSubdomains; preload\", rec.Header().Get(echo.HeaderStrictTransportSecurity))\n\n}\n\nfunc TestSecureWithConfig_HSTSExcludeSubdomains(t *testing.T) {\n\t// Custom with CSPReportOnly flag\n\te := echo.New()\n\th := func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"test\")\n\t}\n\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\n\t// Custom, with preload option enabled and subdomains excluded\n\treq.Header.Set(echo.HeaderXForwardedProto, \"https\")\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\n\terr := SecureWithConfig(SecureConfig{\n\t\tHSTSMaxAge:            3600,\n\t\tHSTSPreloadEnabled:    true,\n\t\tHSTSExcludeSubdomains: true,\n\t})(h)(c)\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, \"max-age=3600; preload\", rec.Header().Get(echo.HeaderStrictTransportSecurity))\n}\n"
  },
  {
    "path": "middleware/slash.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\n// AddTrailingSlashConfig is the middleware config for adding trailing slash to the request.\ntype AddTrailingSlashConfig struct {\n\t// Skipper defines a function to skip middleware.\n\tSkipper Skipper\n\n\t// Status code to be used when redirecting the request.\n\t// Optional, but when provided the request is redirected using this code.\n\t// Valid status codes: [300...308]\n\tRedirectCode int\n}\n\n// AddTrailingSlash returns a root level (before router) middleware which adds a\n// trailing slash to the request `URL#Path`.\n//\n// Usage `Echo#Pre(AddTrailingSlash())`\nfunc AddTrailingSlash() echo.MiddlewareFunc {\n\treturn AddTrailingSlashWithConfig(AddTrailingSlashConfig{})\n}\n\n// AddTrailingSlashWithConfig returns an AddTrailingSlash middleware with config or panics on invalid configuration.\nfunc AddTrailingSlashWithConfig(config AddTrailingSlashConfig) echo.MiddlewareFunc {\n\treturn toMiddlewareOrPanic(config)\n}\n\n// ToMiddleware converts AddTrailingSlashConfig to middleware or returns an error for invalid configuration\nfunc (config AddTrailingSlashConfig) ToMiddleware() (echo.MiddlewareFunc, error) {\n\tif config.Skipper == nil {\n\t\tconfig.Skipper = DefaultSkipper\n\t}\n\tif config.RedirectCode != 0 && (config.RedirectCode < http.StatusMultipleChoices || config.RedirectCode > http.StatusPermanentRedirect) {\n\t\t// this is same check as `echo.context.Redirect()` does, but we can check this before even serving the request.\n\t\treturn nil, errors.New(\"invalid redirect code for add trailing slash middleware\")\n\t}\n\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) error {\n\t\t\tif config.Skipper(c) {\n\t\t\t\treturn next(c)\n\t\t\t}\n\n\t\t\treq := c.Request()\n\t\t\turl := req.URL\n\t\t\tpath := url.Path\n\t\t\tqs := c.QueryString()\n\t\t\tif !strings.HasSuffix(path, \"/\") {\n\t\t\t\tpath += \"/\"\n\t\t\t\turi := path\n\t\t\t\tif qs != \"\" {\n\t\t\t\t\turi += \"?\" + qs\n\t\t\t\t}\n\n\t\t\t\t// Redirect\n\t\t\t\tif config.RedirectCode != 0 {\n\t\t\t\t\treturn c.Redirect(config.RedirectCode, sanitizeURI(uri))\n\t\t\t\t}\n\n\t\t\t\t// Forward\n\t\t\t\treq.RequestURI = uri\n\t\t\t\turl.Path = path\n\t\t\t}\n\t\t\treturn next(c)\n\t\t}\n\t}, nil\n}\n\n// RemoveTrailingSlashConfig is the middleware config for removing trailing slash from the request.\ntype RemoveTrailingSlashConfig struct {\n\t// Skipper defines a function to skip middleware.\n\tSkipper Skipper\n\n\t// Status code to be used when redirecting the request.\n\t// Optional, but when provided the request is redirected using this code.\n\tRedirectCode int\n}\n\n// RemoveTrailingSlash returns a root level (before router) middleware which removes\n// a trailing slash from the request URI.\n//\n// Usage `Echo#Pre(RemoveTrailingSlash())`\nfunc RemoveTrailingSlash() echo.MiddlewareFunc {\n\treturn RemoveTrailingSlashWithConfig(RemoveTrailingSlashConfig{})\n}\n\n// RemoveTrailingSlashWithConfig returns a RemoveTrailingSlash middleware with config or panics on invalid configuration.\nfunc RemoveTrailingSlashWithConfig(config RemoveTrailingSlashConfig) echo.MiddlewareFunc {\n\treturn toMiddlewareOrPanic(config)\n}\n\n// ToMiddleware converts RemoveTrailingSlashConfig to middleware or returns an error for invalid configuration\nfunc (config RemoveTrailingSlashConfig) ToMiddleware() (echo.MiddlewareFunc, error) {\n\tif config.Skipper == nil {\n\t\tconfig.Skipper = DefaultSkipper\n\t}\n\tif config.RedirectCode != 0 && (config.RedirectCode < http.StatusMultipleChoices || config.RedirectCode > http.StatusPermanentRedirect) {\n\t\t// this is same check as `echo.context.Redirect()` does, but we can check this before even serving the request.\n\t\treturn nil, errors.New(\"invalid redirect code for remove trailing slash middleware\")\n\t}\n\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) error {\n\t\t\tif config.Skipper(c) {\n\t\t\t\treturn next(c)\n\t\t\t}\n\n\t\t\treq := c.Request()\n\t\t\turl := req.URL\n\t\t\tpath := url.Path\n\t\t\tqs := c.QueryString()\n\t\t\tl := len(path) - 1\n\t\t\tif l > 0 && strings.HasSuffix(path, \"/\") {\n\t\t\t\tpath = path[:l]\n\t\t\t\turi := path\n\t\t\t\tif qs != \"\" {\n\t\t\t\t\turi += \"?\" + qs\n\t\t\t\t}\n\n\t\t\t\t// Redirect\n\t\t\t\tif config.RedirectCode != 0 {\n\t\t\t\t\treturn c.Redirect(config.RedirectCode, sanitizeURI(uri))\n\t\t\t\t}\n\n\t\t\t\t// Forward\n\t\t\t\treq.RequestURI = uri\n\t\t\t\turl.Path = path\n\t\t\t}\n\t\t\treturn next(c)\n\t\t}\n\t}, nil\n}\n\nfunc sanitizeURI(uri string) string {\n\t// double slash `\\\\`, `//` or even `\\/` is absolute uri for browsers and by redirecting request to that uri\n\t// we are vulnerable to open redirect attack. so replace all slashes from the beginning with single slash\n\tif len(uri) > 1 && (uri[0] == '\\\\' || uri[0] == '/') && (uri[1] == '\\\\' || uri[1] == '/') {\n\t\turi = \"/\" + strings.TrimLeft(uri, `/\\`)\n\t}\n\treturn uri\n}\n"
  },
  {
    "path": "middleware/slash_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestAddTrailingSlashWithConfig(t *testing.T) {\n\tvar testCases = []struct {\n\t\twhenURL        string\n\t\twhenMethod     string\n\t\texpectPath     string\n\t\texpectLocation []string\n\t\texpectStatus   int\n\t}{\n\t\t{\n\t\t\twhenURL:        \"/add-slash\",\n\t\t\twhenMethod:     http.MethodGet,\n\t\t\texpectPath:     \"/add-slash\",\n\t\t\texpectLocation: []string{`/add-slash/`},\n\t\t},\n\t\t{\n\t\t\twhenURL:        \"/add-slash?key=value\",\n\t\t\twhenMethod:     http.MethodGet,\n\t\t\texpectPath:     \"/add-slash\",\n\t\t\texpectLocation: []string{`/add-slash/?key=value`},\n\t\t},\n\t\t{\n\t\t\twhenURL:        \"/\",\n\t\t\twhenMethod:     http.MethodConnect,\n\t\t\texpectPath:     \"/\",\n\t\t\texpectLocation: nil,\n\t\t\texpectStatus:   http.StatusOK,\n\t\t},\n\t\t// cases for open redirect vulnerability\n\t\t{\n\t\t\twhenURL:        \"http://localhost:1323/%5Cexample.com\",\n\t\t\texpectPath:     `/\\example.com`,\n\t\t\texpectLocation: []string{`/example.com/`},\n\t\t},\n\t\t{\n\t\t\twhenURL:        `http://localhost:1323/\\example.com`,\n\t\t\texpectPath:     `/\\example.com`,\n\t\t\texpectLocation: []string{`/example.com/`},\n\t\t},\n\t\t{\n\t\t\twhenURL:        `http://localhost:1323/\\\\%5C////%5C\\\\\\example.com`,\n\t\t\texpectPath:     `/\\\\\\////\\\\\\\\example.com`,\n\t\t\texpectLocation: []string{`/example.com/`},\n\t\t},\n\t\t{\n\t\t\twhenURL:        \"http://localhost:1323//example.com\",\n\t\t\texpectPath:     `//example.com`,\n\t\t\texpectLocation: []string{`/example.com/`},\n\t\t},\n\t\t{\n\t\t\twhenURL:        \"http://localhost:1323/%5C%5C\",\n\t\t\texpectPath:     `/\\\\`,\n\t\t\texpectLocation: []string{`/`},\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenURL, func(t *testing.T) {\n\t\t\te := echo.New()\n\n\t\t\tmw := AddTrailingSlashWithConfig(AddTrailingSlashConfig{\n\t\t\t\tRedirectCode: http.StatusMovedPermanently,\n\t\t\t})\n\t\t\th := mw(func(c *echo.Context) error {\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\trec := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(tc.whenMethod, tc.whenURL, nil)\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\terr := h(c)\n\t\t\tassert.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.expectPath, req.URL.Path)\n\t\t\tassert.Equal(t, tc.expectLocation, rec.Header()[echo.HeaderLocation])\n\t\t\tif tc.expectStatus == 0 {\n\t\t\t\tassert.Equal(t, http.StatusMovedPermanently, rec.Code)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, tc.expectStatus, rec.Code)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAddTrailingSlash(t *testing.T) {\n\tvar testCases = []struct {\n\t\twhenURL        string\n\t\twhenMethod     string\n\t\texpectPath     string\n\t\texpectLocation []string\n\t}{\n\t\t{\n\t\t\twhenURL:    \"/add-slash\",\n\t\t\twhenMethod: http.MethodGet,\n\t\t\texpectPath: \"/add-slash/\",\n\t\t},\n\t\t{\n\t\t\twhenURL:    \"/add-slash?key=value\",\n\t\t\twhenMethod: http.MethodGet,\n\t\t\texpectPath: \"/add-slash/\",\n\t\t},\n\t\t{\n\t\t\twhenURL:        \"/\",\n\t\t\twhenMethod:     http.MethodConnect,\n\t\t\texpectPath:     \"/\",\n\t\t\texpectLocation: nil,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenURL, func(t *testing.T) {\n\t\t\te := echo.New()\n\n\t\t\th := AddTrailingSlash()(func(c *echo.Context) error {\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\trec := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(tc.whenMethod, tc.whenURL, nil)\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\terr := h(c)\n\t\t\tassert.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.expectPath, req.URL.Path)\n\t\t\tassert.Equal(t, []string(nil), rec.Header()[echo.HeaderLocation])\n\t\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\t})\n\t}\n}\n\nfunc TestRemoveTrailingSlashWithConfig(t *testing.T) {\n\tvar testCases = []struct {\n\t\twhenURL        string\n\t\twhenMethod     string\n\t\texpectPath     string\n\t\texpectLocation []string\n\t\texpectStatus   int\n\t}{\n\t\t{\n\t\t\twhenURL:        \"/remove-slash/\",\n\t\t\twhenMethod:     http.MethodGet,\n\t\t\texpectPath:     \"/remove-slash/\",\n\t\t\texpectLocation: []string{`/remove-slash`},\n\t\t},\n\t\t{\n\t\t\twhenURL:        \"/remove-slash/?key=value\",\n\t\t\twhenMethod:     http.MethodGet,\n\t\t\texpectPath:     \"/remove-slash/\",\n\t\t\texpectLocation: []string{`/remove-slash?key=value`},\n\t\t},\n\t\t{\n\t\t\twhenURL:        \"/\",\n\t\t\twhenMethod:     http.MethodConnect,\n\t\t\texpectPath:     \"/\",\n\t\t\texpectLocation: nil,\n\t\t\texpectStatus:   http.StatusOK,\n\t\t},\n\t\t{\n\t\t\twhenURL:        \"http://localhost\",\n\t\t\twhenMethod:     http.MethodGet,\n\t\t\texpectPath:     \"\",\n\t\t\texpectLocation: nil,\n\t\t\texpectStatus:   http.StatusOK,\n\t\t},\n\t\t// cases for open redirect vulnerability\n\t\t{\n\t\t\twhenURL:        \"http://localhost:1323/%5Cexample.com/\",\n\t\t\texpectPath:     `/\\example.com/`,\n\t\t\texpectLocation: []string{`/example.com`},\n\t\t},\n\t\t{\n\t\t\twhenURL:        `http://localhost:1323/\\example.com/`,\n\t\t\texpectPath:     `/\\example.com/`,\n\t\t\texpectLocation: []string{`/example.com`},\n\t\t},\n\t\t{\n\t\t\twhenURL:        `http://localhost:1323/\\\\%5C////%5C\\\\\\example.com/`,\n\t\t\texpectPath:     `/\\\\\\////\\\\\\\\example.com/`,\n\t\t\texpectLocation: []string{`/example.com`},\n\t\t},\n\t\t{\n\t\t\twhenURL:        \"http://localhost:1323//example.com/\",\n\t\t\texpectPath:     `//example.com/`,\n\t\t\texpectLocation: []string{`/example.com`},\n\t\t},\n\t\t{\n\t\t\twhenURL:        \"http://localhost:1323/%5C%5C/\",\n\t\t\texpectPath:     `/\\\\/`,\n\t\t\texpectLocation: []string{`/`},\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenURL, func(t *testing.T) {\n\t\t\te := echo.New()\n\n\t\t\tmw := RemoveTrailingSlashWithConfig(RemoveTrailingSlashConfig{\n\t\t\t\tRedirectCode: http.StatusMovedPermanently,\n\t\t\t})\n\t\t\th := mw(func(c *echo.Context) error {\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\trec := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(tc.whenMethod, tc.whenURL, nil)\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\terr := h(c)\n\t\t\tassert.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.expectPath, req.URL.Path)\n\t\t\tassert.Equal(t, tc.expectLocation, rec.Header()[echo.HeaderLocation])\n\t\t\tif tc.expectStatus == 0 {\n\t\t\t\tassert.Equal(t, http.StatusMovedPermanently, rec.Code)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, tc.expectStatus, rec.Code)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRemoveTrailingSlash(t *testing.T) {\n\tvar testCases = []struct {\n\t\twhenURL    string\n\t\twhenMethod string\n\t\texpectPath string\n\t}{\n\t\t{\n\t\t\twhenURL:    \"/remove-slash/\",\n\t\t\twhenMethod: http.MethodGet,\n\t\t\texpectPath: \"/remove-slash\",\n\t\t},\n\t\t{\n\t\t\twhenURL:    \"/remove-slash/?key=value\",\n\t\t\twhenMethod: http.MethodGet,\n\t\t\texpectPath: \"/remove-slash\",\n\t\t},\n\t\t{\n\t\t\twhenURL:    \"/\",\n\t\t\twhenMethod: http.MethodConnect,\n\t\t\texpectPath: \"/\",\n\t\t},\n\t\t{\n\t\t\twhenURL:    \"http://localhost\",\n\t\t\twhenMethod: http.MethodGet,\n\t\t\texpectPath: \"\",\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenURL, func(t *testing.T) {\n\t\t\te := echo.New()\n\n\t\t\th := RemoveTrailingSlash()(func(c *echo.Context) error {\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\trec := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(tc.whenMethod, tc.whenURL, nil)\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\terr := h(c)\n\t\t\tassert.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.expectPath, req.URL.Path)\n\t\t\tassert.Equal(t, []string(nil), rec.Header()[echo.HeaderLocation])\n\t\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "middleware/static.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"html/template\"\n\t\"io\"\n\t\"io/fs\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/labstack/echo/v5\"\n)\n\n// StaticConfig defines the config for Static middleware.\ntype StaticConfig struct {\n\t// Skipper defines a function to skip middleware.\n\tSkipper Skipper\n\n\t// Root directory from where the static content is served (relative to given Filesystem).\n\t// `Root: \".\"` means root folder from Filesystem.\n\t// Required.\n\tRoot string\n\n\t// Filesystem provides access to the static content.\n\t// Optional. Defaults to echo.Filesystem (serves files from `.` folder where executable is started)\n\tFilesystem fs.FS\n\n\t// Index file for serving a directory.\n\t// Optional. Default value \"index.html\".\n\tIndex string\n\n\t// Enable HTML5 mode by forwarding all not-found requests to root so that\n\t// SPA (single-page application) can handle the routing.\n\t// Optional. Default value false.\n\tHTML5 bool\n\n\t// Enable directory browsing.\n\t// Optional. Default value false.\n\tBrowse bool\n\n\t// Enable ignoring of the base of the URL path.\n\t// Example: when assigning a static middleware to a non root path group,\n\t// the filesystem path is not doubled\n\t// Optional. Default value false.\n\tIgnoreBase bool\n\n\t// DisablePathUnescaping disables path parameter (param: *) unescaping. This is useful when router is set to unescape\n\t// all parameter and doing it again in this middleware would corrupt filename that is requested.\n\tDisablePathUnescaping bool\n\n\t// DirectoryListTemplate is template to list directory contents\n\t// Optional. Default to `directoryListHTMLTemplate` constant below.\n\tDirectoryListTemplate string\n}\n\nconst directoryListHTMLTemplate = `\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\n  <title>{{ .Name }}</title>\n  <style>\n    body {\n\t\t\tfont-family: Menlo, Consolas, monospace;\n\t\t\tpadding: 48px;\n\t\t}\n\t\theader {\n\t\t\tpadding: 4px 16px;\n\t\t\tfont-size: 24px;\n\t\t}\n    ul {\n\t\t\tlist-style-type: none;\n\t\t\tmargin: 0;\n    \tpadding: 20px 0 0 0;\n\t\t\tdisplay: flex;\n\t\t\tflex-wrap: wrap;\n    }\n    li {\n\t\t\twidth: 300px;\n\t\t\tpadding: 16px;\n\t\t}\n\t\tli a {\n\t\t\tdisplay: block;\n\t\t\toverflow: hidden;\n\t\t\twhite-space: nowrap;\n\t\t\ttext-overflow: ellipsis;\n\t\t\ttext-decoration: none;\n\t\t\ttransition: opacity 0.25s;\n\t\t}\n\t\tli span {\n\t\t\tcolor: #707070;\n\t\t\tfont-size: 12px;\n\t\t}\n\t\tli a:hover {\n\t\t\topacity: 0.50;\n\t\t}\n\t\t.dir {\n\t\t\tcolor: #E91E63;\n\t\t}\n\t\t.file {\n\t\t\tcolor: #673AB7;\n\t\t}\n  </style>\n</head>\n<body>\n\t<header>\n\t\t{{ .Name }}\n\t</header>\n\t<ul>\n\t\t{{ range .Files }}\n\t\t<li>\n\t\t{{ if .Dir }}\n\t\t\t{{ $name := print .Name \"/\" }}\n\t\t\t<a class=\"dir\" href=\"{{ $name }}\">{{ $name }}</a>\n\t\t\t{{ else }}\n\t\t\t<a class=\"file\" href=\"{{ .Name }}\">{{ .Name }}</a>\n\t\t\t<span>{{ .Size }}</span>\n\t\t{{ end }}\n\t\t</li>\n\t\t{{ end }}\n  </ul>\n</body>\n</html>\n`\n\n// DefaultStaticConfig is the default Static middleware config.\nvar DefaultStaticConfig = StaticConfig{\n\tSkipper: DefaultSkipper,\n\tIndex:   \"index.html\",\n}\n\n// Static returns a Static middleware to serves static content from the provided root directory.\nfunc Static(root string) echo.MiddlewareFunc {\n\tc := DefaultStaticConfig\n\tc.Root = root\n\treturn StaticWithConfig(c)\n}\n\n// StaticWithConfig returns a Static middleware to serves static content or panics on invalid configuration.\nfunc StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {\n\treturn toMiddlewareOrPanic(config)\n}\n\n// ToMiddleware converts StaticConfig to middleware or returns an error for invalid configuration\nfunc (config StaticConfig) ToMiddleware() (echo.MiddlewareFunc, error) {\n\t// Defaults\n\tif config.Root == \"\" {\n\t\tconfig.Root = \".\" // For security we want to restrict to CWD.\n\t} else {\n\t\tconfig.Root = path.Clean(config.Root) // fs.Open is very picky about ``, `.`, `..` in paths, so remove some of them up.\n\t}\n\n\tif config.Skipper == nil {\n\t\tconfig.Skipper = DefaultStaticConfig.Skipper\n\t}\n\tif config.Index == \"\" {\n\t\tconfig.Index = DefaultStaticConfig.Index\n\t}\n\tif config.DirectoryListTemplate == \"\" {\n\t\tconfig.DirectoryListTemplate = directoryListHTMLTemplate\n\t}\n\n\tdirListTemplate, tErr := template.New(\"index\").Parse(config.DirectoryListTemplate)\n\tif tErr != nil {\n\t\treturn nil, fmt.Errorf(\"echo static middleware directory list template parsing error: %w\", tErr)\n\t}\n\n\tvar once *sync.Once\n\tvar fsErr error\n\tcurrentFS := config.Filesystem\n\tif config.Filesystem == nil {\n\t\tonce = &sync.Once{}\n\t} else if config.Root != \".\" {\n\t\ttmpFs, fErr := fs.Sub(config.Filesystem, path.Join(\".\", config.Root))\n\t\tif fErr != nil {\n\t\t\treturn nil, fmt.Errorf(\"static middleware failed to create sub-filesystem from config.Root, error: %w\", fErr)\n\t\t}\n\t\tcurrentFS = tmpFs\n\t}\n\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c *echo.Context) (err error) {\n\t\t\tif config.Skipper(c) {\n\t\t\t\treturn next(c)\n\t\t\t}\n\n\t\t\tp := c.Request().URL.Path\n\t\t\tpathUnescape := true\n\t\t\tif strings.HasSuffix(c.Path(), \"*\") { // When serving from a group, e.g. `/static*`.\n\t\t\t\tp = c.Param(\"*\")\n\t\t\t\tpathUnescape = !config.DisablePathUnescaping // because router could already do PathUnescape\n\t\t\t}\n\t\t\tif pathUnescape {\n\t\t\t\tp, err = url.PathUnescape(p)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Security: We use path.Clean() (not filepath.Clean()) because:\n\t\t\t// 1. HTTP URLs always use forward slashes, regardless of server OS\n\t\t\t// 2. path.Clean() provides platform-independent behavior for URL paths\n\t\t\t// 3. The \"/\" prefix forces absolute path interpretation, removing \"..\" components\n\t\t\t// 4. Backslashes are treated as literal characters (not path separators), preventing traversal\n\t\t\t// See static_windows.go for Go 1.20+ filepath.Clean compatibility notes\n\t\t\tfilePath := path.Clean(\"./\" + p)\n\n\t\t\tif config.IgnoreBase {\n\t\t\t\troutePath := path.Base(strings.TrimRight(c.Path(), \"/*\"))\n\t\t\t\tbaseURLPath := path.Base(p)\n\t\t\t\tif baseURLPath == routePath {\n\t\t\t\t\ti := strings.LastIndex(filePath, routePath)\n\t\t\t\t\tfilePath = filePath[:i] + strings.Replace(filePath[i:], routePath, \"\", 1)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif once != nil {\n\t\t\t\tonce.Do(func() {\n\t\t\t\t\tif tmp, tmpErr := fs.Sub(c.Echo().Filesystem, config.Root); tmpErr != nil {\n\t\t\t\t\t\tfsErr = fmt.Errorf(\"static middleware failed to create sub-filesystem: %w\", tmpErr)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcurrentFS = tmp\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\tif fsErr != nil {\n\t\t\t\t\treturn fsErr\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfile, err := currentFS.Open(filePath)\n\t\t\tif err != nil {\n\t\t\t\tif !isIgnorableOpenFileError(err) {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\t// file with that path did not exist, so we continue down in middleware/handler chain, hoping that we end up in\n\t\t\t\t// handler that is meant to handle this request\n\t\t\t\terr = next(c)\n\t\t\t\tif err == nil {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tvar he echo.HTTPStatusCoder\n\t\t\t\tif !(errors.As(err, &he) && config.HTML5 && he.StatusCode() == http.StatusNotFound) {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\t// is case HTML5 mode is enabled + echo 404 we serve index to the client\n\t\t\t\tfile, err = currentFS.Open(config.Index)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tdefer file.Close()\n\n\t\t\tinfo, err := file.Stat()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif info.IsDir() {\n\t\t\t\tindex, err := currentFS.Open(path.Join(filePath, config.Index))\n\t\t\t\tif err != nil {\n\t\t\t\t\tif config.Browse {\n\t\t\t\t\t\treturn listDir(dirListTemplate, filePath, currentFS, c.Response())\n\t\t\t\t\t}\n\n\t\t\t\t\treturn next(c)\n\t\t\t\t}\n\n\t\t\t\tdefer index.Close()\n\n\t\t\t\tinfo, err = index.Stat()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\treturn serveFile(c, index, info)\n\t\t\t}\n\n\t\t\treturn serveFile(c, file, info)\n\t\t}\n\t}, nil\n}\n\nfunc serveFile(c *echo.Context, file fs.File, info os.FileInfo) error {\n\tff, ok := file.(io.ReadSeeker)\n\tif !ok {\n\t\treturn errors.New(\"file does not implement io.ReadSeeker\")\n\t}\n\thttp.ServeContent(c.Response(), c.Request(), info.Name(), info.ModTime(), ff)\n\treturn nil\n}\n\nfunc listDir(t *template.Template, pathInFs string, filesystem fs.FS, res http.ResponseWriter) error {\n\tfiles, err := fs.ReadDir(filesystem, pathInFs)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"static middleware failed to read directory for listing: %w\", err)\n\t}\n\n\t// Create directory index\n\tres.Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8)\n\tdata := struct {\n\t\tName  string\n\t\tFiles []any\n\t}{\n\t\tName: pathInFs,\n\t}\n\n\tfor _, f := range files {\n\t\tvar size int64\n\t\tif !f.IsDir() {\n\t\t\tinfo, err := f.Info()\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"static middleware failed to get file info for listing: %w\", err)\n\t\t\t}\n\t\t\tsize = info.Size()\n\t\t}\n\n\t\tdata.Files = append(data.Files, struct {\n\t\t\tName string\n\t\t\tDir  bool\n\t\t\tSize string\n\t\t}{f.Name(), f.IsDir(), format(size)})\n\t}\n\n\treturn t.Execute(res, data)\n}\n\n// format formats bytes integer to human readable string.\n// For example, 31323 bytes will return 30.59KB.\nfunc format(b int64) string {\n\tmultiple := \"\"\n\tvalue := float64(b)\n\n\tswitch {\n\tcase b >= EB:\n\t\tvalue /= float64(EB)\n\t\tmultiple = \"EB\"\n\tcase b >= PB:\n\t\tvalue /= float64(PB)\n\t\tmultiple = \"PB\"\n\tcase b >= TB:\n\t\tvalue /= float64(TB)\n\t\tmultiple = \"TB\"\n\tcase b >= GB:\n\t\tvalue /= float64(GB)\n\t\tmultiple = \"GB\"\n\tcase b >= MB:\n\t\tvalue /= float64(MB)\n\t\tmultiple = \"MB\"\n\tcase b >= KB:\n\t\tvalue /= float64(KB)\n\t\tmultiple = \"KB\"\n\tcase b == 0:\n\t\treturn \"0\"\n\tdefault:\n\t\treturn strconv.FormatInt(b, 10) + \"B\"\n\t}\n\n\treturn fmt.Sprintf(\"%.2f%s\", value, multiple)\n}\n"
  },
  {
    "path": "middleware/static_other.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"errors\"\n\t\"io/fs\"\n\t\"os\"\n)\n\n// We ignore these errors as there could be handler that matches request path.\nfunc isIgnorableOpenFileError(err error) bool {\n\tif os.IsNotExist(err) {\n\t\treturn true\n\t}\n\t// As of Go 1.20 Windows path checks are more strict on the provided path and considers [UNC](https://en.wikipedia.org/wiki/Path_(computing)#UNC)\n\t// paths with missing host etc parts as invalid. Previously it would result you `fs.ErrNotExist`.\n\t// Also `fs.Open` on all OSes does not accept ``, `.`, `..` at all.\n\t//\n\t// so we need to treat those errors the same as `fs.ErrNotExists` so we can continue handling\n\t// errors in the middleware/handler chain. Otherwise we might end up with status 500 instead of finding a route\n\t// or return 404 not found.\n\tvar pErr *fs.PathError\n\tif errors.As(err, &pErr) {\n\t\terr = pErr.Err\n\t\treturn err.Error() == \"invalid argument\"\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "middleware/static_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"io/fs\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"testing\"\n\t\"testing/fstest\"\n\n\t\"github.com/labstack/echo/v5\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestStatic_useCaseForApiAndSPAs(t *testing.T) {\n\te := echo.New()\n\n\t// serve single page application (SPA) files from server root\n\te.Use(StaticWithConfig(StaticConfig{\n\t\tRoot: \"testdata/dist/public\",\n\t}))\n\n\t// all requests to `/api/*` will end up in echo handlers (assuming there is not `api` folder and files)\n\tapi := e.Group(\"/api\")\n\tusers := api.Group(\"/users\")\n\tusers.GET(\"/info\", func(c *echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"users info\")\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/api/users/info\", nil)\n\trec := httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\tassert.Equal(t, http.StatusOK, rec.Code)\n\tassert.Equal(t, \"users info\", rec.Body.String())\n\n\treq = httptest.NewRequest(http.MethodGet, \"/index.html\", nil)\n\trec = httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\tassert.Equal(t, http.StatusOK, rec.Code)\n\tassert.Contains(t, rec.Body.String(), \"<h1>Hello from index</h1>\\n\")\n\n}\n\nfunc TestStatic(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname                 string\n\t\tgivenConfig          *StaticConfig\n\t\tgivenAttachedToGroup string\n\t\twhenURL              string\n\t\texpectContains       string\n\t\texpectNotContains    string\n\t\texpectLength         string\n\t\texpectCode           int\n\t}{\n\t\t{\n\t\t\tname:           \"ok, serve index with Echo message\",\n\t\t\twhenURL:        \"/\",\n\t\t\texpectCode:     http.StatusOK,\n\t\t\texpectContains: \"<h1>Hello from index</h1>\",\n\t\t},\n\t\t{\n\t\t\tname:           \"ok, serve file from subdirectory\",\n\t\t\twhenURL:        \"/assets/readme.md\",\n\t\t\texpectCode:     http.StatusOK,\n\t\t\texpectContains: \"This directory is used for the static middleware test\",\n\t\t},\n\t\t{\n\t\t\tname: \"ok, when html5 mode serve index for any static file that does not exist\",\n\t\t\tgivenConfig: &StaticConfig{\n\t\t\t\tRoot:  \"testdata/dist/public\",\n\t\t\t\tHTML5: true,\n\t\t\t},\n\t\t\twhenURL:        \"/random\",\n\t\t\texpectCode:     http.StatusOK,\n\t\t\texpectContains: \"<h1>Hello from index</h1>\",\n\t\t},\n\t\t{\n\t\t\tname: \"ok, serve index as directory index listing files directory\",\n\t\t\tgivenConfig: &StaticConfig{\n\t\t\t\tRoot:   \"testdata/dist/public/assets\",\n\t\t\t\tBrowse: true,\n\t\t\t},\n\t\t\twhenURL:        \"/\",\n\t\t\texpectCode:     http.StatusOK,\n\t\t\texpectContains: `<a class=\"file\" href=\"readme.md\">readme.md</a>`,\n\t\t},\n\t\t{\n\t\t\tname: \"ok, serve directory index with IgnoreBase and browse\",\n\t\t\tgivenConfig: &StaticConfig{\n\t\t\t\tRoot:       \"testdata/dist/public/assets/\", // <-- last `assets/` is overlapping with group path and needs to be ignored\n\t\t\t\tIgnoreBase: true,\n\t\t\t\tBrowse:     true,\n\t\t\t},\n\t\t\tgivenAttachedToGroup: \"/assets\",\n\t\t\twhenURL:              \"/assets/\",\n\t\t\texpectCode:           http.StatusOK,\n\t\t\texpectContains:       `<a class=\"file\" href=\"readme.md\">readme.md</a>`,\n\t\t},\n\t\t{\n\t\t\tname: \"ok, serve file with IgnoreBase\",\n\t\t\tgivenConfig: &StaticConfig{\n\t\t\t\tRoot:       \"testdata/dist/public/assets\", // <-- last `assets/` is overlapping with group path and needs to be ignored\n\t\t\t\tIgnoreBase: true,\n\t\t\t\tBrowse:     true,\n\t\t\t},\n\t\t\tgivenAttachedToGroup: \"/assets\",\n\t\t\twhenURL:              \"/assets/readme.md\",\n\t\t\texpectCode:           http.StatusOK,\n\t\t\texpectContains:       \"This directory is used for the static middleware test\",\n\t\t},\n\t\t{\n\t\t\tname:           \"nok, file not found\",\n\t\t\twhenURL:        \"/none\",\n\t\t\texpectCode:     http.StatusNotFound,\n\t\t\texpectContains: \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"ok, when no file then a handler will care of the request\",\n\t\t\twhenURL:        \"/regular-handler\",\n\t\t\texpectCode:     http.StatusOK,\n\t\t\texpectContains: \"ok\",\n\t\t},\n\t\t{\n\t\t\tname: \"ok, skip middleware and serve handler\",\n\t\t\tgivenConfig: &StaticConfig{\n\t\t\t\tRoot: \"testdata/dist/public\",\n\t\t\t\tSkipper: func(c *echo.Context) bool {\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t},\n\t\t\twhenURL:        \"/walle.png\",\n\t\t\texpectCode:     http.StatusTeapot,\n\t\t\texpectContains: \"walle\",\n\t\t},\n\t\t{\n\t\t\tname: \"nok, when html5 fail if the index file does not exist\",\n\t\t\tgivenConfig: &StaticConfig{\n\t\t\t\tRoot:  \"testdata/dist/public\",\n\t\t\t\tHTML5: true,\n\t\t\t\tIndex: \"missing.html\", // that folder contains `index.html`\n\t\t\t},\n\t\t\twhenURL:    \"/random\",\n\t\t\texpectCode: http.StatusInternalServerError,\n\t\t},\n\t\t{\n\t\t\tname: \"ok, serve from http.FileSystem\",\n\t\t\tgivenConfig: &StaticConfig{\n\t\t\t\tRoot:       \"public\",\n\t\t\t\tFilesystem: os.DirFS(\"testdata/dist\"),\n\t\t\t},\n\t\t\twhenURL:        \"/\",\n\t\t\texpectCode:     http.StatusOK,\n\t\t\texpectContains: \"<h1>Hello from index</h1>\",\n\t\t},\n\t\t{\n\t\t\tname:              \"nok, do not allow directory traversal (backslash - windows separator)\",\n\t\t\twhenURL:           `/..\\\\private.txt`,\n\t\t\texpectCode:        http.StatusNotFound,\n\t\t\texpectContains:    \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t\texpectNotContains: `private file`,\n\t\t},\n\t\t{\n\t\t\tname:              \"nok,do not allow directory traversal (slash - unix separator)\",\n\t\t\twhenURL:           `/../private.txt`,\n\t\t\texpectCode:        http.StatusNotFound,\n\t\t\texpectContains:    \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t\texpectNotContains: `private file`,\n\t\t},\n\t\t{\n\t\t\tname:              \"nok, URL encoded path traversal (single encoding, slash - unix separator)\",\n\t\t\twhenURL:           \"/%2e%2e%2fprivate.txt\",\n\t\t\texpectCode:        http.StatusNotFound,\n\t\t\texpectContains:    \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t\texpectNotContains: `private file`,\n\t\t},\n\t\t{\n\t\t\tname:              \"nok, URL encoded path traversal (single encoding, backslash - windows separator)\",\n\t\t\twhenURL:           \"/%2e%2e%5cprivate.txt\",\n\t\t\texpectCode:        http.StatusNotFound,\n\t\t\texpectContains:    \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t\texpectNotContains: `private file`,\n\t\t},\n\t\t{\n\t\t\tname:              \"nok, URL encoded path traversal (double encoding, slash - unix separator)\",\n\t\t\twhenURL:           \"/%252e%252e%252fprivate.txt\",\n\t\t\texpectCode:        http.StatusNotFound,\n\t\t\texpectContains:    \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t\texpectNotContains: `private file`,\n\t\t},\n\t\t{\n\t\t\tname:              \"nok, URL encoded path traversal (double encoding, backslash - windows separator)\",\n\t\t\twhenURL:           \"/%252e%252e%255cprivate.txt\",\n\t\t\texpectCode:        http.StatusNotFound,\n\t\t\texpectContains:    \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t\texpectNotContains: `private file`,\n\t\t},\n\t\t{\n\t\t\tname:              \"nok, URL encoded path traversal (mixed encoding)\",\n\t\t\twhenURL:           \"/%2e%2e/private.txt\",\n\t\t\texpectCode:        http.StatusNotFound,\n\t\t\texpectContains:    \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t\texpectNotContains: `private file`,\n\t\t},\n\t\t{\n\t\t\tname:              \"nok, backslash URL encoded\",\n\t\t\twhenURL:           \"/..%5c..%5cprivate.txt\",\n\t\t\texpectCode:        http.StatusNotFound,\n\t\t\texpectContains:    \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t\texpectNotContains: `private file`,\n\t\t},\n\t\t//{ // Under windows, %00 gets cleaned out by `http.ReadRequest` making this test to fail with different code\n\t\t//\tname:           \"nok, null byte injection\",\n\t\t//\twhenURL:        \"/index.html%00.jpg\",\n\t\t//\texpectCode:     http.StatusInternalServerError,\n\t\t//\texpectContains: \"{\\\"message\\\":\\\"Internal Server Error\\\"}\\n\",\n\t\t//},\n\t\t{\n\t\t\tname:              \"nok, mixed backslash and forward slash traversal\",\n\t\t\twhenURL:           \"/..\\\\../private.txt\",\n\t\t\texpectCode:        http.StatusNotFound,\n\t\t\texpectContains:    \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t\texpectNotContains: `private file`,\n\t\t},\n\t\t{\n\t\t\tname:              \"nok, trailing dots (Windows edge case)\",\n\t\t\twhenURL:           \"/../private.txt...\",\n\t\t\texpectCode:        http.StatusNotFound,\n\t\t\texpectContains:    \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t\texpectNotContains: `private file`,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := echo.New()\n\n\t\t\tconfig := StaticConfig{Root: \"testdata/dist/public\"}\n\t\t\tif tc.givenConfig != nil {\n\t\t\t\tconfig = *tc.givenConfig\n\t\t\t}\n\t\t\tmiddlewareFunc := StaticWithConfig(config)\n\t\t\tif tc.givenAttachedToGroup != \"\" {\n\t\t\t\t// middleware is attached to group\n\t\t\t\tsubGroup := e.Group(tc.givenAttachedToGroup, middlewareFunc)\n\t\t\t\t// group without http handlers (routes) does not do anything.\n\t\t\t\t// Request is matched against http handlers (routes) that have group middleware attached to them\n\t\t\t\tsubGroup.GET(\"\", func(c *echo.Context) error { return echo.ErrNotFound })\n\t\t\t\tsubGroup.GET(\"/*\", func(c *echo.Context) error { return echo.ErrNotFound })\n\t\t\t} else {\n\t\t\t\t// middleware is on root level\n\t\t\t\te.Use(middlewareFunc)\n\t\t\t\te.GET(\"/regular-handler\", func(c *echo.Context) error {\n\t\t\t\t\treturn c.String(http.StatusOK, \"ok\")\n\t\t\t\t})\n\t\t\t\te.GET(\"/walle.png\", func(c *echo.Context) error {\n\t\t\t\t\treturn c.String(http.StatusTeapot, \"walle\")\n\t\t\t\t})\n\t\t\t}\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)\n\t\t\trec := httptest.NewRecorder()\n\n\t\t\te.ServeHTTP(rec, req)\n\n\t\t\tassert.Equal(t, tc.expectCode, rec.Code)\n\t\t\tresponseBody := rec.Body.String()\n\t\t\tif tc.expectContains != \"\" {\n\t\t\t\tassert.Contains(t, responseBody, tc.expectContains)\n\t\t\t}\n\t\t\tif tc.expectNotContains != \"\" {\n\t\t\t\tassert.NotContains(t, responseBody, tc.expectNotContains)\n\t\t\t}\n\t\t\tif tc.expectLength != \"\" {\n\t\t\t\tassert.Equal(t, tc.expectLength, rec.Header().Get(echo.HeaderContentLength))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMustStaticWithConfig_panicsInvalidDirListTemplate(t *testing.T) {\n\tassert.Panics(t, func() {\n\t\tStaticWithConfig(StaticConfig{DirectoryListTemplate: `{{}`})\n\t})\n}\n\nfunc TestFormat(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname   string\n\t\twhen   int64\n\t\texpect string\n\t}{\n\t\t{\n\t\t\tname:   \"byte\",\n\t\t\twhen:   0,\n\t\t\texpect: \"0\",\n\t\t},\n\t\t{\n\t\t\tname:   \"bytes\",\n\t\t\twhen:   515,\n\t\t\texpect: \"515B\",\n\t\t},\n\t\t{\n\t\t\tname:   \"KB\",\n\t\t\twhen:   31323,\n\t\t\texpect: \"30.59KB\",\n\t\t},\n\t\t{\n\t\t\tname:   \"MB\",\n\t\t\twhen:   13231323,\n\t\t\texpect: \"12.62MB\",\n\t\t},\n\t\t{\n\t\t\tname:   \"GB\",\n\t\t\twhen:   7323232398,\n\t\t\texpect: \"6.82GB\",\n\t\t},\n\t\t{\n\t\t\tname:   \"TB\",\n\t\t\twhen:   1_099_511_627_776,\n\t\t\texpect: \"1.00TB\",\n\t\t},\n\t\t{\n\t\t\tname:   \"PB\",\n\t\t\twhen:   9923232398434432,\n\t\t\texpect: \"8.81PB\",\n\t\t},\n\t\t{\n\t\t\t// test with 7EB because of https://github.com/labstack/gommon/pull/38 and https://github.com/labstack/gommon/pull/43\n\t\t\t//\n\t\t\t// 8 exbi equals 2^64, therefore it cannot be stored in int64. The tests use\n\t\t\t// the fact that on x86_64 the following expressions holds true:\n\t\t\t// int64(0) - 1 == math.MaxInt64.\n\t\t\t//\n\t\t\t// However, this is not true for other platforms, specifically aarch64, s390x\n\t\t\t// and ppc64le.\n\t\t\tname:   \"EB\",\n\t\t\twhen:   8070450532247929000,\n\t\t\texpect: \"7.00EB\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := format(tc.when)\n\t\t\tassert.Equal(t, tc.expect, result)\n\t\t})\n\t}\n}\n\nfunc TestStatic_CustomFS(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname           string\n\t\tfilesystem     fs.FS\n\t\troot           string\n\t\twhenURL        string\n\t\texpectContains string\n\t\texpectCode     int\n\t}{\n\t\t{\n\t\t\tname:           \"ok, serve index with Echo message\",\n\t\t\twhenURL:        \"/\",\n\t\t\tfilesystem:     os.DirFS(\"../_fixture\"),\n\t\t\texpectCode:     http.StatusOK,\n\t\t\texpectContains: \"<title>Echo</title>\",\n\t\t},\n\n\t\t{\n\t\t\tname:           \"ok, serve index with Echo message\",\n\t\t\twhenURL:        \"/_fixture/\",\n\t\t\tfilesystem:     os.DirFS(\"..\"),\n\t\t\texpectCode:     http.StatusOK,\n\t\t\texpectContains: \"<title>Echo</title>\",\n\t\t},\n\t\t{\n\t\t\tname:    \"ok, serve file from map fs\",\n\t\t\twhenURL: \"/file.txt\",\n\t\t\tfilesystem: fstest.MapFS{\n\t\t\t\t\"file.txt\": &fstest.MapFile{Data: []byte(\"file.txt is ok\")},\n\t\t\t},\n\t\t\texpectCode:     http.StatusOK,\n\t\t\texpectContains: \"file.txt is ok\",\n\t\t},\n\t\t{\n\t\t\tname:       \"nok, missing file in map fs\",\n\t\t\twhenURL:    \"/file.txt\",\n\t\t\texpectCode: http.StatusNotFound,\n\t\t\tfilesystem: fstest.MapFS{\n\t\t\t\t\"file2.txt\": &fstest.MapFile{Data: []byte(\"file2.txt is ok\")},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"nok, file is not a subpath of root\",\n\t\t\twhenURL: `/../../secret.txt`,\n\t\t\troot:    \"/nested/folder\",\n\t\t\tfilesystem: fstest.MapFS{\n\t\t\t\t\"secret.txt\": &fstest.MapFile{Data: []byte(\"this is a secret\")},\n\t\t\t},\n\t\t\texpectCode: http.StatusNotFound,\n\t\t},\n\t\t{\n\t\t\tname:       \"nok, backslash is forbidden\",\n\t\t\twhenURL:    `/..\\..\\secret.txt`,\n\t\t\texpectCode: http.StatusNotFound,\n\t\t\troot:       \"/nested/folder\",\n\t\t\tfilesystem: fstest.MapFS{\n\t\t\t\t\"secret.txt\": &fstest.MapFile{Data: []byte(\"this is a secret\")},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := echo.New()\n\n\t\t\tconfig := StaticConfig{\n\t\t\t\tRoot:       \".\",\n\t\t\t\tFilesystem: tc.filesystem,\n\t\t\t}\n\n\t\t\tif tc.root != \"\" {\n\t\t\t\tconfig.Root = tc.root\n\t\t\t}\n\n\t\t\tmiddlewareFunc, err := config.ToMiddleware()\n\t\t\tassert.NoError(t, err)\n\n\t\t\te.Use(middlewareFunc)\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)\n\t\t\trec := httptest.NewRecorder()\n\n\t\t\te.ServeHTTP(rec, req)\n\n\t\t\tassert.Equal(t, tc.expectCode, rec.Code)\n\t\t\tif tc.expectContains != \"\" {\n\t\t\t\tresponseBody := rec.Body.String()\n\t\t\t\tassert.Contains(t, responseBody, tc.expectContains)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestStatic_DirectoryBrowsing(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname              string\n\t\tgivenConfig       StaticConfig\n\t\twhenURL           string\n\t\texpectContains    string\n\t\texpectNotContains []string\n\t\texpectCode        int\n\t}{\n\t\t{\n\t\t\tname: \"ok, should return index.html contents from Root=public folder\",\n\t\t\tgivenConfig: StaticConfig{\n\t\t\t\tRoot:       \"public\",\n\t\t\t\tFilesystem: os.DirFS(\"../_fixture/dist\"),\n\t\t\t\tBrowse:     true,\n\t\t\t},\n\t\t\twhenURL:        \"/\",\n\t\t\texpectCode:     http.StatusOK,\n\t\t\texpectContains: `<h1>Hello from index</h1>`,\n\t\t},\n\t\t{\n\t\t\tname: \"ok, should return only subfolder folder listing from Root=public/assets\",\n\t\t\tgivenConfig: StaticConfig{\n\t\t\t\tRoot:       \"public\",\n\t\t\t\tFilesystem: os.DirFS(\"../_fixture/dist\"),\n\t\t\t\tBrowse:     true,\n\t\t\t},\n\t\t\twhenURL:        \"/assets\",\n\t\t\texpectCode:     http.StatusOK,\n\t\t\texpectContains: `<a class=\"file\" href=\"readme.md\">readme.md</a>`,\n\t\t\texpectNotContains: []string{\n\t\t\t\t`<h1>Hello from index</h1>`, // should see the listing, not index.html contents\n\t\t\t\t`private.txt`,               // file from the parent folder\n\t\t\t\t`subfolder.md`,              // file from subfolder\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := echo.New()\n\n\t\t\tmiddlewareFunc, err := tc.givenConfig.ToMiddleware()\n\t\t\tassert.NoError(t, err)\n\n\t\t\te.Use(middlewareFunc)\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)\n\t\t\trec := httptest.NewRecorder()\n\n\t\t\te.ServeHTTP(rec, req)\n\n\t\t\tassert.Equal(t, tc.expectCode, rec.Code)\n\n\t\t\tresponseBody := rec.Body.String()\n\t\t\tif tc.expectContains != \"\" {\n\t\t\t\tassert.Contains(t, responseBody, tc.expectContains, \"body should contain: \"+tc.expectContains)\n\t\t\t}\n\t\t\tfor _, notContains := range tc.expectNotContains {\n\t\t\t\tassert.NotContains(t, responseBody, notContains, \"body should NOT contain: \"+notContains)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "middleware/testdata/dist/private.txt",
    "content": "private file\n"
  },
  {
    "path": "middleware/testdata/dist/public/assets/readme.md",
    "content": "This directory is used for the static middleware test\n"
  },
  {
    "path": "middleware/testdata/dist/public/assets/subfolder/subfolder.md",
    "content": "file inside subfolder\n"
  },
  {
    "path": "middleware/testdata/dist/public/index.html",
    "content": "<h1>Hello from index</h1>\n"
  },
  {
    "path": "middleware/testdata/dist/public/test.txt",
    "content": "test.txt contents\n"
  },
  {
    "path": "middleware/testdata/private.txt",
    "content": "private file\n"
  },
  {
    "path": "middleware/util.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"bufio\"\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n)\n\nconst (\n\t_ = int64(1 << (10 * iota)) // ignore first value by assigning to blank identifier\n\t// KB is 1 KiloByte = 1024 bytes\n\tKB\n\t// MB is 1 Megabyte = 1_048_576 bytes\n\tMB\n\t// GB is 1 Gigabyte = 1_073_741_824 bytes\n\tGB\n\t// TB is 1 Terabyte = 1_099_511_627_776 bytes\n\tTB\n\t// PB is 1 Petabyte = 1_125_899_906_842_624 bytes\n\tPB\n\t// EB is 1 Exabyte = 1_152_921_504_606_847_000 bytes\n\tEB\n)\n\nfunc matchScheme(domain, pattern string) bool {\n\tdidx := strings.Index(domain, \":\")\n\tpidx := strings.Index(pattern, \":\")\n\treturn didx != -1 && pidx != -1 && domain[:didx] == pattern[:pidx]\n}\n\nfunc createRandomStringGenerator(length uint8) func() string {\n\treturn func() string {\n\t\treturn randomString(length)\n\t}\n}\n\n// https://tip.golang.org/doc/go1.19#:~:text=Read%20no%20longer%20buffers%20random%20data%20obtained%20from%20the%20operating%20system%20between%20calls\nvar randomReaderPool = sync.Pool{New: func() any {\n\treturn bufio.NewReader(rand.Reader)\n}}\n\nconst randomStringCharset = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\"\nconst randomStringCharsetLen = 52 // len(randomStringCharset)\nconst randomStringMaxByte = 255 - (256 % randomStringCharsetLen)\n\nfunc randomString(length uint8) string {\n\treader := randomReaderPool.Get().(*bufio.Reader)\n\tdefer randomReaderPool.Put(reader)\n\n\tb := make([]byte, length)\n\tr := make([]byte, length+(length/4)) // perf: avoid read from rand.Reader many times\n\tvar i uint8 = 0\n\n\t// security note:\n\t// we can't just simply do b[i]=randomStringCharset[rb%len(randomStringCharset)],\n\t// len(len(randomStringCharset)) is 52, and rb is [0, 255], 256 = 52 * 4 + 48.\n\t// make the first 48 characters more possibly to be generated then others.\n\t// So we have to skip bytes when rb > randomStringMaxByt\n\n\tfor {\n\t\t_, err := io.ReadFull(reader, r)\n\t\tif err != nil {\n\t\t\tpanic(\"unexpected error happened when reading from bufio.NewReader(crypto/rand.Reader)\")\n\t\t}\n\t\tfor _, rb := range r {\n\t\t\tif rb > randomStringMaxByte {\n\t\t\t\t// Skip this number to avoid bias.\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tb[i] = randomStringCharset[rb%randomStringCharsetLen]\n\t\t\ti++\n\t\t\tif i == length {\n\t\t\t\treturn string(b)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc validateOrigins(origins []string, what string) error {\n\tfor _, o := range origins {\n\t\tif err := validateOrigin(o, what); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc validateOrigin(origin string, what string) error {\n\tu, err := url.Parse(origin)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"can not parse %s: %w\", what, err)\n\t}\n\tif u.Scheme == \"\" || u.Host == \"\" {\n\t\treturn fmt.Errorf(\"%s is missing scheme or host: %s\", what, origin)\n\t}\n\tif u.Path != \"\" || u.RawQuery != \"\" || u.Fragment != \"\" {\n\t\treturn fmt.Errorf(\"%s can not have path, query, and fragments: %s\", what, origin)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "middleware/util_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage middleware\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype discardHandler struct {\n\tslog.JSONHandler\n}\n\nfunc (d *discardHandler) Enabled(context.Context, slog.Level) bool { return false }\n\nfunc Test_matchScheme(t *testing.T) {\n\ttests := []struct {\n\t\tdomain, pattern string\n\t\texpected        bool\n\t}{\n\t\t{\n\t\t\tdomain:   \"http://example.com\",\n\t\t\tpattern:  \"http://example.com\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tdomain:   \"https://example.com\",\n\t\t\tpattern:  \"https://example.com\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tdomain:   \"http://example.com\",\n\t\t\tpattern:  \"https://example.com\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tdomain:   \"https://example.com\",\n\t\t\tpattern:  \"http://example.com\",\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, v := range tests {\n\t\tassert.Equal(t, v.expected, matchScheme(v.domain, v.pattern))\n\t}\n}\n\nfunc TestRandomString(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname       string\n\t\twhenLength uint8\n\t\texpect     string\n\t}{\n\t\t{\n\t\t\tname:       \"ok, 16\",\n\t\t\twhenLength: 16,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok, 32\",\n\t\t\twhenLength: 32,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tuid := randomString(tc.whenLength)\n\t\t\tassert.Len(t, uid, int(tc.whenLength))\n\t\t})\n\t}\n}\n\nfunc TestRandomStringBias(t *testing.T) {\n\tt.Parallel()\n\tconst slen = 33\n\tconst loop = 100000\n\n\tcounts := make(map[rune]int)\n\tvar count int64\n\n\tfor i := 0; i < loop; i++ {\n\t\ts := randomString(slen)\n\t\trequire.Equal(t, slen, len(s))\n\t\tfor _, b := range s {\n\t\t\tcounts[b]++\n\t\t\tcount++\n\t\t}\n\t}\n\n\trequire.Equal(t, randomStringCharsetLen, len(counts))\n\n\tavg := float64(count) / float64(len(counts))\n\tfor k, n := range counts {\n\t\tdiff := float64(n) / avg\n\t\tif diff < 0.95 || diff > 1.05 {\n\t\t\tt.Errorf(\"Bias on '%c': expected average %f, got %d\", k, avg, n)\n\t\t}\n\t}\n}\n\nfunc TestValidateOrigins(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname         string\n\t\tgivenOrigins []string\n\t\tgivenWhat    string\n\t\texpectErr    string\n\t}{\n\t\t// Valid cases\n\t\t{\n\t\t\tname:         \"ok, empty origins\",\n\t\t\tgivenOrigins: []string{},\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, basic http\",\n\t\t\tgivenOrigins: []string{\"http://example.com\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, basic https\",\n\t\t\tgivenOrigins: []string{\"https://example.com\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, with port\",\n\t\t\tgivenOrigins: []string{\"http://localhost:8080\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, with subdomain\",\n\t\t\tgivenOrigins: []string{\"https://api.example.com\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, subdomain with port\",\n\t\t\tgivenOrigins: []string{\"https://api.example.com:8080\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, localhost\",\n\t\t\tgivenOrigins: []string{\"http://localhost\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, IPv4 address\",\n\t\t\tgivenOrigins: []string{\"http://192.168.1.1\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, IPv4 with port\",\n\t\t\tgivenOrigins: []string{\"http://192.168.1.1:8080\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, IPv6 loopback\",\n\t\t\tgivenOrigins: []string{\"http://[::1]\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, IPv6 with port\",\n\t\t\tgivenOrigins: []string{\"http://[::1]:8080\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, IPv6 full address\",\n\t\t\tgivenOrigins: []string{\"http://[2001:db8::1]\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, multiple valid origins\",\n\t\t\tgivenOrigins: []string{\"http://example.com\", \"https://api.example.com:8080\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, different schemes\",\n\t\t\tgivenOrigins: []string{\"http://example.com\", \"https://example.com\", \"ws://example.com\"},\n\t\t},\n\t\t// Invalid - missing scheme\n\t\t{\n\t\t\tname:         \"nok, plain domain\",\n\t\t\tgivenOrigins: []string{\"example.com\"},\n\t\t\texpectErr:    \"trusted origin is missing scheme or host: example.com\",\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, with slashes but no scheme\",\n\t\t\tgivenOrigins: []string{\"//example.com\"},\n\t\t\texpectErr:    \"trusted origin is missing scheme or host: //example.com\",\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, www without scheme\",\n\t\t\tgivenOrigins: []string{\"www.example.com\"},\n\t\t\texpectErr:    \"trusted origin is missing scheme or host: www.example.com\",\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, localhost without scheme\",\n\t\t\tgivenOrigins: []string{\"localhost:8080\"},\n\t\t\texpectErr:    \"trusted origin is missing scheme or host: localhost:8080\",\n\t\t},\n\t\t// Invalid - missing host\n\t\t{\n\t\t\tname:         \"nok, scheme only http\",\n\t\t\tgivenOrigins: []string{\"http://\"},\n\t\t\texpectErr:    \"trusted origin is missing scheme or host: http://\",\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, scheme only https\",\n\t\t\tgivenOrigins: []string{\"https://\"},\n\t\t\texpectErr:    \"trusted origin is missing scheme or host: https://\",\n\t\t},\n\t\t// Invalid - has path\n\t\t{\n\t\t\tname:         \"nok, has simple path\",\n\t\t\tgivenOrigins: []string{\"http://example.com/path\"},\n\t\t\texpectErr:    \"trusted origin can not have path, query, and fragments: http://example.com/path\",\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, has nested path\",\n\t\t\tgivenOrigins: []string{\"https://example.com/api/v1\"},\n\t\t\texpectErr:    \"trusted origin can not have path, query, and fragments: https://example.com/api/v1\",\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, has root path\",\n\t\t\tgivenOrigins: []string{\"http://example.com/\"},\n\t\t\texpectErr:    \"trusted origin can not have path, query, and fragments: http://example.com/\",\n\t\t},\n\t\t// Invalid - has query\n\t\t{\n\t\t\tname:         \"nok, has single query param\",\n\t\t\tgivenOrigins: []string{\"http://example.com?foo=bar\"},\n\t\t\texpectErr:    \"trusted origin can not have path, query, and fragments: http://example.com?foo=bar\",\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, has multiple query params\",\n\t\t\tgivenOrigins: []string{\"https://example.com?foo=bar&baz=qux\"},\n\t\t\texpectErr:    \"trusted origin can not have path, query, and fragments: https://example.com?foo=bar&baz=qux\",\n\t\t},\n\t\t// Invalid - has fragment\n\t\t{\n\t\t\tname:         \"nok, has simple fragment\",\n\t\t\tgivenOrigins: []string{\"http://example.com#section\"},\n\t\t\texpectErr:    \"trusted origin can not have path, query, and fragments: http://example.com#section\",\n\t\t},\n\t\t// Invalid - combinations\n\t\t{\n\t\t\tname:         \"nok, has path and query\",\n\t\t\tgivenOrigins: []string{\"http://example.com/path?foo=bar\"},\n\t\t\texpectErr:    \"trusted origin can not have path, query, and fragments: http://example.com/path?foo=bar\",\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, has path and fragment\",\n\t\t\tgivenOrigins: []string{\"http://example.com/path#section\"},\n\t\t\texpectErr:    \"trusted origin can not have path, query, and fragments: http://example.com/path#section\",\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, has query and fragment\",\n\t\t\tgivenOrigins: []string{\"http://example.com?foo=bar#section\"},\n\t\t\texpectErr:    \"trusted origin can not have path, query, and fragments: http://example.com?foo=bar#section\",\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, has path, query, and fragment\",\n\t\t\tgivenOrigins: []string{\"http://example.com/path?foo=bar#section\"},\n\t\t\texpectErr:    \"trusted origin can not have path, query, and fragments: http://example.com/path?foo=bar#section\",\n\t\t},\n\t\t// Edge cases\n\t\t{\n\t\t\tname:         \"nok, empty string\",\n\t\t\tgivenOrigins: []string{\"\"},\n\t\t\texpectErr:    \"trusted origin is missing scheme or host: \",\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, whitespace only\",\n\t\t\tgivenOrigins: []string{\" \"},\n\t\t\texpectErr:    \"trusted origin is missing scheme or host:  \",\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, multiple origins - first invalid\",\n\t\t\tgivenOrigins: []string{\"example.com\", \"http://valid.com\"},\n\t\t\texpectErr:    \"trusted origin is missing scheme or host: example.com\",\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, multiple origins - middle invalid\",\n\t\t\tgivenOrigins: []string{\"http://valid1.com\", \"invalid.com\", \"http://valid2.com\"},\n\t\t\texpectErr:    \"trusted origin is missing scheme or host: invalid.com\",\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, multiple origins - last invalid\",\n\t\t\tgivenOrigins: []string{\"http://valid.com\", \"invalid.com\"},\n\t\t\texpectErr:    \"trusted origin is missing scheme or host: invalid.com\",\n\t\t},\n\t\t// Different \"what\" parameter\n\t\t{\n\t\t\tname:         \"nok, custom what parameter - missing scheme\",\n\t\t\tgivenOrigins: []string{\"example.com\"},\n\t\t\tgivenWhat:    \"allowed origin\",\n\t\t\texpectErr:    \"allowed origin is missing scheme or host: example.com\",\n\t\t},\n\t\t{\n\t\t\tname:         \"nok, custom what parameter - has path\",\n\t\t\tgivenOrigins: []string{\"http://example.com/path\"},\n\t\t\tgivenWhat:    \"cors origin\",\n\t\t\texpectErr:    \"cors origin can not have path, query, and fragments: http://example.com/path\",\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\twhat := tc.givenWhat\n\t\t\tif what == \"\" {\n\t\t\t\twhat = \"trusted origin\"\n\t\t\t}\n\t\t\terr := validateOrigins(tc.givenOrigins, what)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "renderer.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport \"io\"\n\n// Renderer is the interface that wraps the Render function.\ntype Renderer interface {\n\tRender(c *Context, w io.Writer, templateName string, data any) error\n}\n\n// TemplateRenderer is helper to ease creating renderers for `html/template` and `text/template` packages.\n// Example usage:\n//\n//\t\te.Renderer = &echo.TemplateRenderer{\n//\t\t\tTemplate: template.Must(template.ParseGlob(\"templates/*.html\")),\n//\t\t}\n//\n//\t  e.Renderer = &echo.TemplateRenderer{\n//\t\t\tTemplate: template.Must(template.New(\"hello\").Parse(\"Hello, {{.}}!\")),\n//\t\t}\ntype TemplateRenderer struct {\n\tTemplate interface {\n\t\tExecuteTemplate(wr io.Writer, name string, data any) error\n\t}\n}\n\n// Render renders the template with given data.\nfunc (t *TemplateRenderer) Render(c *Context, w io.Writer, name string, data any) error {\n\treturn t.Template.ExecuteTemplate(w, name, data)\n}\n"
  },
  {
    "path": "renderer_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"github.com/stretchr/testify/assert\"\n\t\"html/template\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestRenderWithTemplateRenderer(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(userJSON))\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\te.Renderer = &TemplateRenderer{\n\t\tTemplate: template.Must(template.New(\"hello\").Parse(\"Hello, {{.}}!\")),\n\t}\n\terr := c.Render(http.StatusOK, \"hello\", \"Jon Snow\")\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, http.StatusOK, rec.Code)\n\t\tassert.Equal(t, \"Hello, Jon Snow!\", rec.Body.String())\n\t}\n}\n"
  },
  {
    "path": "response.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/http\"\n)\n\n// Response wraps an http.ResponseWriter and implements its interface to be used\n// by an HTTP handler to construct an HTTP response.\n// See: https://golang.org/pkg/net/http/#ResponseWriter\ntype Response struct {\n\thttp.ResponseWriter\n\tlogger *slog.Logger\n\t// beforeFuncs are functions that are called just before the response (status) is written. Happens only once, during WriteHeader call.\n\tbeforeFuncs []func()\n\t// afterFuncs are functions that are called just after the response is written. During every `Write` method call.\n\tafterFuncs []func()\n\tStatus     int\n\tSize       int64\n\tCommitted  bool\n}\n\n// NewResponse creates a new instance of Response.\nfunc NewResponse(w http.ResponseWriter, logger *slog.Logger) (r *Response) {\n\treturn &Response{ResponseWriter: w, logger: logger}\n}\n\n// Before registers a function which is called just before the response (status) is written.\nfunc (r *Response) Before(fn func()) {\n\tr.beforeFuncs = append(r.beforeFuncs, fn)\n}\n\n// After registers a function which is called just after the response is written.\nfunc (r *Response) After(fn func()) {\n\tr.afterFuncs = append(r.afterFuncs, fn)\n}\n\n// WriteHeader sends an HTTP response header with status code. If WriteHeader is\n// not called explicitly, the first call to Write will trigger an implicit\n// WriteHeader(http.StatusOK). Thus explicit calls to WriteHeader are mainly\n// used to send error codes.\nfunc (r *Response) WriteHeader(code int) {\n\tif r.Committed {\n\t\tr.logger.Error(\"echo: response already written to client\")\n\t\treturn\n\t}\n\tr.Status = code\n\tfor _, fn := range r.beforeFuncs {\n\t\tfn()\n\t}\n\tr.ResponseWriter.WriteHeader(r.Status)\n\tr.Committed = true\n}\n\n// Write writes the data to the connection as part of an HTTP reply.\nfunc (r *Response) Write(b []byte) (n int, err error) {\n\tif !r.Committed {\n\t\tif r.Status == 0 {\n\t\t\tr.Status = http.StatusOK\n\t\t}\n\t\tr.WriteHeader(r.Status)\n\t}\n\tn, err = r.ResponseWriter.Write(b)\n\tr.Size += int64(n)\n\tfor _, fn := range r.afterFuncs {\n\t\tfn()\n\t}\n\treturn\n}\n\n// Flush implements the http.Flusher interface to allow an HTTP handler to flush\n// buffered data to the client.\n// See [http.Flusher](https://golang.org/pkg/net/http/#Flusher)\nfunc (r *Response) Flush() {\n\terr := http.NewResponseController(r.ResponseWriter).Flush()\n\tif err != nil && errors.Is(err, http.ErrNotSupported) {\n\t\tpanic(fmt.Errorf(\"echo: response writer %T does not support flushing (http.Flusher interface)\", r.ResponseWriter))\n\t}\n}\n\n// Hijack implements the http.Hijacker interface to allow an HTTP handler to\n// take over the connection.\n// This method is relevant to Websocket connection upgrades, proxis, and other advanced use cases.\n// See [http.Hijacker](https://golang.org/pkg/net/http/#Hijacker)\nfunc (r *Response) Hijack() (net.Conn, *bufio.ReadWriter, error) {\n\t// newer code should do response hijacking like that\n\t// http.NewResponseController(responseWriter).Hijack()\n\t//\n\t// but there are older libraries that are not aware of `http.NewResponseController` and try to hijack directly\n\t// `hj, ok := resp.(http.Hijacker)` <-- which would fail without Response directly implementing Hijack method\n\t// so for that purpose we need to implement http.Hijacker interface\n\treturn http.NewResponseController(r.ResponseWriter).Hijack()\n}\n\n// Unwrap returns the original http.ResponseWriter.\n// ResponseController can be used to access the original http.ResponseWriter.\n// See [https://go.dev/blog/go1.20]\nfunc (r *Response) Unwrap() http.ResponseWriter {\n\treturn r.ResponseWriter\n}\n\nfunc (r *Response) reset(w http.ResponseWriter) {\n\tr.beforeFuncs = nil\n\tr.afterFuncs = nil\n\tr.ResponseWriter = w\n\tr.Size = 0\n\tr.Status = http.StatusOK\n\tr.Committed = false\n}\n\n// UnwrapResponse unwraps given ResponseWriter to return contexts original Echo Response. rw has to implement\n// following method `Unwrap() http.ResponseWriter`\nfunc UnwrapResponse(rw http.ResponseWriter) (*Response, error) {\n\tfor {\n\t\tswitch t := rw.(type) {\n\t\tcase *Response:\n\t\t\treturn t, nil\n\t\tcase interface{ Unwrap() http.ResponseWriter }:\n\t\t\trw = t.Unwrap()\n\t\t\tcontinue\n\t\tdefault:\n\t\t\treturn nil, errors.New(\"ResponseWriter does not implement 'Unwrap() http.ResponseWriter' interface or unwrap to *echo.Response\")\n\t\t}\n\t}\n}\n\n// delayedStatusWriter is a wrapper around http.ResponseWriter that delays writing the status code until first Write is called.\n// This allows (global) error handler to decide correct status code to be sent to the client.\ntype delayedStatusWriter struct {\n\thttp.ResponseWriter\n\tcommitted bool\n\tstatus   int\n}\n\nfunc (w *delayedStatusWriter) WriteHeader(statusCode int) {\n\t// in case something else writes status code explicitly before us we need mark response committed\n\tw.committed = true\n\tw.ResponseWriter.WriteHeader(statusCode)\n}\n\nfunc (w *delayedStatusWriter) Write(data []byte) (int, error) {\n\tif !w.committed {\n\t\tw.committed = true\n\t\tif w.status == 0 {\n\t\t\tw.status = http.StatusOK\n\t\t}\n\t\tw.ResponseWriter.WriteHeader(w.status)\n\t}\n\treturn w.ResponseWriter.Write(data)\n}\n\nfunc (w *delayedStatusWriter) Flush() {\n\terr := http.NewResponseController(w.ResponseWriter).Flush()\n\tif err != nil && errors.Is(err, http.ErrNotSupported) {\n\t\tpanic(errors.New(\"response writer flushing is not supported\"))\n\t}\n}\n\nfunc (w *delayedStatusWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {\n\treturn http.NewResponseController(w.ResponseWriter).Hijack()\n}\n\nfunc (w *delayedStatusWriter) Unwrap() http.ResponseWriter {\n\treturn w.ResponseWriter\n}\n"
  },
  {
    "path": "response_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestResponse(t *testing.T) {\n\te := New()\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tres := NewResponse(rec, e.Logger)\n\n\t// Before\n\tres.Before(func() {\n\t\tc.Response().Header().Set(HeaderServer, \"echo\")\n\t})\n\t// After\n\tres.After(func() {\n\t\tc.Response().Header().Set(HeaderXFrameOptions, \"DENY\")\n\t})\n\tres.Write([]byte(\"test\"))\n\tassert.Equal(t, \"echo\", rec.Header().Get(HeaderServer))\n\tassert.Equal(t, \"DENY\", rec.Header().Get(HeaderXFrameOptions))\n}\n\nfunc TestResponse_Write_FallsBackToDefaultStatus(t *testing.T) {\n\te := New()\n\trec := httptest.NewRecorder()\n\tres := NewResponse(rec, e.Logger)\n\n\tres.Write([]byte(\"test\"))\n\tassert.Equal(t, http.StatusOK, rec.Code)\n}\n\nfunc TestResponse_Write_UsesSetResponseCode(t *testing.T) {\n\te := New()\n\trec := httptest.NewRecorder()\n\tres := NewResponse(rec, e.Logger)\n\n\tres.Status = http.StatusBadRequest\n\tres.Write([]byte(\"test\"))\n\tassert.Equal(t, http.StatusBadRequest, rec.Code)\n}\n\nfunc TestResponse_ChangeStatusCodeBeforeWrite(t *testing.T) {\n\te := New()\n\trec := httptest.NewRecorder()\n\tres := NewResponse(rec, e.Logger)\n\n\tres.Before(func() {\n\t\tif 200 < res.Status && res.Status < 300 {\n\t\t\tres.Status = 200\n\t\t}\n\t})\n\n\tres.WriteHeader(209)\n\n\tassert.Equal(t, http.StatusOK, rec.Code)\n}\n\nfunc TestResponse_Unwrap(t *testing.T) {\n\te := New()\n\trec := httptest.NewRecorder()\n\tres := NewResponse(rec, e.Logger)\n\n\tassert.Equal(t, rec, res.Unwrap())\n}\n\nfunc TestResponse_isHijacker(t *testing.T) {\n\tvar resp http.ResponseWriter = &Response{}\n\n\t_, ok := resp.(http.Hijacker)\n\tassert.True(t, ok)\n}\n\nfunc TestResponse_Flush(t *testing.T) {\n\te := New()\n\trec := httptest.NewRecorder()\n\tres := NewResponse(rec, e.Logger)\n\n\tres.Write([]byte(\"test\"))\n\tres.Flush()\n\tassert.True(t, rec.Flushed)\n}\n\ntype testResponseWriter struct {\n}\n\nfunc (w *testResponseWriter) WriteHeader(statusCode int) {\n}\n\nfunc (w *testResponseWriter) Write([]byte) (int, error) {\n\treturn 0, nil\n}\n\nfunc (w *testResponseWriter) Header() http.Header {\n\treturn nil\n}\n\nfunc TestResponse_FlushPanics(t *testing.T) {\n\te := New()\n\trw := new(testResponseWriter)\n\tres := NewResponse(rw, e.Logger)\n\n\t// we test that we behave as before unwrapping flushers - flushing writer that does not support it causes panic\n\tassert.PanicsWithError(t, \"echo: response writer *echo.testResponseWriter does not support flushing (http.Flusher interface)\", func() {\n\t\tres.Flush()\n\t})\n}\n\nfunc TestResponse_UnwrapResponse(t *testing.T) {\n\torgRes := NewResponse(httptest.NewRecorder(), nil)\n\tres, err := UnwrapResponse(orgRes)\n\n\tassert.NotNil(t, res)\n\tassert.NoError(t, err)\n}\n\nfunc TestResponse_UnwrapResponse_error(t *testing.T) {\n\trw := new(testResponseWriter)\n\tres, err := UnwrapResponse(rw)\n\n\tassert.Nil(t, res)\n\tassert.EqualError(t, err, \"ResponseWriter does not implement 'Unwrap() http.ResponseWriter' interface or unwrap to *echo.Response\")\n}\n"
  },
  {
    "path": "route.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"runtime\"\n)\n\n// Route contains information to adding/registering new route with the router.\n// Method+Path pair uniquely identifies the Route. It is mandatory to provide Method+Path+Handler fields.\ntype Route struct {\n\tMethod      string\n\tPath        string\n\tName        string\n\tHandler     HandlerFunc\n\tMiddlewares []MiddlewareFunc\n}\n\n// ToRouteInfo converts Route to RouteInfo\nfunc (r Route) ToRouteInfo(params []string) RouteInfo {\n\tname := r.Name\n\tif name == \"\" {\n\t\tname = r.Method + \":\" + r.Path\n\t}\n\n\treturn RouteInfo{\n\t\tMethod:     r.Method,\n\t\tPath:       r.Path,\n\t\tParameters: append([]string(nil), params...),\n\t\tName:       name,\n\t}\n}\n\n// WithPrefix recreates Route with added group prefix and group middlewares it is grouped to.\nfunc (r Route) WithPrefix(pathPrefix string, middlewares []MiddlewareFunc) Route {\n\tr.Path = pathPrefix + r.Path\n\n\tif len(middlewares) > 0 {\n\t\tm := make([]MiddlewareFunc, 0, len(middlewares)+len(r.Middlewares))\n\t\tm = append(m, middlewares...)\n\t\tm = append(m, r.Middlewares...)\n\t\tr.Middlewares = m\n\t}\n\treturn r\n}\n\n// RouteInfo contains information about registered Route.\ntype RouteInfo struct {\n\tName       string\n\tMethod     string\n\tPath       string\n\tParameters []string\n\n\t// NOTE: handler and middlewares are not exposed because handler could be already wrapping middlewares. Therefore,\n\t// it is not always 100% known if handler function already wraps middlewares or not. In Echo handler could be one\n\t// function or several functions wrapping each other.\n}\n\n// Clone creates copy of RouteInfo\nfunc (r RouteInfo) Clone() RouteInfo {\n\treturn RouteInfo{\n\t\tName:       r.Name,\n\t\tMethod:     r.Method,\n\t\tPath:       r.Path,\n\t\tParameters: append([]string(nil), r.Parameters...),\n\t}\n}\n\n// Reverse reverses route to URL string by replacing path parameters with given params values.\nfunc (r RouteInfo) Reverse(pathValues ...any) string {\n\turi := new(bytes.Buffer)\n\tln := len(pathValues)\n\tn := 0\n\tfor i, l := 0, len(r.Path); i < l; i++ {\n\t\thasBackslash := r.Path[i] == '\\\\'\n\t\tif hasBackslash && i+1 < l && r.Path[i+1] == ':' {\n\t\t\ti++ // backslash before colon escapes that colon. in that case skip backslash\n\t\t}\n\t\tif n < ln && (r.Path[i] == anyLabel || (!hasBackslash && r.Path[i] == paramLabel)) {\n\t\t\t// in case of `*` wildcard or `:` (unescaped colon) param we replace everything till next slash or end of path\n\t\t\tfor ; i < l && r.Path[i] != '/'; i++ {\n\t\t\t}\n\t\t\turi.WriteString(fmt.Sprintf(\"%v\", pathValues[n]))\n\t\t\tn++\n\t\t}\n\t\tif i < l {\n\t\t\turi.WriteByte(r.Path[i])\n\t\t}\n\t}\n\treturn uri.String()\n}\n\n// HandlerName returns string name for given function.\nfunc HandlerName(h HandlerFunc) string {\n\tt := reflect.ValueOf(h).Type()\n\tif t.Kind() == reflect.Func {\n\t\treturn runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name()\n\t}\n\treturn t.String()\n}\n\n// Clone creates copy of Routes\nfunc (r Routes) Clone() Routes {\n\tresult := make(Routes, len(r))\n\tfor i, route := range r {\n\t\tresult[i] = route.Clone()\n\t}\n\treturn result\n}\n\n// Reverse reverses route to URL string by replacing path parameters with given params values.\nfunc (r Routes) Reverse(routeName string, pathValues ...any) (string, error) {\n\tfor _, rr := range r {\n\t\tif rr.Name == routeName {\n\t\t\treturn rr.Reverse(pathValues...), nil\n\t\t}\n\t}\n\treturn \"\", errors.New(\"route not found\")\n}\n\n// FindByMethodPath searched for matching route info by method and path\nfunc (r Routes) FindByMethodPath(method string, path string) (RouteInfo, error) {\n\tif r == nil {\n\t\treturn RouteInfo{}, errors.New(\"route not found by method and path\")\n\t}\n\n\tfor _, rr := range r {\n\t\tif rr.Method == method && rr.Path == path {\n\t\t\treturn rr, nil\n\t\t}\n\t}\n\treturn RouteInfo{}, errors.New(\"route not found by method and path\")\n}\n\n// FilterByMethod searched for matching route info by method\nfunc (r Routes) FilterByMethod(method string) (Routes, error) {\n\tif r == nil {\n\t\treturn nil, errors.New(\"route not found by method\")\n\t}\n\n\tresult := make(Routes, 0)\n\tfor _, rr := range r {\n\t\tif rr.Method == method {\n\t\t\tresult = append(result, rr)\n\t\t}\n\t}\n\tif len(result) == 0 {\n\t\treturn nil, errors.New(\"route not found by method\")\n\t}\n\treturn result, nil\n}\n\n// FilterByPath searched for matching route info by path\nfunc (r Routes) FilterByPath(path string) (Routes, error) {\n\tif r == nil {\n\t\treturn nil, errors.New(\"route not found by path\")\n\t}\n\n\tresult := make(Routes, 0)\n\tfor _, rr := range r {\n\t\tif rr.Path == path {\n\t\t\tresult = append(result, rr)\n\t\t}\n\t}\n\tif len(result) == 0 {\n\t\treturn nil, errors.New(\"route not found by path\")\n\t}\n\treturn result, nil\n}\n\n// FilterByName searched for matching route info by name\nfunc (r Routes) FilterByName(name string) (Routes, error) {\n\tif r == nil {\n\t\treturn nil, errors.New(\"route not found by name\")\n\t}\n\n\tresult := make(Routes, 0)\n\tfor _, rr := range r {\n\t\tif rr.Name == name {\n\t\t\tresult = append(result, rr)\n\t\t}\n\t}\n\tif len(result) == 0 {\n\t\treturn nil, errors.New(\"route not found by name\")\n\t}\n\treturn result, nil\n}\n"
  },
  {
    "path": "route_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar myNamedHandler = func(c *Context) error {\n\treturn nil\n}\n\ntype NameStruct struct {\n}\n\nfunc (n *NameStruct) getUsers(c *Context) error {\n\treturn nil\n}\n\nfunc TestHandlerName(t *testing.T) {\n\tmyNameFuncVar := func(c *Context) error {\n\t\treturn nil\n\t}\n\n\ttmp := NameStruct{}\n\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenHandlerFunc HandlerFunc\n\t\texpect          string\n\t}{\n\t\t{\n\t\t\tname: \"ok, func as anonymous func\",\n\t\t\twhenHandlerFunc: func(c *Context) error {\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\texpect: \"github.com/labstack/echo/v5.TestHandlerName.func2\",\n\t\t},\n\t\t{\n\t\t\tname:            \"ok, func as named package variable\",\n\t\t\twhenHandlerFunc: myNamedHandler,\n\t\t\texpect:          \"github.com/labstack/echo/v5.init.func4\",\n\t\t},\n\t\t{\n\t\t\tname:            \"ok, func as named function variable\",\n\t\t\twhenHandlerFunc: myNameFuncVar,\n\t\t\texpect:          \"github.com/labstack/echo/v5.TestHandlerName.func1\",\n\t\t},\n\t\t{\n\t\t\tname:            \"ok, func as struct method\",\n\t\t\twhenHandlerFunc: tmp.getUsers,\n\t\t\texpect:          \"github.com/labstack/echo/v5.(*NameStruct).getUsers-fm\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tname := HandlerName(tc.whenHandlerFunc)\n\t\t\tassert.Equal(t, tc.expect, name)\n\t\t})\n\t}\n}\n\nfunc TestHandlerName_differentFuncSameName(t *testing.T) {\n\thandlerCreator := func(name string) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\treturn c.String(http.StatusTeapot, name)\n\t\t}\n\t}\n\th1 := handlerCreator(\"name1\")\n\tassert.Equal(t, \"github.com/labstack/echo/v5.TestHandlerName_differentFuncSameName.TestHandlerName_differentFuncSameName.func1.func2\", HandlerName(h1))\n\n\th2 := handlerCreator(\"name2\")\n\tassert.Equal(t, \"github.com/labstack/echo/v5.TestHandlerName_differentFuncSameName.TestHandlerName_differentFuncSameName.func1.func3\", HandlerName(h2))\n}\n\nfunc TestRoute_ToRouteInfo(t *testing.T) {\n\tvar testCases = []struct {\n\t\texpect     RouteInfo\n\t\tgiven      Route\n\t\tname       string\n\t\twhenParams []string\n\t}{\n\t\t{\n\t\t\tname: \"ok, no params, with name\",\n\t\t\tgiven: Route{\n\t\t\t\tMethod: http.MethodGet,\n\t\t\t\tPath:   \"/test\",\n\t\t\t\tHandler: func(c *Context) error {\n\t\t\t\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t\t\t\t},\n\t\t\t\tMiddlewares: nil,\n\t\t\t\tName:        \"test route\",\n\t\t\t},\n\t\t\texpect: RouteInfo{\n\t\t\t\tMethod:     http.MethodGet,\n\t\t\t\tPath:       \"/test\",\n\t\t\t\tParameters: nil,\n\t\t\t\tName:       \"test route\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok, params\",\n\t\t\tgiven: Route{\n\t\t\t\tMethod: http.MethodGet,\n\t\t\t\tPath:   \"users/:id/:file\", // no slash prefix\n\t\t\t\tHandler: func(c *Context) error {\n\t\t\t\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t\t\t\t},\n\t\t\t\tMiddlewares: nil,\n\t\t\t\tName:        \"\",\n\t\t\t},\n\t\t\twhenParams: []string{\"id\", \"file\"},\n\t\t\texpect: RouteInfo{\n\t\t\t\tMethod:     http.MethodGet,\n\t\t\t\tPath:       \"users/:id/:file\",\n\t\t\t\tParameters: []string{\"id\", \"file\"},\n\t\t\t\tName:       \"GET:users/:id/:file\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tri := tc.given.ToRouteInfo(tc.whenParams)\n\t\t\tassert.Equal(t, tc.expect, ri)\n\t\t})\n\t}\n}\n\nfunc TestRoute_ForGroup(t *testing.T) {\n\troute := Route{\n\t\tMethod: http.MethodGet,\n\t\tPath:   \"/test\",\n\t\tHandler: func(c *Context) error {\n\t\t\treturn c.String(http.StatusTeapot, \"OK\")\n\t\t},\n\t\tMiddlewares: nil,\n\t\tName:        \"test route\",\n\t}\n\n\tmw := func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\treturn next(c)\n\t\t}\n\t}\n\tr := route.WithPrefix(\"/users\", []MiddlewareFunc{mw})\n\n\tassert.Equal(t, r.Method, http.MethodGet)\n\tassert.Equal(t, r.Path, \"/users/test\")\n\tassert.NotNil(t, r.Handler)\n\tassert.Len(t, r.Middlewares, 1)\n\tassert.Equal(t, r.Name, \"test route\")\n}\n\nfunc exampleRoutes() Routes {\n\treturn Routes{\n\t\tRouteInfo{\n\t\t\tMethod:     http.MethodGet,\n\t\t\tPath:       \"/users\",\n\t\t\tParameters: nil,\n\t\t\tName:       \"GET:/users\",\n\t\t},\n\t\tRouteInfo{\n\t\t\tMethod:     http.MethodGet,\n\t\t\tPath:       \"/users/:id\",\n\t\t\tParameters: []string{\"id\"},\n\t\t\tName:       \"GET:/users/:id\",\n\t\t},\n\t\tRouteInfo{\n\t\t\tMethod:     http.MethodPost,\n\t\t\tPath:       \"/users/:id\",\n\t\t\tParameters: []string{\"id\"},\n\t\t\tName:       \"POST:/users/:id\",\n\t\t},\n\t\tRouteInfo{\n\t\t\tMethod:     http.MethodDelete,\n\t\t\tPath:       \"/groups\",\n\t\t\tParameters: nil,\n\t\t\tName:       \"non_unique_name\",\n\t\t},\n\t\tRouteInfo{\n\t\t\tMethod:     http.MethodPost,\n\t\t\tPath:       \"/groups\",\n\t\t\tParameters: nil,\n\t\t\tName:       \"non_unique_name\",\n\t\t},\n\t}\n}\n\nfunc TestRoutes_Clone(t *testing.T) {\n\torg := exampleRoutes()\n\tcloned := org.Clone()\n\n\tassert.Equal(t, org, cloned)\n\n\torg[1].Path = \"r1\"\n\torg[1].Parameters[0] = \"p0\"\n\n\tassert.NotEqual(t, org, cloned)\n}\n\nfunc TestRoutes_FindByMethodPath(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname        string\n\t\twhenMethod  string\n\t\twhenPath    string\n\t\texpectName  string\n\t\texpectError string\n\t\tgiven       Routes\n\t}{\n\t\t{\n\t\t\tname:       \"ok, found\",\n\t\t\tgiven:      exampleRoutes(),\n\t\t\twhenMethod: http.MethodGet,\n\t\t\twhenPath:   \"/users/:id\",\n\t\t\texpectName: \"GET:/users/:id\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, not found\",\n\t\t\tgiven:       exampleRoutes(),\n\t\t\twhenMethod:  http.MethodPut,\n\t\t\twhenPath:    \"/users/:id\",\n\t\t\texpectName:  \"\",\n\t\t\texpectError: \"route not found by method and path\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, not found from nil\",\n\t\t\tgiven:       nil,\n\t\t\twhenMethod:  http.MethodGet,\n\t\t\twhenPath:    \"/users/:id\",\n\t\t\texpectName:  \"\",\n\t\t\texpectError: \"route not found by method and path\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tri, err := tc.given.FindByMethodPath(tc.whenMethod, tc.whenPath)\n\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t\tassert.Equal(t, RouteInfo{}, ri)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tif tc.expectName != \"\" {\n\t\t\t\tassert.Equal(t, tc.expectName, ri.Name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRoutes_FilterByMethod(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname        string\n\t\twhenMethod  string\n\t\texpectError string\n\t\tgiven       Routes\n\t\texpectNames []string\n\t}{\n\t\t{\n\t\t\tname:        \"ok, found\",\n\t\t\tgiven:       exampleRoutes(),\n\t\t\twhenMethod:  http.MethodGet,\n\t\t\texpectNames: []string{\"GET:/users\", \"GET:/users/:id\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, not found\",\n\t\t\tgiven:       exampleRoutes(),\n\t\t\twhenMethod:  http.MethodPut,\n\t\t\texpectNames: nil,\n\t\t\texpectError: \"route not found by method\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, not found from nil\",\n\t\t\tgiven:       nil,\n\t\t\twhenMethod:  http.MethodGet,\n\t\t\texpectNames: nil,\n\t\t\texpectError: \"route not found by method\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tris, err := tc.given.FilterByMethod(tc.whenMethod)\n\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\n\t\t\tif len(tc.expectNames) > 0 {\n\t\t\t\tassert.Len(t, ris, len(tc.expectNames))\n\t\t\t\tfor _, ri := range ris {\n\t\t\t\t\tassert.Contains(t, tc.expectNames, ri.Name)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, ris)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRoutes_FilterByPath(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname        string\n\t\twhenPath    string\n\t\texpectError string\n\t\tgiven       Routes\n\t\texpectNames []string\n\t}{\n\t\t{\n\t\t\tname:        \"ok, found\",\n\t\t\tgiven:       exampleRoutes(),\n\t\t\twhenPath:    \"/users/:id\",\n\t\t\texpectNames: []string{\"GET:/users/:id\", \"POST:/users/:id\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, not found\",\n\t\t\tgiven:       exampleRoutes(),\n\t\t\twhenPath:    \"/\",\n\t\t\texpectNames: nil,\n\t\t\texpectError: \"route not found by path\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, not found from nil\",\n\t\t\tgiven:       nil,\n\t\t\twhenPath:    \"/users/:id\",\n\t\t\texpectNames: nil,\n\t\t\texpectError: \"route not found by path\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tris, err := tc.given.FilterByPath(tc.whenPath)\n\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\n\t\t\tif len(tc.expectNames) > 0 {\n\t\t\t\tassert.Len(t, ris, len(tc.expectNames))\n\t\t\t\tfor _, ri := range ris {\n\t\t\t\t\tassert.Contains(t, tc.expectNames, ri.Name)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, ris)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRoutes_FilterByName(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname             string\n\t\twhenName         string\n\t\texpectError      string\n\t\tgiven            Routes\n\t\texpectMethodPath []string\n\t}{\n\t\t{\n\t\t\tname:             \"ok, found multiple\",\n\t\t\tgiven:            exampleRoutes(),\n\t\t\twhenName:         \"non_unique_name\",\n\t\t\texpectMethodPath: []string{\"DELETE:/groups\", \"POST:/groups\"},\n\t\t},\n\t\t{\n\t\t\tname:             \"ok, found single\",\n\t\t\tgiven:            exampleRoutes(),\n\t\t\twhenName:         \"GET:/users/:id\",\n\t\t\texpectMethodPath: []string{\"GET:/users/:id\"},\n\t\t},\n\t\t{\n\t\t\tname:             \"nok, not found\",\n\t\t\tgiven:            exampleRoutes(),\n\t\t\twhenName:         \"/\",\n\t\t\texpectMethodPath: nil,\n\t\t\texpectError:      \"route not found by name\",\n\t\t},\n\t\t{\n\t\t\tname:             \"nok, not found from nil\",\n\t\t\tgiven:            nil,\n\t\t\twhenName:         \"/users/:id\",\n\t\t\texpectMethodPath: nil,\n\t\t\texpectError:      \"route not found by name\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tris, err := tc.given.FilterByName(tc.whenName)\n\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\n\t\t\tif len(tc.expectMethodPath) > 0 {\n\t\t\t\tassert.Len(t, ris, len(tc.expectMethodPath))\n\t\t\t\tfor _, ri := range ris {\n\t\t\t\t\tassert.Contains(t, tc.expectMethodPath, fmt.Sprintf(\"%v:%v\", ri.Method, ri.Path))\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, ris)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRouteInfo_Reverse(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname        string\n\t\tgivenPath   string\n\t\texpect      string\n\t\tgivenParams []string\n\t\twhenParams  []any\n\t}{\n\t\t{\n\t\t\tname:      \"ok,static with no params\",\n\t\t\tgivenPath: \"/static\",\n\t\t\texpect:    \"/static\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok,static with non existent param\",\n\t\t\tgivenParams: []string{\"missing param\"},\n\t\t\tgivenPath:   \"/static\",\n\t\t\twhenParams:  []any{\"missing param\"},\n\t\t\texpect:      \"/static\",\n\t\t},\n\t\t{\n\t\t\tname:      \"ok, wildcard with no params\",\n\t\t\tgivenPath: \"/static/*\",\n\t\t\texpect:    \"/static/*\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, wildcard with params\",\n\t\t\tgivenParams: []string{\"foo.txt\"},\n\t\t\tgivenPath:   \"/static/*\",\n\t\t\twhenParams:  []any{\"foo.txt\"},\n\t\t\texpect:      \"/static/foo.txt\",\n\t\t},\n\t\t{\n\t\t\tname:      \"ok, single param without param\",\n\t\t\tgivenPath: \"/params/:foo\",\n\t\t\texpect:    \"/params/:foo\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, single param with param\",\n\t\t\tgivenParams: []string{\"one\"},\n\t\t\tgivenPath:   \"/params/:foo\",\n\t\t\twhenParams:  []any{\"one\"},\n\t\t\texpect:      \"/params/one\",\n\t\t},\n\t\t{\n\t\t\tname:      \"ok, multi param without params\",\n\t\t\tgivenPath: \"/params/:foo/bar/:qux\",\n\t\t\texpect:    \"/params/:foo/bar/:qux\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, multi param with one param\",\n\t\t\tgivenParams: []string{\"one\"},\n\t\t\tgivenPath:   \"/params/:foo/bar/:qux\",\n\t\t\twhenParams:  []any{\"one\"},\n\t\t\texpect:      \"/params/one/bar/:qux\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, multi param with all params\",\n\t\t\tgivenParams: []string{\"one\", \"two\"},\n\t\t\tgivenPath:   \"/params/:foo/bar/:qux\",\n\t\t\twhenParams:  []any{\"one\", \"two\"},\n\t\t\texpect:      \"/params/one/bar/two\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, multi param + wildcard with all params\",\n\t\t\tgivenParams: []string{\"one\", \"two\", \"three\"},\n\t\t\tgivenPath:   \"/params/:foo/bar/:qux/*\",\n\t\t\twhenParams:  []any{\"one\", \"two\", \"three\"},\n\t\t\texpect:      \"/params/one/bar/two/three\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, backslash is not escaped\",\n\t\t\tgivenParams: []string{\"test\"},\n\t\t\tgivenPath:   \"/a\\\\b/:x\",\n\t\t\twhenParams:  []any{\"test\"},\n\t\t\texpect:      `/a\\b/test`,\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, escaped colon verbs\",\n\t\t\tgivenParams: []string{\"PATCH\"},\n\t\t\tgivenPath:   \"/params\\\\::customVerb\",\n\t\t\twhenParams:  []any{\"PATCH\"},\n\t\t\texpect:      `/params:PATCH`,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tr := RouteInfo{\n\t\t\t\tPath:       tc.givenPath,\n\t\t\t\tParameters: tc.givenParams,\n\t\t\t\tName:       tc.expect,\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expect, r.Reverse(tc.whenParams...))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "router.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\n// Router is interface for routing request contexts to registered routes.\n//\n// Contract between Echo/Context instance and the router:\n//   - all routes must be added through methods on echo.Echo instance.\n//     Reason: Echo instance uses RouteInfo.Params() length to allocate slice for paths parameters (see `Echo.contextPathParamAllocSize`).\n//   - Router must populate Context during Router.Route call with:\n//   - Context.InitializeRoute (IMPORTANT! to reduce allocations use same slice that c.PathValues() returns)\n//   - Optionally can set additional information to Context with Context.Set\ntype Router interface {\n\t// Add registers Routable with the Router and returns registered RouteInfo.\n\t//\n\t// Router may change Route.Path value in returned RouteInfo.Path.\n\t// Router generates RouteInfo.Parameters values from Route.Path.\n\t// Router generates RouteInfo.Name value if it is not provided.\n\tAdd(routable Route) (RouteInfo, error)\n\n\t// Remove removes route from the Router.\n\t//\n\t// Router may choose not to implement this method.\n\tRemove(method string, path string) error\n\n\t// Routes returns information about all registered routes\n\tRoutes() Routes\n\n\t// Route searches Router for matching route and applies it to the given context. In case when no matching method\n\t// was not found (405) or no matching route exists for path (404), router will return its implementation of 405/404\n\t// handler function.\n\t//\n\t// Router must populate Context during Router.Route call with:\n\t// - Context.InitializeRoute() (IMPORTANT! to reduce allocations use same slice that c.PathValues() returns)\n\t// - optionally can set additional information to Context with Context.Set()\n\tRoute(c *Context) HandlerFunc\n}\n\nconst (\n\t// NotFoundRouteName is name of RouteInfo returned when router did not find matching route (404: not found).\n\tNotFoundRouteName = \"echo_route_not_found_name\"\n\t// MethodNotAllowedRouteName is name of RouteInfo returned when router did not find matching method for route  (405: method not allowed).\n\tMethodNotAllowedRouteName = \"echo_route_method_not_allowed_name\"\n)\n\n// Routes is collection of RouteInfo instances with various helper methods.\ntype Routes []RouteInfo\n\n// DefaultRouter is the registry of all registered routes for an `Echo` instance for\n// request matching and URL path parameter parsing.\n// Note: DefaultRouter is not coroutine-safe. Do not Add/Remove routes after HTTP server has been started with Echo.\ntype DefaultRouter struct {\n\ttree                    *node\n\tnotFoundHandler         HandlerFunc\n\tmethodNotAllowedHandler HandlerFunc\n\toptionsMethodHandler    HandlerFunc\n\troutes                  Routes\n\t// maxPathParamsLength tracks highest count of PathValues for all routes.\n\tmaxPathParamsLength int\n\n\tallowOverwritingRoute    bool\n\tunescapePathParamValues  bool\n\tuseEscapedPathForRouting bool\n}\n\n// RouterConfig is configuration options for (default) router\ntype RouterConfig struct {\n\tNotFoundHandler           HandlerFunc\n\tMethodNotAllowedHandler   HandlerFunc\n\tOptionsMethodHandler      HandlerFunc\n\tAllowOverwritingRoute     bool\n\tUnescapePathParamValues   bool\n\tUseEscapedPathForMatching bool\n}\n\n// NewRouter returns a new Router instance.\nfunc NewRouter(config RouterConfig) *DefaultRouter {\n\tr := &DefaultRouter{\n\t\ttree: &node{\n\t\t\tmethods:   new(routeMethods),\n\t\t\tisLeaf:    true,\n\t\t\tisHandler: false,\n\t\t},\n\t\troutes: make(Routes, 0),\n\n\t\tallowOverwritingRoute:    config.AllowOverwritingRoute,\n\t\tunescapePathParamValues:  config.UnescapePathParamValues,\n\t\tuseEscapedPathForRouting: config.UseEscapedPathForMatching,\n\n\t\tnotFoundHandler:         notFoundHandler,\n\t\tmethodNotAllowedHandler: methodNotAllowedHandler,\n\t\toptionsMethodHandler:    optionsMethodHandler,\n\t}\n\tif config.NotFoundHandler != nil {\n\t\tr.notFoundHandler = config.NotFoundHandler\n\t}\n\tif config.MethodNotAllowedHandler != nil {\n\t\tr.methodNotAllowedHandler = config.MethodNotAllowedHandler\n\t}\n\tif config.OptionsMethodHandler != nil {\n\t\tr.optionsMethodHandler = config.OptionsMethodHandler\n\t}\n\treturn r\n}\n\ntype children []*node\n\ntype node struct {\n\tparent         *node\n\tmethods        *routeMethods\n\tparamChild     *node\n\tanyChild       *node\n\tprefix         string\n\toriginalPath   string\n\tstaticChildren children\n\tparamsCount    int\n\tkind           kind\n\tlabel          byte\n\tisLeaf         bool\n\tisHandler      bool\n}\n\ntype kind uint8\n\nconst (\n\tstaticKind kind = iota\n\tparamKind\n\tanyKind\n\n\tparamLabel = byte(':')\n\tanyLabel   = byte('*')\n)\n\ntype routeMethod struct {\n\t*RouteInfo\n\thandler      HandlerFunc\n\torgRouteInfo RouteInfo\n}\n\ntype routeMethods struct {\n\tconnect  *routeMethod\n\tdelete   *routeMethod\n\tget      *routeMethod\n\thead     *routeMethod\n\toptions  *routeMethod\n\tpatch    *routeMethod\n\tpost     *routeMethod\n\tpropfind *routeMethod\n\tput      *routeMethod\n\ttrace    *routeMethod\n\treport   *routeMethod\n\tany      *routeMethod\n\tanyOther map[string]*routeMethod\n\n\t// notFoundHandler is handler registered with RouteNotFound method and is executed for 404 cases\n\tnotFoundHandler *routeMethod\n\n\t// allowHeader contains comma-separated list of Methods registered to this node path.\n\t// it is optimization for http.StatusMethodNotAllowed (405) handling.\n\tallowHeader string\n}\n\nfunc (m *routeMethods) set(method string, r *routeMethod) {\n\tswitch method {\n\tcase http.MethodConnect:\n\t\tm.connect = r\n\tcase http.MethodDelete:\n\t\tm.delete = r\n\tcase http.MethodGet:\n\t\tm.get = r\n\tcase http.MethodHead:\n\t\tm.head = r\n\tcase http.MethodOptions:\n\t\tm.options = r\n\tcase http.MethodPatch:\n\t\tm.patch = r\n\tcase http.MethodPost:\n\t\tm.post = r\n\tcase PROPFIND:\n\t\tm.propfind = r\n\tcase http.MethodPut:\n\t\tm.put = r\n\tcase http.MethodTrace:\n\t\tm.trace = r\n\tcase REPORT:\n\t\tm.report = r\n\tcase RouteAny:\n\t\tm.any = r\n\tcase RouteNotFound:\n\t\tm.notFoundHandler = r\n\t\treturn // RouteNotFound/404 is not considered as a handler so no further logic needs to be executed\n\tdefault:\n\t\tif m.anyOther == nil {\n\t\t\tm.anyOther = make(map[string]*routeMethod)\n\t\t}\n\t\tif r.handler == nil {\n\t\t\tdelete(m.anyOther, method)\n\t\t} else {\n\t\t\tm.anyOther[method] = r\n\t\t}\n\t}\n\tm.updateAllowHeader()\n}\n\nfunc (m *routeMethods) find(method string, fallbackToAny bool) *routeMethod {\n\tvar r *routeMethod\n\tswitch method {\n\tcase http.MethodConnect:\n\t\tr = m.connect\n\tcase http.MethodDelete:\n\t\tr = m.delete\n\tcase http.MethodGet:\n\t\tr = m.get\n\tcase http.MethodHead:\n\t\tr = m.head\n\tcase http.MethodOptions:\n\t\tr = m.options\n\tcase http.MethodPatch:\n\t\tr = m.patch\n\tcase http.MethodPost:\n\t\tr = m.post\n\tcase PROPFIND:\n\t\tr = m.propfind\n\tcase http.MethodPut:\n\t\tr = m.put\n\tcase http.MethodTrace:\n\t\tr = m.trace\n\tcase REPORT:\n\t\tr = m.report\n\tcase RouteAny:\n\t\tr = m.any\n\tcase RouteNotFound:\n\t\tr = m.notFoundHandler\n\tdefault:\n\t\tr = m.anyOther[method]\n\t}\n\tif r != nil || !fallbackToAny {\n\t\treturn r\n\t}\n\treturn m.any\n}\n\nfunc (m *routeMethods) updateAllowHeader() {\n\tbuf := new(bytes.Buffer)\n\tbuf.WriteString(http.MethodOptions)\n\thasAnyMethod := m.any != nil\n\n\tif hasAnyMethod || m.connect != nil {\n\t\tbuf.WriteString(\", \")\n\t\tbuf.WriteString(http.MethodConnect)\n\t}\n\tif hasAnyMethod || m.delete != nil {\n\t\tbuf.WriteString(\", \")\n\t\tbuf.WriteString(http.MethodDelete)\n\t}\n\tif hasAnyMethod || m.get != nil {\n\t\tbuf.WriteString(\", \")\n\t\tbuf.WriteString(http.MethodGet)\n\t}\n\tif hasAnyMethod || m.head != nil {\n\t\tbuf.WriteString(\", \")\n\t\tbuf.WriteString(http.MethodHead)\n\t}\n\tif hasAnyMethod || m.patch != nil {\n\t\tbuf.WriteString(\", \")\n\t\tbuf.WriteString(http.MethodPatch)\n\t}\n\tif hasAnyMethod || m.post != nil {\n\t\tbuf.WriteString(\", \")\n\t\tbuf.WriteString(http.MethodPost)\n\t}\n\tif hasAnyMethod || m.propfind != nil {\n\t\tbuf.WriteString(\", PROPFIND\")\n\t}\n\tif hasAnyMethod || m.put != nil {\n\t\tbuf.WriteString(\", \")\n\t\tbuf.WriteString(http.MethodPut)\n\t}\n\tif hasAnyMethod || m.trace != nil {\n\t\tbuf.WriteString(\", \")\n\t\tbuf.WriteString(http.MethodTrace)\n\t}\n\tif hasAnyMethod || m.report != nil {\n\t\tbuf.WriteString(\", REPORT\")\n\t}\n\tfor method := range m.anyOther { // for simplicity, we use map and therefore order is not deterministic here\n\t\tbuf.WriteString(\", \")\n\t\tbuf.WriteString(method)\n\t}\n\tm.allowHeader = buf.String()\n}\n\nfunc (m *routeMethods) isHandler() bool {\n\treturn m.get != nil ||\n\t\tm.post != nil ||\n\t\tm.options != nil ||\n\t\tm.put != nil ||\n\t\tm.delete != nil ||\n\t\tm.connect != nil ||\n\t\tm.head != nil ||\n\t\tm.patch != nil ||\n\t\tm.propfind != nil ||\n\t\tm.trace != nil ||\n\t\tm.report != nil ||\n\t\tm.any != nil ||\n\t\tlen(m.anyOther) != 0\n\t// RouteNotFound/404 is not considered as a handler\n}\n\n// Routes returns all registered routes\nfunc (r *DefaultRouter) Routes() Routes {\n\treturn r.routes\n}\n\n// Remove unregisters registered route\nfunc (r *DefaultRouter) Remove(method string, path string) error {\n\tcurrentNode := r.tree\n\tif currentNode == nil || (currentNode.isLeaf && !currentNode.isHandler) {\n\t\treturn errors.New(\"router has no routes to remove\")\n\t}\n\n\tif path == \"\" {\n\t\tpath = \"/\"\n\t}\n\tif path[0] != '/' {\n\t\tpath = \"/\" + path\n\t}\n\n\tvar nodeToRemove *node\n\tprefixLen := 0\n\tfor {\n\t\tif currentNode.originalPath == path && currentNode.isHandler {\n\t\t\tnodeToRemove = currentNode\n\t\t\tbreak\n\t\t}\n\t\tif currentNode.kind == staticKind {\n\t\t\tprefixLen = prefixLen + len(currentNode.prefix)\n\t\t} else {\n\t\t\tprefixLen = len(currentNode.originalPath)\n\t\t}\n\n\t\tif prefixLen >= len(path) {\n\t\t\tbreak\n\t\t}\n\n\t\tnext := path[prefixLen]\n\t\tswitch next {\n\t\tcase paramLabel:\n\t\t\tcurrentNode = currentNode.paramChild\n\t\tcase anyLabel:\n\t\t\tcurrentNode = currentNode.anyChild\n\t\tdefault:\n\t\t\tcurrentNode = currentNode.findStaticChild(next)\n\t\t}\n\n\t\tif currentNode == nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif nodeToRemove == nil {\n\t\treturn errors.New(\"could not find route to remove by given path\")\n\t}\n\n\tif !nodeToRemove.isHandler {\n\t\treturn errors.New(\"could not find route to remove by given path\")\n\t}\n\n\tif mh := nodeToRemove.methods.find(method, false); mh == nil {\n\t\treturn errors.New(\"could not find route to remove by given path and method\")\n\t}\n\tnodeToRemove.setHandler(method, nil)\n\n\tvar rIndex int\n\tfor i, rr := range r.routes {\n\t\tif rr.Method == method && rr.Path == path {\n\t\t\trIndex = i\n\t\t\tbreak\n\t\t}\n\t}\n\tr.routes = append(r.routes[:rIndex], r.routes[rIndex+1:]...)\n\n\tif !nodeToRemove.isHandler && nodeToRemove.isLeaf {\n\t\t// TODO: if !nodeToRemove.isLeaf and has at least 2 children merge paths for remaining nodes?\n\t\tcurrent := nodeToRemove\n\t\tfor {\n\t\t\tparent := current.parent\n\t\t\tif parent == nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tswitch current.kind {\n\t\t\tcase staticKind:\n\t\t\t\tvar index int\n\t\t\t\tfor i, c := range parent.staticChildren {\n\t\t\t\t\tif c == current {\n\t\t\t\t\t\tindex = i\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tparent.staticChildren = append(parent.staticChildren[:index], parent.staticChildren[index+1:]...)\n\t\t\tcase paramKind:\n\t\t\t\tparent.paramChild = nil\n\t\t\tcase anyKind:\n\t\t\t\tparent.anyChild = nil\n\t\t\t}\n\n\t\t\tparent.isLeaf = parent.anyChild == nil && parent.paramChild == nil && len(parent.staticChildren) == 0\n\t\t\tif !parent.isLeaf || parent.isHandler {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tcurrent = parent\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// AddRouteError is error returned by Router.Add containing information what actual route adding failed. Useful for\n// mass adding (i.e. Any() routes)\ntype AddRouteError struct {\n\tErr    error\n\tMethod string\n\tPath   string\n}\n\nfunc (e *AddRouteError) Error() string { return e.Method + \" \" + e.Path + \": \" + e.Err.Error() }\n\nfunc (e *AddRouteError) Unwrap() error { return e.Err }\n\nfunc newAddRouteError(route Route, err error) *AddRouteError {\n\treturn &AddRouteError{\n\t\tMethod: route.Method,\n\t\tPath:   route.Path,\n\t\tErr:    err,\n\t}\n}\n\n// Add registers a new route for method and path with matching handler.\nfunc (r *DefaultRouter) Add(route Route) (RouteInfo, error) {\n\tif route.Handler == nil {\n\t\treturn RouteInfo{}, newAddRouteError(route, errors.New(\"adding route without handler function\"))\n\t}\n\tmethod := route.Method\n\tpath := normalizePathSlash(route.Path)\n\n\th := applyMiddleware(route.Handler, route.Middlewares...)\n\tif !r.allowOverwritingRoute {\n\t\tfor _, rr := range r.routes {\n\t\t\tif route.Method == rr.Method && route.Path == rr.Path {\n\t\t\t\treturn RouteInfo{}, newAddRouteError(route, errors.New(\"adding duplicate route (same method+path) is not allowed\"))\n\t\t\t}\n\t\t}\n\t}\n\n\tparamNames := make([]string, 0)\n\toriginalPath := path\n\twasAdded := false\n\tvar ri RouteInfo\n\tfor i, lcpIndex := 0, len(path); i < lcpIndex; i++ {\n\t\tif path[i] == paramLabel {\n\t\t\tif i > 0 && path[i-1] == '\\\\' {\n\t\t\t\tpath = path[:i-1] + path[i:]\n\t\t\t\ti--\n\t\t\t\tlcpIndex--\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tj := i + 1\n\n\t\t\tr.insert(staticKind, path[:i], method, routeMethod{RouteInfo: &RouteInfo{Method: method}})\n\t\t\tfor ; i < lcpIndex && path[i] != '/'; i++ {\n\t\t\t}\n\n\t\t\tparamNames = append(paramNames, path[j:i])\n\t\t\tpath = path[:j] + path[i:]\n\t\t\ti, lcpIndex = j, len(path)\n\n\t\t\tif i == lcpIndex {\n\t\t\t\t// path node is last fragment of route path. ie. `/users/:id`\n\t\t\t\tri = route.ToRouteInfo(paramNames)\n\t\t\t\trm := routeMethod{\n\t\t\t\t\tRouteInfo:    &RouteInfo{Method: method, Path: originalPath, Parameters: paramNames, Name: route.Name},\n\t\t\t\t\thandler:      h,\n\t\t\t\t\torgRouteInfo: ri,\n\t\t\t\t}\n\t\t\t\tr.insert(paramKind, path[:i], method, rm)\n\t\t\t\twasAdded = true\n\t\t\t\tbreak\n\t\t\t} else {\n\t\t\t\tr.insert(paramKind, path[:i], method, routeMethod{RouteInfo: &RouteInfo{Method: method}})\n\t\t\t}\n\t\t} else if path[i] == anyLabel {\n\t\t\tr.insert(staticKind, path[:i], method, routeMethod{RouteInfo: &RouteInfo{Method: method}})\n\t\t\tparamNames = append(paramNames, \"*\")\n\t\t\tri = route.ToRouteInfo(paramNames)\n\t\t\trm := routeMethod{\n\t\t\t\tRouteInfo:    &RouteInfo{Method: method, Path: originalPath, Parameters: paramNames, Name: route.Name},\n\t\t\t\thandler:      h,\n\t\t\t\torgRouteInfo: ri,\n\t\t\t}\n\t\t\tr.insert(anyKind, path[:i+1], method, rm)\n\t\t\twasAdded = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !wasAdded {\n\t\tri = route.ToRouteInfo(paramNames)\n\t\trm := routeMethod{\n\t\t\tRouteInfo:    &RouteInfo{Method: method, Path: originalPath, Parameters: paramNames, Name: route.Name},\n\t\t\thandler:      h,\n\t\t\torgRouteInfo: ri,\n\t\t}\n\t\tr.insert(staticKind, path, method, rm)\n\t}\n\n\tr.storeRouteInfo(ri)\n\n\treturn ri, nil\n}\n\nfunc normalizePathSlash(path string) string {\n\tif path == \"\" {\n\t\tpath = \"/\"\n\t} else if path[0] != '/' {\n\t\tpath = \"/\" + path\n\t}\n\treturn path\n}\n\nfunc (r *DefaultRouter) storeRouteInfo(ri RouteInfo) {\n\tfor i, rr := range r.routes {\n\t\tif ri.Method == rr.Method && ri.Path == rr.Path {\n\t\t\tr.routes[i] = ri\n\t\t\treturn\n\t\t}\n\t}\n\tr.routes = append(r.routes, ri)\n}\n\nfunc (r *DefaultRouter) insert(t kind, path string, method string, ri routeMethod) {\n\tif len(ri.Parameters) > r.maxPathParamsLength {\n\t\tr.maxPathParamsLength = len(ri.Parameters)\n\t}\n\tcurrentNode := r.tree // Current node as root\n\tsearch := path\n\n\tfor {\n\t\tsearchLen := len(search)\n\t\tprefixLen := len(currentNode.prefix)\n\t\tlcpLen := 0\n\n\t\t// LCP - Longest Common Prefix (https://en.wikipedia.org/wiki/LCP_array)\n\t\tmaxL := prefixLen\n\t\tif searchLen < maxL {\n\t\t\tmaxL = searchLen\n\t\t}\n\t\tfor ; lcpLen < maxL && search[lcpLen] == currentNode.prefix[lcpLen]; lcpLen++ {\n\t\t}\n\n\t\tif lcpLen == 0 {\n\t\t\t// At root node\n\t\t\tcurrentNode.label = search[0]\n\t\t\tcurrentNode.prefix = search\n\t\t\tif ri.handler != nil {\n\t\t\t\tcurrentNode.kind = t\n\t\t\t\tcurrentNode.setHandler(method, &ri)\n\t\t\t\tcurrentNode.paramsCount = len(ri.Parameters)\n\t\t\t\tcurrentNode.originalPath = ri.Path\n\t\t\t}\n\t\t\tcurrentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil\n\t\t} else if lcpLen < prefixLen {\n\t\t\t// Split node into two before we insert new node.\n\t\t\t// This happens when we are inserting path that is submatch of any existing inserted paths.\n\t\t\t// For example, we have node `/test` and now are about to insert `/te/*`. In that case\n\t\t\t// 1. overlapping part is `/te` that is used as parent node\n\t\t\t// 2. `st` is part from existing node that is not matching - it gets its own node (child to `/te`)\n\t\t\t// 3. `/*` is the new part we are about to insert (child to `/te`)\n\t\t\tn := newNode(\n\t\t\t\tcurrentNode.kind,\n\t\t\t\tcurrentNode.prefix[lcpLen:],\n\t\t\t\tcurrentNode,\n\t\t\t\tcurrentNode.staticChildren,\n\t\t\t\tcurrentNode.methods,\n\t\t\t\tcurrentNode.paramsCount,\n\t\t\t\tcurrentNode.originalPath,\n\t\t\t\tcurrentNode.paramChild,\n\t\t\t\tcurrentNode.anyChild,\n\t\t\t)\n\t\t\t// Update parent path for all children to new node\n\t\t\tfor _, child := range currentNode.staticChildren {\n\t\t\t\tchild.parent = n\n\t\t\t}\n\t\t\tif currentNode.paramChild != nil {\n\t\t\t\tcurrentNode.paramChild.parent = n\n\t\t\t}\n\t\t\tif currentNode.anyChild != nil {\n\t\t\t\tcurrentNode.anyChild.parent = n\n\t\t\t}\n\n\t\t\t// Reset parent node\n\t\t\tcurrentNode.kind = staticKind\n\t\t\tcurrentNode.label = currentNode.prefix[0]\n\t\t\tcurrentNode.prefix = currentNode.prefix[:lcpLen]\n\t\t\tcurrentNode.staticChildren = nil\n\t\t\tcurrentNode.methods = new(routeMethods)\n\t\t\tcurrentNode.originalPath = \"\"\n\t\t\tcurrentNode.paramsCount = 0\n\t\t\tcurrentNode.paramChild = nil\n\t\t\tcurrentNode.anyChild = nil\n\t\t\tcurrentNode.isLeaf = false\n\t\t\tcurrentNode.isHandler = false\n\n\t\t\t// Only Static children could reach here\n\t\t\tcurrentNode.addStaticChild(n)\n\n\t\t\tif lcpLen == searchLen {\n\t\t\t\t// At parent node\n\t\t\t\tcurrentNode.kind = t\n\t\t\t\tif ri.handler != nil {\n\t\t\t\t\tcurrentNode.setHandler(method, &ri)\n\t\t\t\t\tcurrentNode.paramsCount = len(ri.Parameters)\n\t\t\t\t\tcurrentNode.originalPath = ri.Path\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Create child node\n\t\t\t\tn = newNode(t, search[lcpLen:], currentNode, nil, new(routeMethods), 0, ri.Path, nil, nil)\n\t\t\t\tif ri.handler != nil {\n\t\t\t\t\tn.setHandler(method, &ri)\n\t\t\t\t\tn.paramsCount = len(ri.Parameters)\n\t\t\t\t}\n\t\t\t\t// Only Static children could reach here\n\t\t\t\tcurrentNode.addStaticChild(n)\n\t\t\t}\n\t\t\tcurrentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil\n\t\t} else if lcpLen < searchLen {\n\t\t\tsearch = search[lcpLen:]\n\t\t\tc := currentNode.findChildWithLabel(search[0])\n\t\t\tif c != nil {\n\t\t\t\t// Go deeper\n\t\t\t\tcurrentNode = c\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Create child node\n\t\t\tn := newNode(t, search, currentNode, nil, new(routeMethods), 0, ri.Path, nil, nil)\n\t\t\tif ri.handler != nil {\n\t\t\t\tn.setHandler(method, &ri)\n\t\t\t\tn.paramsCount = len(ri.Parameters)\n\t\t\t}\n\t\t\tswitch t {\n\t\t\tcase staticKind:\n\t\t\t\tcurrentNode.addStaticChild(n)\n\t\t\tcase paramKind:\n\t\t\t\tcurrentNode.paramChild = n\n\t\t\tcase anyKind:\n\t\t\t\tcurrentNode.anyChild = n\n\t\t\t}\n\t\t\tcurrentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil\n\t\t} else {\n\t\t\t// Node already exists\n\t\t\tif ri.handler != nil {\n\t\t\t\tcurrentNode.setHandler(method, &ri)\n\t\t\t\tcurrentNode.paramsCount = len(ri.Parameters)\n\t\t\t\tcurrentNode.originalPath = ri.Path\n\t\t\t}\n\t\t}\n\t\treturn\n\t}\n}\n\nfunc newNode(\n\tt kind,\n\tpre string,\n\tp *node,\n\tsc children,\n\tmh *routeMethods,\n\tparamsCount int,\n\tppath string,\n\tparamChildren,\n\tanyChildren *node,\n) *node {\n\treturn &node{\n\t\tkind:           t,\n\t\tlabel:          pre[0],\n\t\tprefix:         pre,\n\t\tparent:         p,\n\t\tstaticChildren: sc,\n\t\toriginalPath:   ppath,\n\t\tparamsCount:    paramsCount,\n\t\tmethods:        mh,\n\t\tparamChild:     paramChildren,\n\t\tanyChild:       anyChildren,\n\t\tisLeaf:         sc == nil && paramChildren == nil && anyChildren == nil,\n\t\tisHandler:      mh.isHandler(),\n\t}\n}\n\nfunc (n *node) addStaticChild(c *node) {\n\tn.staticChildren = append(n.staticChildren, c)\n}\n\nfunc (n *node) findStaticChild(l byte) *node {\n\tfor _, c := range n.staticChildren {\n\t\tif c.label == l {\n\t\t\treturn c\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (n *node) findChildWithLabel(l byte) *node {\n\tif c := n.findStaticChild(l); c != nil {\n\t\treturn c\n\t}\n\tif l == paramLabel {\n\t\treturn n.paramChild\n\t}\n\tif l == anyLabel {\n\t\treturn n.anyChild\n\t}\n\treturn nil\n}\n\nfunc (n *node) setHandler(method string, r *routeMethod) {\n\tn.methods.set(method, r)\n\tn.isHandler = n.methods.isHandler()\n}\n\n// Note: notFoundRouteInfo exists to avoid allocations when setting 404 RouteInfo to Context\nvar notFoundRouteInfo = &RouteInfo{\n\tMethod:     \"\",\n\tPath:       \"\",\n\tParameters: nil,\n\tName:       NotFoundRouteName,\n}\n\n// Note: methodNotAllowedRouteInfo exists to avoid allocations when setting 405 RouteInfo to Context\nvar methodNotAllowedRouteInfo = &RouteInfo{\n\tMethod:     \"\",\n\tPath:       \"\",\n\tParameters: nil,\n\tName:       MethodNotAllowedRouteName,\n}\n\n// notFoundHandler is handler for 404 cases\n// Handle returned ErrNotFound errors in Echo.HTTPErrorHandler\nvar notFoundHandler = func(c *Context) error {\n\treturn ErrNotFound\n}\n\n// methodNotAllowedHandler is handler for case when route for path+method match was not found (http code 405)\n// Handle returned ErrMethodNotAllowed errors in Echo.HTTPErrorHandler\nvar methodNotAllowedHandler = func(c *Context) error {\n\t// See RFC 7231 section 7.4.1: An origin server MUST generate an Allow field in a 405 (Method Not Allowed)\n\t// response and MAY do so in any other response. For disabled resources an empty Allow header may be returned\n\trouterAllowMethods, ok := c.Get(ContextKeyHeaderAllow).(string)\n\tif ok && routerAllowMethods != \"\" {\n\t\tc.Response().Header().Set(HeaderAllow, routerAllowMethods)\n\t}\n\treturn ErrMethodNotAllowed\n}\n\n// optionsMethodHandler is default handler for OPTIONS method.\n// Use `middleware.CORS` if you need support for preflighted requests in CORS\n// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS\nvar optionsMethodHandler = func(c *Context) error {\n\t// See RFC 7231 section 7.4.1: An origin server MUST generate an Allow field in a 405 (Method Not Allowed)\n\t// response and MAY do so in any other response. For disabled resources an empty Allow header may be returned\n\trouterAllowMethods, ok := c.Get(ContextKeyHeaderAllow).(string)\n\tif ok && routerAllowMethods != \"\" {\n\t\tc.Response().Header().Set(HeaderAllow, routerAllowMethods)\n\t}\n\treturn c.NoContent(http.StatusNoContent)\n}\n\n// Route looks up a handler registered for method and path. It also parses URL for path parameters and loads them\n// into context.\n//\n// For performance:\n//\n// - Get context from `Echo#AcquireContext()`\n// - Reset it `Context#Reset()`\n// - Return it `Echo#ReleaseContext()`.\nfunc (r *DefaultRouter) Route(c *Context) HandlerFunc {\n\tpathValues := c.PathValues()\n\tif cap(pathValues) < r.maxPathParamsLength {\n\t\tpathValues = make(PathValues, 0, r.maxPathParamsLength)\n\t} else {\n\t\tpathValues = pathValues[0:cap(pathValues)] // resize slice to maximum capacity so we can index set values\n\t}\n\n\treq := c.Request()\n\tpath := req.URL.Path\n\tif !r.useEscapedPathForRouting && req.URL.RawPath != \"\" {\n\t\t// Difference between URL.RawPath and URL.Path is:\n\t\t//  * URL.Path is where request path is stored. Value is stored in decoded form: /%47%6f%2f becomes /Go/.\n\t\t//  * URL.RawPath is an optional field which only gets set if the default encoding is different from Path.\n\t\tpath = req.URL.RawPath\n\t}\n\tvar (\n\t\tcurrentNode           = r.tree // root as current node\n\t\tpreviousBestMatchNode *node\n\t\tmatchedRouteMethod    *routeMethod\n\t\t// search stores the remaining path to check for match. By each iteration we move from start of path to end of the path\n\t\t// and search value gets shorter and shorter.\n\t\tsearch      = path\n\t\tsearchIndex = 0\n\t\tparamIndex  int // Param counter\n\t)\n\n\t// Backtracking is needed when a dead end (leaf node) is reached in the router tree.\n\t// To backtrack the current node will be changed to the parent node and the next kind for the\n\t// router logic will be returned based on fromKind or kind of the dead end node (static > param > any).\n\t// For example if there is no static node match we should check parent next sibling by kind (param).\n\t// Backtracking itself does not check if there is a next sibling, this is done by the router logic.\n\tbacktrackToNextNodeKind := func(fromKind kind) (nextNodeKind kind, valid bool) {\n\t\tprevious := currentNode\n\t\tcurrentNode = previous.parent\n\t\tvalid = currentNode != nil\n\n\t\t// Next node type by priority\n\t\tif previous.kind == anyKind {\n\t\t\tnextNodeKind = staticKind\n\t\t} else {\n\t\t\tnextNodeKind = previous.kind + 1\n\t\t}\n\n\t\tif fromKind == staticKind {\n\t\t\t// when backtracking is done from static kind block we did not change search so nothing to restore\n\t\t\treturn\n\t\t}\n\n\t\t// restore search to value it was before we move to current node we are backtracking from.\n\t\tif previous.kind == staticKind {\n\t\t\tsearchIndex -= len(previous.prefix)\n\t\t} else {\n\t\t\tparamIndex--\n\t\t\t// for param/any node.prefix value is always `:` so we can not deduce searchIndex from that and must use pValue\n\t\t\t// for that index as it would also contain part of path we cut off before moving into node we are backtracking from\n\t\t\tsearchIndex -= len(pathValues[paramIndex].Value)\n\t\t\tpathValues[paramIndex].Value = \"\"\n\t\t}\n\t\tsearch = path[searchIndex:]\n\t\treturn\n\t}\n\n\t// Router tree is implemented by longest common prefix array (LCP array) https://en.wikipedia.org/wiki/LCP_array\n\t// Tree search is implemented as for loop where one loop iteration is divided into 3 separate blocks\n\t// Each of these blocks checks specific kind of node (static/param/any). Order of blocks reflex their priority in routing.\n\t// Search order/priority is: static > param > any.\n\t//\n\t// Note: backtracking in tree is implemented by replacing/switching currentNode to previous node\n\t// and hoping to (goto statement) next block by priority to check if it is the match.\n\tfor {\n\t\tprefixLen := 0 // Prefix length\n\t\tlcpLen := 0    // LCP (longest common prefix) length\n\n\t\tif currentNode.kind == staticKind {\n\t\t\tsearchLen := len(search)\n\t\t\tprefixLen = len(currentNode.prefix)\n\n\t\t\t// LCP - Longest Common Prefix (https://en.wikipedia.org/wiki/LCP_array)\n\t\t\tlMax := prefixLen\n\t\t\tif searchLen < lMax {\n\t\t\t\tlMax = searchLen\n\t\t\t}\n\t\t\tfor ; lcpLen < lMax && search[lcpLen] == currentNode.prefix[lcpLen]; lcpLen++ {\n\t\t\t}\n\t\t}\n\n\t\tif lcpLen != prefixLen {\n\t\t\t// No matching prefix, let's backtrack to the first possible alternative node of the decision path\n\t\t\tnk, ok := backtrackToNextNodeKind(staticKind)\n\t\t\tif !ok {\n\t\t\t\tbreak // No other possibilities on the decision path, handler will be whatever context is reset to.\n\t\t\t} else if nk == paramKind {\n\t\t\t\tgoto Param\n\t\t\t\t// NOTE: this case (backtracking from static node to previous any node) can not happen by current any matching logic. Any node is end of search currently\n\t\t\t\t//} else if nk == anyKind {\n\t\t\t\t//\tgoto Any\n\t\t\t} else {\n\t\t\t\t// Not found (this should never be possible for static node we are looking currently)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\t// The full prefix has matched, remove the prefix from the remaining search\n\t\tsearch = search[lcpLen:]\n\t\tsearchIndex = searchIndex + lcpLen\n\n\t\t// Finish routing if is no request path remaining to search\n\t\tif search == \"\" {\n\t\t\t// in case of node that is handler we have exact method type match or something for 405 to use\n\t\t\tif currentNode.isHandler {\n\t\t\t\t// check if current node has handler registered for http method we are looking for. we store currentNode as\n\t\t\t\t// best matching in case we do no find no more routes matching this path+method\n\t\t\t\tif previousBestMatchNode == nil {\n\t\t\t\t\tpreviousBestMatchNode = currentNode\n\t\t\t\t}\n\t\t\t\tif h := currentNode.methods.find(req.Method, true); h != nil {\n\t\t\t\t\tmatchedRouteMethod = h\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t} else if currentNode.methods.notFoundHandler != nil {\n\t\t\t\tmatchedRouteMethod = currentNode.methods.notFoundHandler\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\t// Static node\n\t\tif search != \"\" {\n\t\t\tif child := currentNode.findStaticChild(search[0]); child != nil {\n\t\t\t\tcurrentNode = child\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\tParam:\n\t\t// Param node\n\t\tif child := currentNode.paramChild; search != \"\" && child != nil {\n\t\t\tcurrentNode = child\n\t\t\ti := 0\n\t\t\tl := len(search)\n\t\t\tif currentNode.isLeaf {\n\t\t\t\t// when param node does not have any children (path param is last piece of route path) then param node should\n\t\t\t\t// act similarly to any node - consider all remaining search as match\n\t\t\t\ti = l\n\t\t\t} else {\n\t\t\t\tfor ; i < l && search[i] != '/'; i++ {\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tpathValues[paramIndex].Value = search[:i]\n\t\t\tparamIndex++\n\t\t\tsearch = search[i:]\n\t\t\tsearchIndex = searchIndex + i\n\t\t\tcontinue\n\t\t}\n\n\tAny:\n\t\t// Any node\n\t\tif child := currentNode.anyChild; child != nil {\n\t\t\t// If any node is found, use remaining path for paramValues\n\t\t\tcurrentNode = child\n\t\t\tpathValues[currentNode.paramsCount-1].Value = search\n\t\t\t// update indexes/search in case we need to backtrack when no handler match is found\n\t\t\tparamIndex++\n\t\t\tsearchIndex += len(search)\n\t\t\tsearch = \"\"\n\n\t\t\tif rMethod := currentNode.methods.find(req.Method, true); rMethod != nil {\n\t\t\t\tmatchedRouteMethod = rMethod\n\t\t\t\tbreak\n\t\t\t}\n\t\t\t// we store currentNode as best matching in case we do not find more routes matching this path+method. Needed for 405\n\t\t\tif previousBestMatchNode == nil {\n\t\t\t\tpreviousBestMatchNode = currentNode\n\t\t\t}\n\t\t\tif currentNode.methods.notFoundHandler != nil {\n\t\t\t\tmatchedRouteMethod = currentNode.methods.notFoundHandler\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\t// Let's backtrack to the first possible alternative node of the decision path\n\t\tnk, ok := backtrackToNextNodeKind(anyKind)\n\t\tif !ok {\n\t\t\tbreak // No other possibilities on the decision path\n\t\t} else if nk == paramKind {\n\t\t\tgoto Param\n\t\t} else if nk == anyKind {\n\t\t\tgoto Any\n\t\t} else {\n\t\t\t// Not found\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif currentNode == nil && previousBestMatchNode == nil {\n\t\tpathValues = pathValues[0:0]\n\n\t\tc.InitializeRoute(notFoundRouteInfo, &pathValues)\n\t\treturn r.notFoundHandler // nothing matched at all with given path\n\t}\n\n\tvar rHandler HandlerFunc\n\tvar rPath string\n\tvar rInfo *RouteInfo\n\tif matchedRouteMethod != nil {\n\t\trHandler = matchedRouteMethod.handler\n\t\trPath = matchedRouteMethod.RouteInfo.Path\n\t\trInfo = matchedRouteMethod.RouteInfo\n\t} else {\n\t\t// use previous match as basis. although we have no matching handler we have path match.\n\t\t// so we can send http.StatusMethodNotAllowed (405) instead of http.StatusNotFound (404)\n\t\tcurrentNode = previousBestMatchNode\n\n\t\trPath = currentNode.originalPath\n\t\trInfo = notFoundRouteInfo\n\t\tif currentNode.methods.notFoundHandler != nil {\n\t\t\tmatchedRouteMethod = currentNode.methods.notFoundHandler\n\n\t\t\trInfo = matchedRouteMethod.RouteInfo\n\t\t\trPath = matchedRouteMethod.Path\n\t\t\trHandler = matchedRouteMethod.handler\n\t\t} else if currentNode.isHandler {\n\t\t\trInfo = methodNotAllowedRouteInfo\n\n\t\t\tc.Set(ContextKeyHeaderAllow, currentNode.methods.allowHeader)\n\t\t\trHandler = r.methodNotAllowedHandler\n\t\t\tif req.Method == http.MethodOptions {\n\t\t\t\trHandler = r.optionsMethodHandler\n\t\t\t}\n\t\t}\n\t}\n\n\tpathValues = pathValues[0:currentNode.paramsCount]\n\tif matchedRouteMethod != nil {\n\t\tfor i, name := range matchedRouteMethod.Parameters {\n\t\t\tpathValues[i].Name = name\n\t\t}\n\t}\n\n\tif r.unescapePathParamValues {\n\t\t// See issue #1531, #1258 - there are cases when path parameter need to be unescaped\n\t\tfor i, p := range pathValues {\n\t\t\ttmpVal, err := url.PathUnescape(p.Value)\n\t\t\tif err == nil { // handle problems by ignoring them.\n\t\t\t\tpathValues[i].Value = tmpVal\n\t\t\t}\n\t\t}\n\t}\n\n\tc.InitializeRoute(rInfo, &pathValues)\n\tc.SetPath(rPath)          // after InitializeRoute so we would not accidentally change `notFoundRouteInfo` or `methodNotAllowedRouteInfo` Path\n\tc.request.Pattern = rPath // help standard library based middlewares. This is a deliberate choice not to call `request.SetPathValue` for params.\n\treturn rHandler\n}\n\n// PathValues is collections of PathValue instances with various helper methods\ntype PathValues []PathValue\n\n// PathValue is tuple pf path parameter name and its value in request path\ntype PathValue struct {\n\tName  string\n\tValue string\n}\n\n// Get returns path parameter value for given name or false.\nfunc (p PathValues) Get(name string) (string, bool) {\n\tfor _, param := range p {\n\t\tif param.Name == name {\n\t\t\treturn param.Value, true\n\t\t}\n\t}\n\treturn \"\", false\n}\n\n// GetOr returns path parameter value for given name or default value if the name does not exist.\nfunc (p PathValues) GetOr(name string, defaultValue string) string {\n\tfor _, param := range p {\n\t\tif param.Name == name {\n\t\t\treturn param.Value\n\t\t}\n\t}\n\treturn defaultValue\n}\n"
  },
  {
    "path": "router_concurrent.go",
    "content": "package echo\n\nimport (\n\t\"sync\"\n)\n\n// NewConcurrentRouter creates concurrency safe Router which routes can be added/removed safely\n// even after http.Server has been started.\nfunc NewConcurrentRouter(r Router) Router {\n\treturn &concurrentRouter{\n\t\tmu:     sync.RWMutex{},\n\t\trouter: r,\n\t}\n}\n\ntype concurrentRouter struct {\n\tmu     sync.RWMutex\n\trouter Router\n}\n\nfunc (r *concurrentRouter) Route(c *Context) HandlerFunc {\n\tr.mu.RLock()\n\tdefer r.mu.RUnlock()\n\n\treturn r.router.Route(c)\n}\n\nfunc (r *concurrentRouter) Routes() Routes {\n\tr.mu.RLock()\n\tdefer r.mu.RUnlock()\n\n\treturn r.router.Routes().Clone()\n}\n\nfunc (r *concurrentRouter) Add(routable Route) (RouteInfo, error) {\n\tr.mu.Lock()\n\tdefer r.mu.Unlock()\n\n\treturn r.router.Add(routable)\n}\n\nfunc (r *concurrentRouter) Remove(method string, path string) error {\n\tr.mu.Lock()\n\tdefer r.mu.Unlock()\n\n\treturn r.router.Remove(method, path)\n}\n"
  },
  {
    "path": "router_concurrent_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestConcurrentRouter_Remove(t *testing.T) {\n\trouter := NewConcurrentRouter(NewRouter(RouterConfig{}))\n\n\t_, err := router.Add(Route{\n\t\tMethod:  http.MethodGet,\n\t\tPath:    \"/initial1\",\n\t\tHandler: handlerFunc,\n\t})\n\tassert.NoError(t, err)\n\tassert.Equal(t, len(router.Routes()), 1)\n\n\terr = router.Remove(http.MethodGet, \"/initial1\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, len(router.Routes()), 0)\n}\n\nfunc TestConcurrentRouter_ConcurrentReads(t *testing.T) {\n\trouter := NewConcurrentRouter(NewRouter(RouterConfig{}))\n\n\ttestPaths := []string{\"/route1\", \"/route2\", \"/route3\", \"/route4\", \"/route5\"}\n\tfor _, path := range testPaths {\n\t\t_, err := router.Add(Route{\n\t\t\tMethod:  http.MethodGet,\n\t\t\tPath:    path,\n\t\t\tHandler: handlerFunc,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n\n\t// Launch 10 goroutines for concurrent reads\n\tvar wg sync.WaitGroup\n\tvar routeCallCount atomic.Int64\n\tvar routesCallCount atomic.Int64\n\n\tnumGoroutines := 10\n\trouteCallsPerGoroutine := 50\n\troutesCallsPerGoroutine := 20\n\n\tfor i := 0; i < numGoroutines; i++ {\n\t\twg.Add(1)\n\t\tgo func(goroutineID int) {\n\t\t\tdefer wg.Done()\n\n\t\t\t// Call Route() 50 times\n\t\t\tfor j := 0; j < routeCallsPerGoroutine; j++ {\n\t\t\t\tpath := testPaths[j%len(testPaths)]\n\t\t\t\treq := httptest.NewRequest(http.MethodGet, path, nil)\n\t\t\t\trec := httptest.NewRecorder()\n\t\t\t\tc := newContext(req, rec, nil)\n\n\t\t\t\thandler := router.Route(c)\n\t\t\t\tif handler != nil {\n\t\t\t\t\trouteCallCount.Add(1)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Call Routes() 20 times\n\t\t\tfor j := 0; j < routesCallsPerGoroutine; j++ {\n\t\t\t\troutes := router.Routes()\n\t\t\t\tif len(routes) == 5 {\n\t\t\t\t\troutesCallCount.Add(1)\n\t\t\t\t}\n\t\t\t}\n\t\t}(i)\n\t}\n\n\twg.Wait()\n\n\t// Verify all operations succeeded\n\texpectedRouteCalls := int64(numGoroutines * routeCallsPerGoroutine)\n\texpectedRoutesCalls := int64(numGoroutines * routesCallsPerGoroutine)\n\n\tassert.Equal(t, expectedRouteCalls, routeCallCount.Load(), \"all Route() calls should succeed\")\n\tassert.Equal(t, expectedRoutesCalls, routesCallCount.Load(), \"all Routes() calls should succeed\")\n}\n\nfunc TestConcurrentRouter_ConcurrentWrites(t *testing.T) {\n\trouter := NewConcurrentRouter(NewRouter(RouterConfig{}))\n\n\t_, _ = router.Add(Route{Method: http.MethodGet, Path: \"/initial1\", Handler: handlerFunc})\n\t_, _ = router.Add(Route{Method: http.MethodGet, Path: \"/initial2\", Handler: handlerFunc})\n\n\t// Launch 5 goroutines, each adds 10 unique routes\n\tvar wg sync.WaitGroup\n\tvar addCount atomic.Int64\n\n\tnumGoroutines := 5\n\taddsPerGoroutine := 10\n\n\tfor i := 0; i < numGoroutines; i++ {\n\t\twg.Add(1)\n\t\tgo func(goroutineID int) {\n\t\t\tdefer wg.Done()\n\n\t\t\tfor j := 0; j < addsPerGoroutine; j++ {\n\t\t\t\tpath := fmt.Sprintf(\"/route-g%d-n%d\", goroutineID, j)\n\t\t\t\t_, err := router.Add(Route{\n\t\t\t\t\tMethod:  http.MethodGet,\n\t\t\t\t\tPath:    path,\n\t\t\t\t\tHandler: handlerFunc,\n\t\t\t\t})\n\t\t\t\tif err == nil {\n\t\t\t\t\taddCount.Add(1)\n\t\t\t\t}\n\t\t\t}\n\t\t}(i)\n\t}\n\n\twg.Wait()\n\n\t// Verify final route count\n\texpectedAdds := int64(numGoroutines * addsPerGoroutine)\n\tassert.Equal(t, expectedAdds, addCount.Load(), \"all Add() calls should succeed\")\n\n\texpectedTotal := 2 + int(expectedAdds) // 2 initial + 50 added\n\tassert.Len(t, router.Routes(), expectedTotal, \"route count mismatch\")\n\n\t// Verify all routes are accessible\n\tallRoutes := router.Routes()\n\tassert.Len(t, allRoutes, expectedTotal)\n}\n\nfunc TestConcurrentRouter_ConcurrentReadWrite(t *testing.T) {\n\trouter := NewConcurrentRouter(NewRouter(RouterConfig{}))\n\n\tinitialPaths := []string{\"/read1\", \"/read2\", \"/read3\"}\n\tfor _, path := range initialPaths {\n\t\t_, err := router.Add(Route{Method: http.MethodGet, Path: path, Handler: handlerFunc})\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n\n\tvar wg sync.WaitGroup\n\tvar routeCallCount atomic.Int64\n\tvar addCallCount atomic.Int64\n\tvar routesCallCount atomic.Int64\n\n\t// Launch 4 reader goroutines: call Route() 100 times each\n\tfor i := 0; i < 4; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tfor j := 0; j < 100; j++ {\n\t\t\t\tpath := initialPaths[j%len(initialPaths)]\n\n\t\t\t\treq := httptest.NewRequest(http.MethodGet, path, nil)\n\t\t\t\trec := httptest.NewRecorder()\n\t\t\t\tc := newContext(req, rec, nil)\n\n\t\t\t\thandler := router.Route(c)\n\t\t\t\tif handler != nil {\n\t\t\t\t\trouteCallCount.Add(1)\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\n\t// Launch 2 writer goroutines: call Add() 20 times each\n\tfor i := 0; i < 2; i++ {\n\t\twg.Add(1)\n\t\tgo func(goroutineID int) {\n\t\t\tdefer wg.Done()\n\t\t\tfor j := 0; j < 20; j++ {\n\t\t\t\tpath := fmt.Sprintf(\"/write-g%d-n%d\", goroutineID, j)\n\t\t\t\t_, err := router.Add(Route{\n\t\t\t\t\tMethod:  http.MethodGet,\n\t\t\t\t\tPath:    path,\n\t\t\t\t\tHandler: handlerFunc,\n\t\t\t\t})\n\t\t\t\tif err == nil {\n\t\t\t\t\taddCallCount.Add(1)\n\t\t\t\t}\n\t\t\t}\n\t\t}(i)\n\t}\n\n\t// Launch 2 inspector goroutines: call Routes() 50 times each\n\tfor i := 0; i < 2; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tfor j := 0; j < 50; j++ {\n\t\t\t\troutes := router.Routes()\n\t\t\t\tif routes != nil {\n\t\t\t\t\troutesCallCount.Add(1)\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\n\twg.Wait()\n\n\t// Verify all operations succeeded\n\tassert.Equal(t, int64(400), routeCallCount.Load(), \"all Route() calls should succeed\")\n\tassert.Equal(t, int64(40), addCallCount.Load(), \"all Add() calls should succeed\")\n\tassert.Equal(t, int64(100), routesCallCount.Load(), \"all Routes() calls should succeed\")\n\n\t// Verify final route count\n\texpectedTotal := 3 + 40 // 3 initial + 40 added\n\tassert.Len(t, router.Routes(), expectedTotal, \"route count mismatch\")\n}\n\n// TestConcurrentRouter_RoutesIterationDuringModification verifies that iterating over\n// Routes() while Add/Remove operations are happening doesn't cause data races.\n// This test specifically validates that Routes() returns a copy, not a reference.\nfunc TestConcurrentRouter_RoutesIterationDuringModification(t *testing.T) {\n\trouter := NewConcurrentRouter(NewRouter(RouterConfig{}))\n\n\t// Add initial routes\n\tfor i := 0; i < 10; i++ {\n\t\t_, err := router.Add(Route{\n\t\t\tMethod:  http.MethodGet,\n\t\t\tPath:    fmt.Sprintf(\"/initial-%d\", i),\n\t\t\tHandler: handlerFunc,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n\n\tvar wg sync.WaitGroup\n\tvar iterationCount atomic.Int64\n\tvar addRemoveCount atomic.Int64\n\n\t// Launch 3 goroutines that iterate over Routes() and access each element\n\tfor i := 0; i < 3; i++ {\n\t\twg.Add(1)\n\t\tgo func(goroutineID int) {\n\t\t\tdefer wg.Done()\n\t\t\tfor j := 0; j < 100; j++ {\n\t\t\t\troutes := router.Routes()\n\t\t\t\t// Actually iterate and access the route data\n\t\t\t\t// This would cause a data race if Routes() returned a direct reference\n\t\t\t\tfor _, route := range routes {\n\t\t\t\t\t_ = route.Method // Read the method\n\t\t\t\t\t_ = route.Path   // Read the path\n\t\t\t\t\t_ = route.Name   // Read the name\n\t\t\t\t\tif len(route.Parameters) > 0 {\n\t\t\t\t\t\t_ = route.Parameters[0] // Read parameters if present\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\titerationCount.Add(1)\n\t\t\t}\n\t\t}(i)\n\t}\n\n\t// Launch 2 goroutines that continuously Add routes\n\tfor i := 0; i < 2; i++ {\n\t\twg.Add(1)\n\t\tgo func(goroutineID int) {\n\t\t\tdefer wg.Done()\n\t\t\tfor j := 0; j < 30; j++ {\n\t\t\t\tpath := fmt.Sprintf(\"/add-g%d-n%d\", goroutineID, j)\n\t\t\t\t_, err := router.Add(Route{\n\t\t\t\t\tMethod:  http.MethodPost,\n\t\t\t\t\tPath:    path,\n\t\t\t\t\tHandler: handlerFunc,\n\t\t\t\t})\n\t\t\t\tif err == nil {\n\t\t\t\t\taddRemoveCount.Add(1)\n\t\t\t\t}\n\t\t\t}\n\t\t}(i)\n\t}\n\n\twg.Wait()\n\n\t// Verify operations completed\n\tassert.Equal(t, int64(300), iterationCount.Load(), \"all iterations should complete\")\n\tassert.Equal(t, int64(60), addRemoveCount.Load(), \"all add operations should succeed\")\n\n\t// Verify final state\n\tfinalRoutes := router.Routes()\n\tassert.Len(t, finalRoutes, 70, \"should have 10 initial + 60 added routes\")\n}\n\n// TestConcurrentRouter_ParametersNoRace verifies that accessing RouteInfo.Parameters\n// while routes are being added concurrently doesn't cause data races.\n// This test validates that Routes() deep-copies RouteInfo, not just the Routes slice.\nfunc TestConcurrentRouter_ParametersNoRace(t *testing.T) {\n\trouter := NewConcurrentRouter(NewRouter(RouterConfig{}))\n\n\t// Add routes with parameters\n\t_, err := router.Add(Route{\n\t\tMethod:  http.MethodGet,\n\t\tPath:    \"/users/:id/:name\",\n\t\tHandler: handlerFunc,\n\t})\n\tassert.NoError(t, err)\n\n\t_, err = router.Add(Route{\n\t\tMethod:  http.MethodPost,\n\t\tPath:    \"/posts/:postId/comments/:commentId\",\n\t\tHandler: handlerFunc,\n\t})\n\tassert.NoError(t, err)\n\n\tvar wg sync.WaitGroup\n\tvar paramsAccessCount atomic.Int64\n\tvar addCount atomic.Int64\n\n\t// Launch 3 goroutines that read Parameters repeatedly\n\tfor i := 0; i < 3; i++ {\n\t\twg.Add(1)\n\t\tgo func(goroutineID int) {\n\t\t\tdefer wg.Done()\n\t\t\tfor j := 0; j < 100; j++ {\n\t\t\t\troutes := router.Routes()\n\t\t\t\t// Actually access the Parameters slice data\n\t\t\t\t// This would cause a data race if Parameters weren't deep-copied\n\t\t\t\tfor _, r := range routes {\n\t\t\t\t\tfor _, p := range r.Parameters {\n\t\t\t\t\t\t_ = len(p)      // Read parameter name length\n\t\t\t\t\t\tif len(p) > 0 { // Read first character\n\t\t\t\t\t\t\t_ = p[0]\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tparamsAccessCount.Add(int64(len(r.Parameters)))\n\t\t\t\t}\n\t\t\t}\n\t\t}(i)\n\t}\n\n\t// Launch 2 goroutines that add routes with parameters concurrently\n\tfor i := 0; i < 2; i++ {\n\t\twg.Add(1)\n\t\tgo func(goroutineID int) {\n\t\t\tdefer wg.Done()\n\t\t\tfor j := 0; j < 20; j++ {\n\t\t\t\tpath := fmt.Sprintf(\"/api/:v%d/resource/:id\", goroutineID*100+j)\n\t\t\t\t_, err := router.Add(Route{\n\t\t\t\t\tMethod:  http.MethodPost,\n\t\t\t\t\tPath:    path,\n\t\t\t\t\tHandler: handlerFunc,\n\t\t\t\t})\n\t\t\t\tif err == nil {\n\t\t\t\t\taddCount.Add(1)\n\t\t\t\t}\n\t\t\t}\n\t\t}(i)\n\t}\n\n\twg.Wait()\n\n\t// Verify operations completed\n\tassert.Equal(t, int64(40), addCount.Load(), \"all add operations should succeed\")\n\tassert.Greater(t, paramsAccessCount.Load(), int64(0), \"should have accessed parameters\")\n\n\t// Verify final state\n\tfinalRoutes := router.Routes()\n\tassert.Len(t, finalRoutes, 42, \"should have 2 initial + 40 added routes\")\n\n\t// Verify we can still safely access Parameters after concurrent operations\n\tfor _, route := range finalRoutes {\n\t\tfor _, param := range route.Parameters {\n\t\t\tassert.NotEmpty(t, param, \"parameter name should not be empty\")\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "router_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype testRoute struct {\n\tMethod  string\n\tPath    string\n\tHandler string\n}\n\nvar (\n\tstaticRoutes = []testRoute{\n\t\t{\"GET\", \"/\", \"\"},\n\t\t{\"GET\", \"/cmd.html\", \"\"},\n\t\t{\"GET\", \"/code.html\", \"\"},\n\t\t{\"GET\", \"/contrib.html\", \"\"},\n\t\t{\"GET\", \"/contribute.html\", \"\"},\n\t\t{\"GET\", \"/debugging_with_gdb.html\", \"\"},\n\t\t{\"GET\", \"/docs.html\", \"\"},\n\t\t{\"GET\", \"/effective_go.html\", \"\"},\n\t\t{\"GET\", \"/files.log\", \"\"},\n\t\t{\"GET\", \"/gccgo_contribute.html\", \"\"},\n\t\t{\"GET\", \"/gccgo_install.html\", \"\"},\n\t\t{\"GET\", \"/go-logo-black.png\", \"\"},\n\t\t{\"GET\", \"/go-logo-blue.png\", \"\"},\n\t\t{\"GET\", \"/go-logo-white.png\", \"\"},\n\t\t{\"GET\", \"/go1.1.html\", \"\"},\n\t\t{\"GET\", \"/go1.2.html\", \"\"},\n\t\t{\"GET\", \"/go1.html\", \"\"},\n\t\t{\"GET\", \"/go1compat.html\", \"\"},\n\t\t{\"GET\", \"/go_faq.html\", \"\"},\n\t\t{\"GET\", \"/go_mem.html\", \"\"},\n\t\t{\"GET\", \"/go_spec.html\", \"\"},\n\t\t{\"GET\", \"/help.html\", \"\"},\n\t\t{\"GET\", \"/ie.css\", \"\"},\n\t\t{\"GET\", \"/install-source.html\", \"\"},\n\t\t{\"GET\", \"/install.html\", \"\"},\n\t\t{\"GET\", \"/logo-153x55.png\", \"\"},\n\t\t{\"GET\", \"/Makefile\", \"\"},\n\t\t{\"GET\", \"/root.html\", \"\"},\n\t\t{\"GET\", \"/share.png\", \"\"},\n\t\t{\"GET\", \"/sieve.gif\", \"\"},\n\t\t{\"GET\", \"/tos.html\", \"\"},\n\t\t{\"GET\", \"/articles/\", \"\"},\n\t\t{\"GET\", \"/articles/go_command.html\", \"\"},\n\t\t{\"GET\", \"/articles/index.html\", \"\"},\n\t\t{\"GET\", \"/articles/wiki/\", \"\"},\n\t\t{\"GET\", \"/articles/wiki/edit.html\", \"\"},\n\t\t{\"GET\", \"/articles/wiki/final-noclosure.go\", \"\"},\n\t\t{\"GET\", \"/articles/wiki/final-noerror.go\", \"\"},\n\t\t{\"GET\", \"/articles/wiki/final-parsetemplate.go\", \"\"},\n\t\t{\"GET\", \"/articles/wiki/final-template.go\", \"\"},\n\t\t{\"GET\", \"/articles/wiki/final.go\", \"\"},\n\t\t{\"GET\", \"/articles/wiki/get.go\", \"\"},\n\t\t{\"GET\", \"/articles/wiki/http-sample.go\", \"\"},\n\t\t{\"GET\", \"/articles/wiki/index.html\", \"\"},\n\t\t{\"GET\", \"/articles/wiki/Makefile\", \"\"},\n\t\t{\"GET\", \"/articles/wiki/notemplate.go\", \"\"},\n\t\t{\"GET\", \"/articles/wiki/part1-noerror.go\", \"\"},\n\t\t{\"GET\", \"/articles/wiki/part1.go\", \"\"},\n\t\t{\"GET\", \"/articles/wiki/part2.go\", \"\"},\n\t\t{\"GET\", \"/articles/wiki/part3-errorhandling.go\", \"\"},\n\t\t{\"GET\", \"/articles/wiki/part3.go\", \"\"},\n\t\t{\"GET\", \"/articles/wiki/test.bash\", \"\"},\n\t\t{\"GET\", \"/articles/wiki/test_edit.good\", \"\"},\n\t\t{\"GET\", \"/articles/wiki/test_Test.txt.good\", \"\"},\n\t\t{\"GET\", \"/articles/wiki/test_view.good\", \"\"},\n\t\t{\"GET\", \"/articles/wiki/view.html\", \"\"},\n\t\t{\"GET\", \"/codewalk/\", \"\"},\n\t\t{\"GET\", \"/codewalk/codewalk.css\", \"\"},\n\t\t{\"GET\", \"/codewalk/codewalk.js\", \"\"},\n\t\t{\"GET\", \"/codewalk/codewalk.xml\", \"\"},\n\t\t{\"GET\", \"/codewalk/functions.xml\", \"\"},\n\t\t{\"GET\", \"/codewalk/markov.go\", \"\"},\n\t\t{\"GET\", \"/codewalk/markov.xml\", \"\"},\n\t\t{\"GET\", \"/codewalk/pig.go\", \"\"},\n\t\t{\"GET\", \"/codewalk/popout.png\", \"\"},\n\t\t{\"GET\", \"/codewalk/run\", \"\"},\n\t\t{\"GET\", \"/codewalk/sharemem.xml\", \"\"},\n\t\t{\"GET\", \"/codewalk/urlpoll.go\", \"\"},\n\t\t{\"GET\", \"/devel/\", \"\"},\n\t\t{\"GET\", \"/devel/release.html\", \"\"},\n\t\t{\"GET\", \"/devel/weekly.html\", \"\"},\n\t\t{\"GET\", \"/gopher/\", \"\"},\n\t\t{\"GET\", \"/gopher/appenginegopher.jpg\", \"\"},\n\t\t{\"GET\", \"/gopher/appenginegophercolor.jpg\", \"\"},\n\t\t{\"GET\", \"/gopher/appenginelogo.gif\", \"\"},\n\t\t{\"GET\", \"/gopher/bumper.png\", \"\"},\n\t\t{\"GET\", \"/gopher/bumper192x108.png\", \"\"},\n\t\t{\"GET\", \"/gopher/bumper320x180.png\", \"\"},\n\t\t{\"GET\", \"/gopher/bumper480x270.png\", \"\"},\n\t\t{\"GET\", \"/gopher/bumper640x360.png\", \"\"},\n\t\t{\"GET\", \"/gopher/doc.png\", \"\"},\n\t\t{\"GET\", \"/gopher/frontpage.png\", \"\"},\n\t\t{\"GET\", \"/gopher/gopherbw.png\", \"\"},\n\t\t{\"GET\", \"/gopher/gophercolor.png\", \"\"},\n\t\t{\"GET\", \"/gopher/gophercolor16x16.png\", \"\"},\n\t\t{\"GET\", \"/gopher/help.png\", \"\"},\n\t\t{\"GET\", \"/gopher/pkg.png\", \"\"},\n\t\t{\"GET\", \"/gopher/project.png\", \"\"},\n\t\t{\"GET\", \"/gopher/ref.png\", \"\"},\n\t\t{\"GET\", \"/gopher/run.png\", \"\"},\n\t\t{\"GET\", \"/gopher/talks.png\", \"\"},\n\t\t{\"GET\", \"/gopher/pencil/\", \"\"},\n\t\t{\"GET\", \"/gopher/pencil/gopherhat.jpg\", \"\"},\n\t\t{\"GET\", \"/gopher/pencil/gopherhelmet.jpg\", \"\"},\n\t\t{\"GET\", \"/gopher/pencil/gophermega.jpg\", \"\"},\n\t\t{\"GET\", \"/gopher/pencil/gopherrunning.jpg\", \"\"},\n\t\t{\"GET\", \"/gopher/pencil/gopherswim.jpg\", \"\"},\n\t\t{\"GET\", \"/gopher/pencil/gopherswrench.jpg\", \"\"},\n\t\t{\"GET\", \"/play/\", \"\"},\n\t\t{\"GET\", \"/play/fib.go\", \"\"},\n\t\t{\"GET\", \"/play/hello.go\", \"\"},\n\t\t{\"GET\", \"/play/life.go\", \"\"},\n\t\t{\"GET\", \"/play/peano.go\", \"\"},\n\t\t{\"GET\", \"/play/pi.go\", \"\"},\n\t\t{\"GET\", \"/play/sieve.go\", \"\"},\n\t\t{\"GET\", \"/play/solitaire.go\", \"\"},\n\t\t{\"GET\", \"/play/tree.go\", \"\"},\n\t\t{\"GET\", \"/progs/\", \"\"},\n\t\t{\"GET\", \"/progs/cgo1.go\", \"\"},\n\t\t{\"GET\", \"/progs/cgo2.go\", \"\"},\n\t\t{\"GET\", \"/progs/cgo3.go\", \"\"},\n\t\t{\"GET\", \"/progs/cgo4.go\", \"\"},\n\t\t{\"GET\", \"/progs/defer.go\", \"\"},\n\t\t{\"GET\", \"/progs/defer.out\", \"\"},\n\t\t{\"GET\", \"/progs/defer2.go\", \"\"},\n\t\t{\"GET\", \"/progs/defer2.out\", \"\"},\n\t\t{\"GET\", \"/progs/eff_bytesize.go\", \"\"},\n\t\t{\"GET\", \"/progs/eff_bytesize.out\", \"\"},\n\t\t{\"GET\", \"/progs/eff_qr.go\", \"\"},\n\t\t{\"GET\", \"/progs/eff_sequence.go\", \"\"},\n\t\t{\"GET\", \"/progs/eff_sequence.out\", \"\"},\n\t\t{\"GET\", \"/progs/eff_unused1.go\", \"\"},\n\t\t{\"GET\", \"/progs/eff_unused2.go\", \"\"},\n\t\t{\"GET\", \"/progs/error.go\", \"\"},\n\t\t{\"GET\", \"/progs/error2.go\", \"\"},\n\t\t{\"GET\", \"/progs/error3.go\", \"\"},\n\t\t{\"GET\", \"/progs/error4.go\", \"\"},\n\t\t{\"GET\", \"/progs/go1.go\", \"\"},\n\t\t{\"GET\", \"/progs/gobs1.go\", \"\"},\n\t\t{\"GET\", \"/progs/gobs2.go\", \"\"},\n\t\t{\"GET\", \"/progs/image_draw.go\", \"\"},\n\t\t{\"GET\", \"/progs/image_package1.go\", \"\"},\n\t\t{\"GET\", \"/progs/image_package1.out\", \"\"},\n\t\t{\"GET\", \"/progs/image_package2.go\", \"\"},\n\t\t{\"GET\", \"/progs/image_package2.out\", \"\"},\n\t\t{\"GET\", \"/progs/image_package3.go\", \"\"},\n\t\t{\"GET\", \"/progs/image_package3.out\", \"\"},\n\t\t{\"GET\", \"/progs/image_package4.go\", \"\"},\n\t\t{\"GET\", \"/progs/image_package4.out\", \"\"},\n\t\t{\"GET\", \"/progs/image_package5.go\", \"\"},\n\t\t{\"GET\", \"/progs/image_package5.out\", \"\"},\n\t\t{\"GET\", \"/progs/image_package6.go\", \"\"},\n\t\t{\"GET\", \"/progs/image_package6.out\", \"\"},\n\t\t{\"GET\", \"/progs/interface.go\", \"\"},\n\t\t{\"GET\", \"/progs/interface2.go\", \"\"},\n\t\t{\"GET\", \"/progs/interface2.out\", \"\"},\n\t\t{\"GET\", \"/progs/json1.go\", \"\"},\n\t\t{\"GET\", \"/progs/json2.go\", \"\"},\n\t\t{\"GET\", \"/progs/json2.out\", \"\"},\n\t\t{\"GET\", \"/progs/json3.go\", \"\"},\n\t\t{\"GET\", \"/progs/json4.go\", \"\"},\n\t\t{\"GET\", \"/progs/json5.go\", \"\"},\n\t\t{\"GET\", \"/progs/run\", \"\"},\n\t\t{\"GET\", \"/progs/slices.go\", \"\"},\n\t\t{\"GET\", \"/progs/timeout1.go\", \"\"},\n\t\t{\"GET\", \"/progs/timeout2.go\", \"\"},\n\t\t{\"GET\", \"/progs/update.bash\", \"\"},\n\t}\n\n\tgitHubAPI = []testRoute{\n\t\t// OAuth Authorizations\n\t\t{\"GET\", \"/authorizations\", \"\"},\n\t\t{\"GET\", \"/authorizations/:id\", \"\"},\n\t\t{\"POST\", \"/authorizations\", \"\"},\n\n\t\t{\"PUT\", \"/authorizations/clients/:client_id\", \"\"},\n\t\t{\"PATCH\", \"/authorizations/:id\", \"\"},\n\n\t\t{\"DELETE\", \"/authorizations/:id\", \"\"},\n\t\t{\"GET\", \"/applications/:client_id/tokens/:access_token\", \"\"},\n\t\t{\"DELETE\", \"/applications/:client_id/tokens\", \"\"},\n\t\t{\"DELETE\", \"/applications/:client_id/tokens/:access_token\", \"\"},\n\n\t\t// Activity\n\t\t{\"GET\", \"/events\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/events\", \"\"},\n\t\t{\"GET\", \"/networks/:owner/:repo/events\", \"\"},\n\t\t{\"GET\", \"/orgs/:org/events\", \"\"},\n\t\t{\"GET\", \"/users/:user/received_events\", \"\"},\n\t\t{\"GET\", \"/users/:user/received_events/public\", \"\"},\n\t\t{\"GET\", \"/users/:user/events\", \"\"},\n\t\t{\"GET\", \"/users/:user/events/public\", \"\"},\n\t\t{\"GET\", \"/users/:user/events/orgs/:org\", \"\"},\n\t\t{\"GET\", \"/feeds\", \"\"},\n\t\t{\"GET\", \"/notifications\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/notifications\", \"\"},\n\t\t{\"PUT\", \"/notifications\", \"\"},\n\t\t{\"PUT\", \"/repos/:owner/:repo/notifications\", \"\"},\n\t\t{\"GET\", \"/notifications/threads/:id\", \"\"},\n\n\t\t{\"PATCH\", \"/notifications/threads/:id\", \"\"},\n\n\t\t{\"GET\", \"/notifications/threads/:id/subscription\", \"\"},\n\t\t{\"PUT\", \"/notifications/threads/:id/subscription\", \"\"},\n\t\t{\"DELETE\", \"/notifications/threads/:id/subscription\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/stargazers\", \"\"},\n\t\t{\"GET\", \"/users/:user/starred\", \"\"},\n\t\t{\"GET\", \"/user/starred\", \"\"},\n\t\t{\"GET\", \"/user/starred/:owner/:repo\", \"\"},\n\t\t{\"PUT\", \"/user/starred/:owner/:repo\", \"\"},\n\t\t{\"DELETE\", \"/user/starred/:owner/:repo\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/subscribers\", \"\"},\n\t\t{\"GET\", \"/users/:user/subscriptions\", \"\"},\n\t\t{\"GET\", \"/user/subscriptions\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/subscription\", \"\"},\n\t\t{\"PUT\", \"/repos/:owner/:repo/subscription\", \"\"},\n\t\t{\"DELETE\", \"/repos/:owner/:repo/subscription\", \"\"},\n\t\t{\"GET\", \"/user/subscriptions/:owner/:repo\", \"\"},\n\t\t{\"PUT\", \"/user/subscriptions/:owner/:repo\", \"\"},\n\t\t{\"DELETE\", \"/user/subscriptions/:owner/:repo\", \"\"},\n\n\t\t// Gists\n\t\t{\"GET\", \"/users/:user/gists\", \"\"},\n\t\t{\"GET\", \"/gists\", \"\"},\n\n\t\t{\"GET\", \"/gists/public\", \"\"},\n\t\t{\"GET\", \"/gists/starred\", \"\"},\n\n\t\t{\"GET\", \"/gists/:id\", \"\"},\n\t\t{\"POST\", \"/gists\", \"\"},\n\n\t\t{\"PATCH\", \"/gists/:id\", \"\"},\n\n\t\t{\"PUT\", \"/gists/:id/star\", \"\"},\n\t\t{\"DELETE\", \"/gists/:id/star\", \"\"},\n\t\t{\"GET\", \"/gists/:id/star\", \"\"},\n\t\t{\"POST\", \"/gists/:id/forks\", \"\"},\n\t\t{\"DELETE\", \"/gists/:id\", \"\"},\n\n\t\t// Git Data\n\t\t{\"GET\", \"/repos/:owner/:repo/git/blobs/:sha\", \"\"},\n\t\t{\"POST\", \"/repos/:owner/:repo/git/blobs\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/git/commits/:sha\", \"\"},\n\t\t{\"POST\", \"/repos/:owner/:repo/git/commits\", \"\"},\n\n\t\t{\"GET\", \"/repos/:owner/:repo/git/refs/*ref\", \"\"},\n\n\t\t{\"GET\", \"/repos/:owner/:repo/git/refs\", \"\"},\n\t\t{\"POST\", \"/repos/:owner/:repo/git/refs\", \"\"},\n\n\t\t{\"PATCH\", \"/repos/:owner/:repo/git/refs/*ref\", \"\"},\n\t\t{\"DELETE\", \"/repos/:owner/:repo/git/refs/*ref\", \"\"},\n\n\t\t{\"GET\", \"/repos/:owner/:repo/git/tags/:sha\", \"\"},\n\t\t{\"POST\", \"/repos/:owner/:repo/git/tags\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/git/trees/:sha\", \"\"},\n\t\t{\"POST\", \"/repos/:owner/:repo/git/trees\", \"\"},\n\n\t\t// Issues\n\t\t{\"GET\", \"/issues\", \"\"},\n\t\t{\"GET\", \"/user/issues\", \"\"},\n\t\t{\"GET\", \"/orgs/:org/issues\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/issues\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/issues/:number\", \"\"},\n\t\t{\"POST\", \"/repos/:owner/:repo/issues\", \"\"},\n\n\t\t{\"PATCH\", \"/repos/:owner/:repo/issues/:number\", \"\"},\n\n\t\t{\"GET\", \"/repos/:owner/:repo/assignees\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/assignees/:assignee\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/issues/:number/comments\", \"\"},\n\n\t\t{\"GET\", \"/repos/:owner/:repo/issues/comments\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/issues/comments/:id\", \"\"},\n\n\t\t{\"POST\", \"/repos/:owner/:repo/issues/:number/comments\", \"\"},\n\n\t\t{\"PATCH\", \"/repos/:owner/:repo/issues/comments/:id\", \"\"},\n\t\t{\"DELETE\", \"/repos/:owner/:repo/issues/comments/:id\", \"\"},\n\n\t\t{\"GET\", \"/repos/:owner/:repo/issues/:number/events\", \"\"},\n\n\t\t{\"GET\", \"/repos/:owner/:repo/issues/events\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/issues/events/:id\", \"\"},\n\n\t\t{\"GET\", \"/repos/:owner/:repo/labels\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/labels/:name\", \"\"},\n\t\t{\"POST\", \"/repos/:owner/:repo/labels\", \"\"},\n\n\t\t{\"PATCH\", \"/repos/:owner/:repo/labels/:name\", \"\"},\n\n\t\t{\"DELETE\", \"/repos/:owner/:repo/labels/:name\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/issues/:number/labels\", \"\"},\n\t\t{\"POST\", \"/repos/:owner/:repo/issues/:number/labels\", \"\"},\n\t\t{\"DELETE\", \"/repos/:owner/:repo/issues/:number/labels/:name\", \"\"},\n\t\t{\"PUT\", \"/repos/:owner/:repo/issues/:number/labels\", \"\"},\n\t\t{\"DELETE\", \"/repos/:owner/:repo/issues/:number/labels\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/milestones/:number/labels\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/milestones\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/milestones/:number\", \"\"},\n\t\t{\"POST\", \"/repos/:owner/:repo/milestones\", \"\"},\n\n\t\t{\"PATCH\", \"/repos/:owner/:repo/milestones/:number\", \"\"},\n\n\t\t{\"DELETE\", \"/repos/:owner/:repo/milestones/:number\", \"\"},\n\n\t\t// Miscellaneous\n\t\t{\"GET\", \"/emojis\", \"\"},\n\t\t{\"GET\", \"/gitignore/templates\", \"\"},\n\t\t{\"GET\", \"/gitignore/templates/:name\", \"\"},\n\t\t{\"POST\", \"/markdown\", \"\"},\n\t\t{\"POST\", \"/markdown/raw\", \"\"},\n\t\t{\"GET\", \"/meta\", \"\"},\n\t\t{\"GET\", \"/rate_limit\", \"\"},\n\n\t\t// Organizations\n\t\t{\"GET\", \"/users/:user/orgs\", \"\"},\n\t\t{\"GET\", \"/user/orgs\", \"\"},\n\t\t{\"GET\", \"/orgs/:org\", \"\"},\n\n\t\t{\"PATCH\", \"/orgs/:org\", \"\"},\n\n\t\t{\"GET\", \"/orgs/:org/members\", \"\"},\n\t\t{\"GET\", \"/orgs/:org/members/:user\", \"\"},\n\t\t{\"DELETE\", \"/orgs/:org/members/:user\", \"\"},\n\t\t{\"GET\", \"/orgs/:org/public_members\", \"\"},\n\t\t{\"GET\", \"/orgs/:org/public_members/:user\", \"\"},\n\t\t{\"PUT\", \"/orgs/:org/public_members/:user\", \"\"},\n\t\t{\"DELETE\", \"/orgs/:org/public_members/:user\", \"\"},\n\t\t{\"GET\", \"/orgs/:org/teams\", \"\"},\n\t\t{\"GET\", \"/teams/:id\", \"\"},\n\t\t{\"POST\", \"/orgs/:org/teams\", \"\"},\n\n\t\t{\"PATCH\", \"/teams/:id\", \"\"},\n\n\t\t{\"DELETE\", \"/teams/:id\", \"\"},\n\t\t{\"GET\", \"/teams/:id/members\", \"\"},\n\t\t{\"GET\", \"/teams/:id/members/:user\", \"\"},\n\t\t{\"PUT\", \"/teams/:id/members/:user\", \"\"},\n\t\t{\"DELETE\", \"/teams/:id/members/:user\", \"\"},\n\t\t{\"GET\", \"/teams/:id/repos\", \"\"},\n\t\t{\"GET\", \"/teams/:id/repos/:owner/:repo\", \"\"},\n\t\t{\"PUT\", \"/teams/:id/repos/:owner/:repo\", \"\"},\n\t\t{\"DELETE\", \"/teams/:id/repos/:owner/:repo\", \"\"},\n\t\t{\"GET\", \"/user/teams\", \"\"},\n\n\t\t// Pull Requests\n\t\t{\"GET\", \"/repos/:owner/:repo/pulls\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/pulls/:number\", \"\"},\n\t\t{\"POST\", \"/repos/:owner/:repo/pulls\", \"\"},\n\n\t\t{\"PATCH\", \"/repos/:owner/:repo/pulls/:number\", \"\"},\n\n\t\t{\"GET\", \"/repos/:owner/:repo/pulls/:number/commits\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/pulls/:number/files\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/pulls/:number/merge\", \"\"},\n\t\t{\"PUT\", \"/repos/:owner/:repo/pulls/:number/merge\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/pulls/:number/comments\", \"\"},\n\n\t\t{\"GET\", \"/repos/:owner/:repo/pulls/comments\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/pulls/comments/:number\", \"\"},\n\n\t\t{\"PUT\", \"/repos/:owner/:repo/pulls/:number/comments\", \"\"},\n\n\t\t{\"PATCH\", \"/repos/:owner/:repo/pulls/comments/:number\", \"\"},\n\t\t{\"DELETE\", \"/repos/:owner/:repo/pulls/comments/:number\", \"\"},\n\n\t\t// Repositories\n\t\t{\"GET\", \"/user/repos\", \"\"},\n\t\t{\"GET\", \"/users/:user/repos\", \"\"},\n\t\t{\"GET\", \"/orgs/:org/repos\", \"\"},\n\t\t{\"GET\", \"/repositories\", \"\"},\n\t\t{\"POST\", \"/user/repos\", \"\"},\n\t\t{\"POST\", \"/orgs/:org/repos\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo\", \"\"},\n\n\t\t{\"PATCH\", \"/repos/:owner/:repo\", \"\"},\n\n\t\t{\"GET\", \"/repos/:owner/:repo/contributors\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/languages\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/teams\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/tags\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/branches\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/branches/:branch\", \"\"},\n\t\t{\"DELETE\", \"/repos/:owner/:repo\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/collaborators\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/collaborators/:user\", \"\"},\n\t\t{\"PUT\", \"/repos/:owner/:repo/collaborators/:user\", \"\"},\n\t\t{\"DELETE\", \"/repos/:owner/:repo/collaborators/:user\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/comments\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/commits/:sha/comments\", \"\"},\n\t\t{\"POST\", \"/repos/:owner/:repo/commits/:sha/comments\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/comments/:id\", \"\"},\n\n\t\t{\"PATCH\", \"/repos/:owner/:repo/comments/:id\", \"\"},\n\n\t\t{\"DELETE\", \"/repos/:owner/:repo/comments/:id\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/commits\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/commits/:sha\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/readme\", \"\"},\n\n\t\t//{\"GET\", \"/repos/:owner/:repo/contents/*path\", \"\"},\n\t\t//{\"PUT\", \"/repos/:owner/:repo/contents/*path\", \"\"},\n\t\t//{\"DELETE\", \"/repos/:owner/:repo/contents/*path\", \"\"},\n\n\t\t{\"GET\", \"/repos/:owner/:repo/:archive_format/:ref\", \"\"},\n\n\t\t{\"GET\", \"/repos/:owner/:repo/keys\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/keys/:id\", \"\"},\n\t\t{\"POST\", \"/repos/:owner/:repo/keys\", \"\"},\n\n\t\t{\"PATCH\", \"/repos/:owner/:repo/keys/:id\", \"\"},\n\n\t\t{\"DELETE\", \"/repos/:owner/:repo/keys/:id\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/downloads\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/downloads/:id\", \"\"},\n\t\t{\"DELETE\", \"/repos/:owner/:repo/downloads/:id\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/forks\", \"\"},\n\t\t{\"POST\", \"/repos/:owner/:repo/forks\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/hooks\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/hooks/:id\", \"\"},\n\t\t{\"POST\", \"/repos/:owner/:repo/hooks\", \"\"},\n\n\t\t{\"PATCH\", \"/repos/:owner/:repo/hooks/:id\", \"\"},\n\n\t\t{\"POST\", \"/repos/:owner/:repo/hooks/:id/tests\", \"\"},\n\t\t{\"DELETE\", \"/repos/:owner/:repo/hooks/:id\", \"\"},\n\t\t{\"POST\", \"/repos/:owner/:repo/merges\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/releases\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/releases/:id\", \"\"},\n\t\t{\"POST\", \"/repos/:owner/:repo/releases\", \"\"},\n\n\t\t{\"PATCH\", \"/repos/:owner/:repo/releases/:id\", \"\"},\n\n\t\t{\"DELETE\", \"/repos/:owner/:repo/releases/:id\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/releases/:id/assets\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/stats/contributors\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/stats/commit_activity\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/stats/code_frequency\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/stats/participation\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/stats/punch_card\", \"\"},\n\t\t{\"GET\", \"/repos/:owner/:repo/statuses/:ref\", \"\"},\n\t\t{\"POST\", \"/repos/:owner/:repo/statuses/:ref\", \"\"},\n\n\t\t// Search\n\t\t{\"GET\", \"/search/repositories\", \"\"},\n\t\t{\"GET\", \"/search/code\", \"\"},\n\t\t{\"GET\", \"/search/issues\", \"\"},\n\t\t{\"GET\", \"/search/users\", \"\"},\n\t\t{\"GET\", \"/legacy/issues/search/:owner/:repository/:state/:keyword\", \"\"},\n\t\t{\"GET\", \"/legacy/repos/search/:keyword\", \"\"},\n\t\t{\"GET\", \"/legacy/user/search/:keyword\", \"\"},\n\t\t{\"GET\", \"/legacy/user/email/:email\", \"\"},\n\n\t\t// Users\n\t\t{\"GET\", \"/users/:user\", \"\"},\n\t\t{\"GET\", \"/user\", \"\"},\n\n\t\t{\"PATCH\", \"/user\", \"\"},\n\n\t\t{\"GET\", \"/users\", \"\"},\n\t\t{\"GET\", \"/user/emails\", \"\"},\n\t\t{\"POST\", \"/user/emails\", \"\"},\n\t\t{\"DELETE\", \"/user/emails\", \"\"},\n\t\t{\"GET\", \"/users/:user/followers\", \"\"},\n\t\t{\"GET\", \"/user/followers\", \"\"},\n\t\t{\"GET\", \"/users/:user/following\", \"\"},\n\t\t{\"GET\", \"/user/following\", \"\"},\n\t\t{\"GET\", \"/user/following/:user\", \"\"},\n\t\t{\"GET\", \"/users/:user/following/:target_user\", \"\"},\n\t\t{\"PUT\", \"/user/following/:user\", \"\"},\n\t\t{\"DELETE\", \"/user/following/:user\", \"\"},\n\t\t{\"GET\", \"/users/:user/keys\", \"\"},\n\t\t{\"GET\", \"/user/keys\", \"\"},\n\t\t{\"GET\", \"/user/keys/:id\", \"\"},\n\t\t{\"POST\", \"/user/keys\", \"\"},\n\n\t\t{\"PATCH\", \"/user/keys/:id\", \"\"},\n\n\t\t{\"DELETE\", \"/user/keys/:id\", \"\"},\n\t}\n\n\tparseAPI = []testRoute{\n\t\t// Objects\n\t\t{\"POST\", \"/1/classes/:className\", \"\"},\n\t\t{\"GET\", \"/1/classes/:className/:objectId\", \"\"},\n\t\t{\"PUT\", \"/1/classes/:className/:objectId\", \"\"},\n\t\t{\"GET\", \"/1/classes/:className\", \"\"},\n\t\t{\"DELETE\", \"/1/classes/:className/:objectId\", \"\"},\n\n\t\t// Users\n\t\t{\"POST\", \"/1/users\", \"\"},\n\t\t{\"GET\", \"/1/login\", \"\"},\n\t\t{\"GET\", \"/1/users/:objectId\", \"\"},\n\t\t{\"PUT\", \"/1/users/:objectId\", \"\"},\n\t\t{\"GET\", \"/1/users\", \"\"},\n\t\t{\"DELETE\", \"/1/users/:objectId\", \"\"},\n\t\t{\"POST\", \"/1/requestPasswordReset\", \"\"},\n\n\t\t// Roles\n\t\t{\"POST\", \"/1/roles\", \"\"},\n\t\t{\"GET\", \"/1/roles/:objectId\", \"\"},\n\t\t{\"PUT\", \"/1/roles/:objectId\", \"\"},\n\t\t{\"GET\", \"/1/roles\", \"\"},\n\t\t{\"DELETE\", \"/1/roles/:objectId\", \"\"},\n\n\t\t// Files\n\t\t{\"POST\", \"/1/files/:fileName\", \"\"},\n\n\t\t// Analytics\n\t\t{\"POST\", \"/1/events/:eventName\", \"\"},\n\n\t\t// Push Notifications\n\t\t{\"POST\", \"/1/push\", \"\"},\n\n\t\t// Installations\n\t\t{\"POST\", \"/1/installations\", \"\"},\n\t\t{\"GET\", \"/1/installations/:objectId\", \"\"},\n\t\t{\"PUT\", \"/1/installations/:objectId\", \"\"},\n\t\t{\"GET\", \"/1/installations\", \"\"},\n\t\t{\"DELETE\", \"/1/installations/:objectId\", \"\"},\n\n\t\t// Cloud Functions\n\t\t{\"POST\", \"/1/functions\", \"\"},\n\t}\n\n\tgooglePlusAPI = []testRoute{\n\t\t// People\n\t\t{\"GET\", \"/people/:userId\", \"\"},\n\t\t{\"GET\", \"/people\", \"\"},\n\t\t{\"GET\", \"/activities/:activityId/people/:collection\", \"\"},\n\t\t{\"GET\", \"/people/:userId/people/:collection\", \"\"},\n\t\t{\"GET\", \"/people/:userId/openIdConnect\", \"\"},\n\n\t\t// Activities\n\t\t{\"GET\", \"/people/:userId/activities/:collection\", \"\"},\n\t\t{\"GET\", \"/activities/:activityId\", \"\"},\n\t\t{\"GET\", \"/activities\", \"\"},\n\n\t\t// Comments\n\t\t{\"GET\", \"/activities/:activityId/comments\", \"\"},\n\t\t{\"GET\", \"/comments/:commentId\", \"\"},\n\n\t\t// Moments\n\t\t{\"POST\", \"/people/:userId/moments/:collection\", \"\"},\n\t\t{\"GET\", \"/people/:userId/moments/:collection\", \"\"},\n\t\t{\"DELETE\", \"/moments/:id\", \"\"},\n\t}\n\n\tparamAndAnyAPI = []testRoute{\n\t\t{\"GET\", \"/root/:first/foo/*\", \"\"},\n\t\t{\"GET\", \"/root/:first/:second/*\", \"\"},\n\t\t{\"GET\", \"/root/:first/bar/:second/*\", \"\"},\n\t\t{\"GET\", \"/root/:first/qux/:second/:third/:fourth\", \"\"},\n\t\t{\"GET\", \"/root/:first/qux/:second/:third/:fourth/*\", \"\"},\n\t\t{\"GET\", \"/root/*\", \"\"},\n\n\t\t{\"POST\", \"/root/:first/foo/*\", \"\"},\n\t\t{\"POST\", \"/root/:first/:second/*\", \"\"},\n\t\t{\"POST\", \"/root/:first/bar/:second/*\", \"\"},\n\t\t{\"POST\", \"/root/:first/qux/:second/:third/:fourth\", \"\"},\n\t\t{\"POST\", \"/root/:first/qux/:second/:third/:fourth/*\", \"\"},\n\t\t{\"POST\", \"/root/*\", \"\"},\n\n\t\t{\"PUT\", \"/root/:first/foo/*\", \"\"},\n\t\t{\"PUT\", \"/root/:first/:second/*\", \"\"},\n\t\t{\"PUT\", \"/root/:first/bar/:second/*\", \"\"},\n\t\t{\"PUT\", \"/root/:first/qux/:second/:third/:fourth\", \"\"},\n\t\t{\"PUT\", \"/root/:first/qux/:second/:third/:fourth/*\", \"\"},\n\t\t{\"PUT\", \"/root/*\", \"\"},\n\n\t\t{\"DELETE\", \"/root/:first/foo/*\", \"\"},\n\t\t{\"DELETE\", \"/root/:first/:second/*\", \"\"},\n\t\t{\"DELETE\", \"/root/:first/bar/:second/*\", \"\"},\n\t\t{\"DELETE\", \"/root/:first/qux/:second/:third/:fourth\", \"\"},\n\t\t{\"DELETE\", \"/root/:first/qux/:second/:third/:fourth/*\", \"\"},\n\t\t{\"DELETE\", \"/root/*\", \"\"},\n\t}\n\n\tparamAndAnyAPIToFind = []testRoute{\n\t\t{\"GET\", \"/root/one/foo/after/the/asterisk\", \"\"},\n\t\t{\"GET\", \"/root/one/foo/path/after/the/asterisk\", \"\"},\n\t\t{\"GET\", \"/root/one/two/path/after/the/asterisk\", \"\"},\n\t\t{\"GET\", \"/root/one/bar/two/after/the/asterisk\", \"\"},\n\t\t{\"GET\", \"/root/one/qux/two/three/four\", \"\"},\n\t\t{\"GET\", \"/root/one/qux/two/three/four/after/the/asterisk\", \"\"},\n\n\t\t{\"POST\", \"/root/one/foo/after/the/asterisk\", \"\"},\n\t\t{\"POST\", \"/root/one/foo/path/after/the/asterisk\", \"\"},\n\t\t{\"POST\", \"/root/one/two/path/after/the/asterisk\", \"\"},\n\t\t{\"POST\", \"/root/one/bar/two/after/the/asterisk\", \"\"},\n\t\t{\"POST\", \"/root/one/qux/two/three/four\", \"\"},\n\t\t{\"POST\", \"/root/one/qux/two/three/four/after/the/asterisk\", \"\"},\n\n\t\t{\"PUT\", \"/root/one/foo/after/the/asterisk\", \"\"},\n\t\t{\"PUT\", \"/root/one/foo/path/after/the/asterisk\", \"\"},\n\t\t{\"PUT\", \"/root/one/two/path/after/the/asterisk\", \"\"},\n\t\t{\"PUT\", \"/root/one/bar/two/after/the/asterisk\", \"\"},\n\t\t{\"PUT\", \"/root/one/qux/two/three/four\", \"\"},\n\t\t{\"PUT\", \"/root/one/qux/two/three/four/after/the/asterisk\", \"\"},\n\n\t\t{\"DELETE\", \"/root/one/foo/after/the/asterisk\", \"\"},\n\t\t{\"DELETE\", \"/root/one/foo/path/after/the/asterisk\", \"\"},\n\t\t{\"DELETE\", \"/root/one/two/path/after/the/asterisk\", \"\"},\n\t\t{\"DELETE\", \"/root/one/bar/two/after/the/asterisk\", \"\"},\n\t\t{\"DELETE\", \"/root/one/qux/two/three/four\", \"\"},\n\t\t{\"DELETE\", \"/root/one/qux/two/three/four/after/the/asterisk\", \"\"},\n\t}\n\n\tmissesAPI = []testRoute{\n\t\t{\"GET\", \"/missOne\", \"\"},\n\t\t{\"GET\", \"/miss/two\", \"\"},\n\t\t{\"GET\", \"/miss/three/levels\", \"\"},\n\t\t{\"GET\", \"/miss/four/levels/nooo\", \"\"},\n\n\t\t{\"POST\", \"/missOne\", \"\"},\n\t\t{\"POST\", \"/miss/two\", \"\"},\n\t\t{\"POST\", \"/miss/three/levels\", \"\"},\n\t\t{\"POST\", \"/miss/four/levels/nooo\", \"\"},\n\n\t\t{\"PUT\", \"/missOne\", \"\"},\n\t\t{\"PUT\", \"/miss/two\", \"\"},\n\t\t{\"PUT\", \"/miss/three/levels\", \"\"},\n\t\t{\"PUT\", \"/miss/four/levels/nooo\", \"\"},\n\n\t\t{\"DELETE\", \"/missOne\", \"\"},\n\t\t{\"DELETE\", \"/miss/two\", \"\"},\n\t\t{\"DELETE\", \"/miss/three/levels\", \"\"},\n\t\t{\"DELETE\", \"/miss/four/levels/nooo\", \"\"},\n\t}\n\n\t// handlerHelper created a function that will set a context key for assertion\n\thandlerHelper = func(key string, value int) func(c *Context) error {\n\t\treturn func(c *Context) error {\n\t\t\tc.Set(key, value)\n\t\t\tc.Set(\"path\", c.RouteInfo().Path)\n\t\t\treturn nil\n\t\t}\n\t}\n\thandlerFunc = func(c *Context) error {\n\t\tc.Set(\"path\", c.RouteInfo().Path)\n\t\treturn nil\n\t}\n)\n\nfunc checkUnusedParamValues(t *testing.T, c *Context, expectParam map[string]string) {\n\tfor _, p := range c.PathValues() {\n\t\tvalue := p.Value\n\t\tif value != \"\" {\n\t\t\tif expectParam == nil {\n\t\t\t\tt.Errorf(\"pValue '%v' is set for param name '%v' but we are not expecting it with expectParam\", value, p)\n\t\t\t} else {\n\t\t\t\tif _, ok := expectParam[p.Name]; !ok {\n\t\t\t\t\tt.Errorf(\"pValue '%v' is set for param name '%v' but we are not expecting it with expectParam\", value, p)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestRouterFillsRequestPatternField(t *testing.T) {\n\tpath := \"/folders/a/files/echo.gif\"\n\treq := httptest.NewRequest(http.MethodGet, path, nil)\n\trec := httptest.NewRecorder()\n\n\te := New()\n\te.GET(path, handlerFunc)\n\n\tc := e.NewContext(req, rec)\n\t_ = e.router.Route(c)\n\n\tassert.Equal(t, path, c.Path())\n\tassert.Equal(t, path, c.Request().Pattern)\n}\n\nfunc TestRouterStatic(t *testing.T) {\n\tpath := \"/folders/a/files/echo.gif\"\n\treq := httptest.NewRequest(http.MethodGet, path, nil)\n\trec := httptest.NewRecorder()\n\n\te := New()\n\te.GET(path, handlerFunc)\n\n\tc := e.NewContext(req, rec)\n\t_ = e.router.Route(c)\n\n\tassert.Equal(t, path, c.Path())\n\tassert.Equal(t, 0, cap(*c.pathValues))\n\tassert.Len(t, *c.pathValues, 0)\n}\n\nfunc TestRouterParam(t *testing.T) {\n\te := New()\n\n\te.GET(\"/users/:id\", handlerFunc)\n\n\tvar testCases = []struct {\n\t\texpectRoute any\n\t\texpectParam map[string]string\n\t\tname        string\n\t\twhenURL     string\n\t}{\n\t\t{\n\t\t\tname:        \"route /users/1 to /users/:id\",\n\t\t\twhenURL:     \"/users/1\",\n\t\t\texpectRoute: \"/users/:id\",\n\t\t\texpectParam: map[string]string{\"id\": \"1\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"route /users/1/ to /users/:id\",\n\t\t\twhenURL:     \"/users/1/\",\n\t\t\texpectRoute: \"/users/:id\",\n\t\t\texpectParam: map[string]string{\"id\": \"1/\"},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := e.NewContext(nil, nil)\n\t\t\tc.SetRequest(httptest.NewRequest(http.MethodGet, tc.whenURL, nil))\n\t\t\t_ = e.router.Route(c)\n\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.pathValues.GetOr(param, \"---none---\"))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\nfunc TestRouter_addAndMatchAllSupportedMethods(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname            string\n\t\twhenMethod      string\n\t\texpectPath      string\n\t\texpectError     string\n\t\tgivenNoAddRoute bool\n\t}{\n\t\t{name: \"ok, CONNECT\", whenMethod: http.MethodConnect},\n\t\t{name: \"ok, DELETE\", whenMethod: http.MethodDelete},\n\t\t{name: \"ok, GET\", whenMethod: http.MethodGet},\n\t\t{name: \"ok, HEAD\", whenMethod: http.MethodHead},\n\t\t{name: \"ok, OPTIONS\", whenMethod: http.MethodOptions},\n\t\t{name: \"ok, PATCH\", whenMethod: http.MethodPatch},\n\t\t{name: \"ok, POST\", whenMethod: http.MethodPost},\n\t\t{name: \"ok, PROPFIND\", whenMethod: PROPFIND},\n\t\t{name: \"ok, PUT\", whenMethod: http.MethodPut},\n\t\t{name: \"ok, TRACE\", whenMethod: http.MethodTrace},\n\t\t{name: \"ok, REPORT\", whenMethod: REPORT},\n\t\t{name: \"ok, NON_TRADITIONAL_METHOD\", whenMethod: \"NON_TRADITIONAL_METHOD\"},\n\t\t{\n\t\t\tname:            \"ok, NOT_EXISTING_METHOD\",\n\t\t\twhenMethod:      \"NOT_EXISTING_METHOD\",\n\t\t\tgivenNoAddRoute: true,\n\t\t\texpectPath:      \"/*\",\n\t\t\texpectError:     \"Method Not Allowed\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\n\t\t\te.GET(\"/*\", handlerFunc)\n\t\t\tif !tc.givenNoAddRoute {\n\t\t\t\te.Add(tc.whenMethod, \"/my/*\", handlerFunc)\n\t\t\t}\n\n\t\t\treq := httptest.NewRequest(tc.whenMethod, \"/my/some-url\", nil)\n\t\t\trec := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\thandler := e.router.Route(c)\n\t\t\terr := handler(c)\n\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\n\t\t\texpectPath := \"/my/*\"\n\t\t\tif tc.expectPath != \"\" {\n\t\t\t\texpectPath = tc.expectPath\n\t\t\t}\n\t\t\tassert.Equal(t, expectPath, c.Path())\n\t\t})\n\t}\n}\n\nfunc TestRouterAllowHeaderForAnyOtherMethodType(t *testing.T) {\n\te := New()\n\tr := e.router\n\n\t_, err := r.Add(Route{Method: http.MethodGet, Path: \"/users\", Handler: handlerFunc})\n\tassert.NoError(t, err)\n\t_, err = r.Add(Route{Method: \"COPY\", Path: \"/users\", Handler: handlerFunc})\n\tassert.NoError(t, err)\n\t_, err = r.Add(Route{Method: \"LOCK\", Path: \"/users\", Handler: handlerFunc})\n\tassert.NoError(t, err)\n\n\treq := httptest.NewRequest(\"TEST\", \"/users\", nil)\n\trec := httptest.NewRecorder()\n\n\t//r.Find(\"TEST\", \"/users\", c)\n\tc := e.NewContext(req, rec)\n\n\thandler := e.router.Route(c)\n\terr = handler(c)\n\n\tassert.EqualError(t, err, \"Method Not Allowed\")\n\tassert.ElementsMatch(t, []string{\"COPY\", \"GET\", \"LOCK\", \"OPTIONS\"}, strings.Split(c.Response().Header().Get(HeaderAllow), \", \"))\n}\n\nfunc TestMethodNotAllowedAndNotFound(t *testing.T) {\n\te := New()\n\n\t// Routes\n\tri, err := e.AddRoute(Route{Method: http.MethodGet, Path: \"/*\", Handler: handlerFunc})\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"GET:/*\", ri.Name)\n\n\tri, err = e.AddRoute(Route{Method: http.MethodPost, Path: \"/users/:id\", Handler: handlerFunc})\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"POST:/users/:id\", ri.Name)\n\n\tvar testCases = []struct {\n\t\tname              string\n\t\twhenMethod        string\n\t\twhenURL           string\n\t\texpectRoute       string\n\t\texpectParam       map[string]string\n\t\texpectError       error\n\t\texpectAllowHeader string\n\t}{\n\t\t{\n\t\t\tname:        \"exact match for route+method\",\n\t\t\twhenMethod:  http.MethodPost,\n\t\t\twhenURL:     \"/users/1\",\n\t\t\texpectRoute: \"/users/:id\",\n\t\t\texpectParam: map[string]string{\"id\": \"1\"},\n\t\t},\n\t\t{\n\t\t\tname:              \"matches node but not method. sends 405 from best match node\",\n\t\t\twhenMethod:        http.MethodPut,\n\t\t\twhenURL:           \"/users/1\",\n\t\t\texpectRoute:       \"/users/:id\",\n\t\t\texpectError:       ErrMethodNotAllowed,\n\t\t\texpectAllowHeader: \"OPTIONS, POST\",\n\t\t},\n\t\t{\n\t\t\tname:        \"best match is any route up in tree\",\n\t\t\twhenMethod:  http.MethodGet,\n\t\t\twhenURL:     \"/users/1\",\n\t\t\texpectRoute: \"/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"users/1\"},\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\n\t\t\tmethod := http.MethodGet\n\t\t\tif tc.whenMethod != \"\" {\n\t\t\t\tmethod = tc.whenMethod\n\t\t\t}\n\t\t\treq := httptest.NewRequest(method, tc.whenURL, nil)\n\t\t\trec := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, rec)\n\t\t\thandler := e.router.Route(c)\n\n\t\t\terr := handler(c)\n\t\t\tif tc.expectError != nil {\n\t\t\t\tassert.Equal(t, tc.expectError, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.pathValues.GetOr(param, \"---none---\"))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t\tassert.Equal(t, tc.expectAllowHeader, c.Response().Header().Get(HeaderAllow))\n\t\t})\n\t}\n}\n\nfunc TestRouterOptionsMethodHandler(t *testing.T) {\n\te := New()\n\n\tvar keyInContext any\n\te.Use(func(next HandlerFunc) HandlerFunc {\n\t\treturn func(c *Context) error {\n\t\t\terr := next(c)\n\t\t\tkeyInContext = c.Get(ContextKeyHeaderAllow)\n\t\t\treturn err\n\t\t}\n\t})\n\te.GET(\"/test\", func(c *Context) error {\n\t\treturn c.String(http.StatusOK, \"Echo!\")\n\t})\n\n\treq := httptest.NewRequest(http.MethodOptions, \"/test\", nil)\n\trec := httptest.NewRecorder()\n\te.ServeHTTP(rec, req)\n\n\tassert.Equal(t, http.StatusNoContent, rec.Code)\n\tassert.Equal(t, \"OPTIONS, GET\", rec.Header().Get(HeaderAllow))\n\tassert.Equal(t, \"OPTIONS, GET\", keyInContext)\n}\n\nfunc TestRouterHandleMethodOptions(t *testing.T) {\n\te := New()\n\te.contextPathParamAllocSize.Store(1)\n\tr := e.router\n\n\tr.Add(Route{Method: http.MethodGet, Path: \"/users\", Handler: handlerFunc})\n\tr.Add(Route{Method: http.MethodPost, Path: \"/users\", Handler: handlerFunc})\n\tr.Add(Route{Method: http.MethodPut, Path: \"/users/:id\", Handler: handlerFunc})\n\tr.Add(Route{Method: http.MethodGet, Path: \"/users/:id\", Handler: handlerFunc})\n\n\tvar testCases = []struct {\n\t\tname              string\n\t\twhenMethod        string\n\t\twhenURL           string\n\t\texpectAllowHeader string\n\t\texpectStatus      int\n\t}{\n\t\t{\n\t\t\tname:              \"allows GET and POST handlers\",\n\t\t\twhenMethod:        http.MethodOptions,\n\t\t\twhenURL:           \"/users\",\n\t\t\texpectAllowHeader: \"OPTIONS, GET, POST\",\n\t\t\texpectStatus:      http.StatusNoContent,\n\t\t},\n\t\t{\n\t\t\tname:              \"allows GET and PUT handlers\",\n\t\t\twhenMethod:        http.MethodOptions,\n\t\t\twhenURL:           \"/users/1\",\n\t\t\texpectAllowHeader: \"OPTIONS, GET, PUT\",\n\t\t\texpectStatus:      http.StatusNoContent,\n\t\t},\n\t\t{\n\t\t\tname:              \"GET does not have allows header\",\n\t\t\twhenMethod:        http.MethodGet,\n\t\t\twhenURL:           \"/users\",\n\t\t\texpectAllowHeader: \"\",\n\t\t\texpectStatus:      http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:              \"path with no handlers does not set Allows header\",\n\t\t\twhenMethod:        http.MethodOptions,\n\t\t\twhenURL:           \"/notFound\",\n\t\t\texpectAllowHeader: \"\",\n\t\t\texpectStatus:      http.StatusNotFound,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(tc.whenMethod, tc.whenURL, nil)\n\t\t\trec := httptest.NewRecorder()\n\t\t\tc := e.NewContext(req, rec)\n\n\t\t\th := r.Route(c)\n\t\t\terr := h(c)\n\n\t\t\tif tc.expectStatus >= 400 {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\the := err.(HTTPStatusCoder)\n\t\t\t\tassert.Equal(t, tc.expectStatus, he.StatusCode())\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, tc.expectStatus, rec.Code)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectAllowHeader, c.Response().Header().Get(HeaderAllow))\n\t\t})\n\t}\n}\n\nfunc TestRouterTwoParam(t *testing.T) {\n\te := New()\n\te.GET(\"/users/:uid/files/:fid\", handlerFunc)\n\n\tc := e.NewContext(httptest.NewRequest(http.MethodGet, \"/users/1/files/1\", nil), nil)\n\t_ = e.router.Route(c)\n\n\tassert.Equal(t, \"/users/:uid/files/:fid\", c.Path())\n\tassert.Equal(t, \"1\", c.pathValues.GetOr(\"uid\", \"\"))\n\tassert.Equal(t, \"1\", c.pathValues.GetOr(\"fid\", \"\"))\n}\n\n// Issue #378\nfunc TestRouterParamWithSlash(t *testing.T) {\n\te := New()\n\n\te.GET(\"/a/:b/c/d/:e\", handlerFunc)\n\te.GET(\"/a/:b/c/:d/:f\", handlerFunc)\n\n\tc := e.NewContext(httptest.NewRequest(http.MethodGet, \"/a/1/c/d/2/3\", nil), nil)\n\t// `2/3` should mapped to path `/a/:b/c/d/:e` and into `:e`\n\thandler := e.router.Route(c)\n\n\terr := handler(c)\n\tassert.Equal(t, \"/a/:b/c/d/:e\", c.Path())\n\tassert.NoError(t, err)\n}\n\n// Issue #1754 - router needs to backtrack multiple levels upwards in tree to find the matching route\n// route evaluation order\n//\n// Routes:\n// 1) /a/:b/c\n// 2) /a/c/d\n// 3) /a/c/df\n//\n// 4) /a/*/f\n// 5) /:e/c/f\n//\n// 6) /*\n//\n// Searching route for \"/a/c/f\" should match \"/a/*/f\"\n// When route `4) /a/*/f` is not added then request for \"/a/c/f\" should match \"/:e/c/f\"\n//\n//\t              +----------+\n//\t        +-----+ \"/\" root +--------------------+--------------------------+\n//\t        |     +----------+                    |                          |\n//\t        |                                     |                          |\n//\t+-------v-------+                         +---v---------+        +-------v---+\n//\t| \"a/\" (static) +---------------+         | \":\" (param) |        | \"*\" (any) |\n//\t+-+----------+--+               |         +-----------+-+        +-----------+\n//\t  |          |                  |                     |\n//\n// +---------------v+  +-- ---v------+    +------v----+          +-----v-----------+\n// | \"c/d\" (static) |  | \":\" (param) |    | \"*\" (any) |          | \"/c/f\" (static) |\n// +---------+------+  +--------+----+    +----------++          +-----------------+\n//\n//\t|                  |                    |\n//\t|                  |                    |\n//\n// +---------v----+      +------v--------+    +------v--------+\n// | \"f\" (static) |      | \"/c\" (static) |    | \"/f\" (static) |\n// +--------------+      +---------------+    +---------------+\nfunc TestRouteMultiLevelBacktracking(t *testing.T) {\n\tvar testCases = []struct {\n\t\texpectRoute any\n\t\texpectParam map[string]string\n\t\tname        string\n\t\twhenURL     string\n\t}{\n\t\t{\n\t\t\tname:        \"route /a/c/df to /a/c/df\",\n\t\t\twhenURL:     \"/a/c/df\",\n\t\t\texpectRoute: \"/a/c/df\",\n\t\t},\n\t\t{\n\t\t\tname:        \"route /a/x/df to /a/:b/c\",\n\t\t\twhenURL:     \"/a/x/c\",\n\t\t\texpectRoute: \"/a/:b/c\",\n\t\t\texpectParam: map[string]string{\"b\": \"x\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"route /a/x/f to /a/*/f\",\n\t\t\twhenURL:     \"/a/x/f\",\n\t\t\texpectRoute: \"/a/*/f\",\n\t\t\texpectParam: map[string]string{\"*\": \"x/f\"}, // NOTE: `x` would be probably more suitable\n\t\t},\n\t\t{\n\t\t\tname:        \"route /b/c/f to /:e/c/f\",\n\t\t\twhenURL:     \"/b/c/f\",\n\t\t\texpectRoute: \"/:e/c/f\",\n\t\t\texpectParam: map[string]string{\"e\": \"b\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"route /b/c/c to /*\",\n\t\t\twhenURL:     \"/b/c/c\",\n\t\t\texpectRoute: \"/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"b/c/c\"},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\n\t\t\te.GET(\"/a/:b/c\", handlerFunc)\n\t\t\te.GET(\"/a/c/d\", handlerFunc)\n\t\t\te.GET(\"/a/c/df\", handlerFunc)\n\t\t\te.GET(\"/a/*/f\", handlerFunc)\n\t\t\te.GET(\"/:e/c/f\", handlerFunc)\n\t\t\te.GET(\"/*\", handlerFunc)\n\n\t\t\tc := e.NewContext(httptest.NewRequest(http.MethodGet, tc.whenURL, nil), nil)\n\t\t\t_ = e.router.Route(c)\n\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.pathValues.GetOr(param, \"---none---\"))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\n// Issue #1754 - router needs to backtrack multiple levels upwards in tree to find the matching route\n// route evaluation order\n//\n// Request for \"/a/c/f\" should match \"/:e/c/f\"\n//\n//\t                       +-0,7--------+\n//\t                       | \"/\" (root) |----------------------------------+\n//\t                       +------------+                                  |\n//\t                            |      |                                   |\n//\t                            |      |                                   |\n//\t        +-1,6-----------+   |      |          +-8-----------+   +------v----+\n//\t        | \"a/\" (static) +<--+      +--------->+ \":\" (param) |   | \"*\" (any) |\n//\t        +---------------+                     +-------------+   +-----------+\n//\t           |          |                             |\n//\t+-2--------v-----+   +v-3,5--------+       +-9------v--------+\n//\t| \"c/d\" (static) |   | \":\" (param) |       | \"/c/f\" (static) |\n//\t+----------------+   +-------------+       +-----------------+\n//\t                      |\n//\t                 +-4--v----------+\n//\t                 | \"/c\" (static) |\n//\t                 +---------------+\nfunc TestRouteMultiLevelBacktracking2(t *testing.T) {\n\te := New()\n\n\te.GET(\"/a/:b/c\", handlerFunc)\n\te.GET(\"/a/c/d\", handlerFunc)\n\te.GET(\"/a/c/df\", handlerFunc)\n\te.GET(\"/:e/c/f\", handlerFunc)\n\te.GET(\"/*\", handlerFunc)\n\n\tvar testCases = []struct {\n\t\texpectParam map[string]string\n\t\tname        string\n\t\twhenURL     string\n\t\texpectRoute string\n\t}{\n\t\t{\n\t\t\tname:        \"route /a/c/df to /a/c/df\",\n\t\t\twhenURL:     \"/a/c/df\",\n\t\t\texpectRoute: \"/a/c/df\",\n\t\t},\n\t\t{\n\t\t\tname:        \"route /a/x/df to /a/:b/c\",\n\t\t\twhenURL:     \"/a/x/c\",\n\t\t\texpectRoute: \"/a/:b/c\",\n\t\t\texpectParam: map[string]string{\"b\": \"x\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"route /a/c/f to /:e/c/f\",\n\t\t\twhenURL:     \"/a/c/f\",\n\t\t\texpectRoute: \"/:e/c/f\",\n\t\t\texpectParam: map[string]string{\"e\": \"a\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"route /b/c/f to /:e/c/f\",\n\t\t\twhenURL:     \"/b/c/f\",\n\t\t\texpectRoute: \"/:e/c/f\",\n\t\t\texpectParam: map[string]string{\"e\": \"b\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"route /b/c/c to /*\",\n\t\t\twhenURL:     \"/b/c/c\",\n\t\t\texpectRoute: \"/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"b/c/c\"},\n\t\t},\n\t\t{ // this traverses `/a/:b/c` and `/:e/c/f` branches and eventually backtracks to `/*`\n\t\t\tname:        \"route /a/c/cf to /*\",\n\t\t\twhenURL:     \"/a/c/cf\",\n\t\t\texpectRoute: \"/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"a/c/cf\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"route /anyMatch to /*\",\n\t\t\twhenURL:     \"/anyMatch\",\n\t\t\texpectRoute: \"/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"anyMatch\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"route /anyMatch/withSlash to /*\",\n\t\t\twhenURL:     \"/anyMatch/withSlash\",\n\t\t\texpectRoute: \"/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"anyMatch/withSlash\"},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := e.NewContext(httptest.NewRequest(http.MethodGet, tc.whenURL, nil), nil)\n\t\t\t_ = e.router.Route(c)\n\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.pathValues.GetOr(param, \"---none---\"))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\nfunc TestRouterBacktrackingFromMultipleParamKinds(t *testing.T) {\n\te := New()\n\n\te.GET(\"/*\", handlerFunc) // this can match only path that does not have slash in it\n\te.GET(\"/:1/second\", handlerFunc)\n\te.GET(\"/:1/:2\", handlerFunc) // this acts as match ANY for all routes that have at least one slash\n\te.GET(\"/:1/:2/third\", handlerFunc)\n\te.GET(\"/:1/:2/:3/fourth\", handlerFunc)\n\te.GET(\"/:1/:2/:3/:4/fifth\", handlerFunc)\n\n\tvar testCases = []struct {\n\t\texpectParam map[string]string\n\t\tname        string\n\t\twhenURL     string\n\t\texpectRoute string\n\t}{\n\t\t{\n\t\t\tname:        \"route /first to /*\",\n\t\t\twhenURL:     \"/first\",\n\t\t\texpectRoute: \"/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"first\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"route /first/second to /:1/second\",\n\t\t\twhenURL:     \"/first/second\",\n\t\t\texpectRoute: \"/:1/second\",\n\t\t\texpectParam: map[string]string{\"1\": \"first\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"route /first/second-new to /:1/:2\",\n\t\t\twhenURL:     \"/first/second-new\",\n\t\t\texpectRoute: \"/:1/:2\",\n\t\t\texpectParam: map[string]string{\n\t\t\t\t\"1\": \"first\",\n\t\t\t\t\"2\": \"second-new\",\n\t\t\t},\n\t\t},\n\t\t{ // FIXME: should match `/:1/:2` when backtracking in tree. this 1 level backtracking fails even with old implementation\n\t\t\tname:        \"route /first/second/ to /:1/:2\",\n\t\t\twhenURL:     \"/first/second/\",                        /// <-- slash at the end is problematic\n\t\t\texpectRoute: \"/*\",                                    // \"/:1/:2\",\n\t\t\texpectParam: map[string]string{\"*\": \"first/second/\"}, // map[string]string{\"1\": \"first\", \"2\": \"second/\"},\n\t\t},\n\t\t{ // FIXME: should match `/:1/:2`. same backtracking problem. when backtracking is at `/:1/:2` during backtracking this node should be match as it has executable handler\n\t\t\tname:        \"route /first/second/third/fourth/fifth/nope to /:1/:2\",\n\t\t\twhenURL:     \"/first/second/third/fourth/fifth/nope\",\n\t\t\texpectRoute: \"/*\",                                                           // \"/:1/:2\",\n\t\t\texpectParam: map[string]string{\"*\": \"first/second/third/fourth/fifth/nope\"}, // map[string]string{\"1\": \"first\", \"2\": \"second/third/fourth/fifth/nope\"},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := e.NewContext(httptest.NewRequest(http.MethodGet, tc.whenURL, nil), nil)\n\t\t\t_ = e.router.Route(c)\n\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.pathValues.GetOr(param, \"---none---\"))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\n// Issue #1509\nfunc TestRouterParamStaticConflict(t *testing.T) {\n\te := New()\n\n\tg := e.Group(\"/g\")\n\tg.GET(\"/skills\", handlerFunc)\n\tg.GET(\"/status\", handlerFunc)\n\tg.GET(\"/:name\", handlerFunc)\n\n\tvar testCases = []struct {\n\t\texpectRoute any\n\t\texpectParam map[string]string\n\t\twhenURL     string\n\t}{\n\t\t{\n\t\t\twhenURL:     \"/g/s\",\n\t\t\texpectRoute: \"/g/:name\",\n\t\t\texpectParam: map[string]string{\"name\": \"s\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/g/status\",\n\t\t\texpectRoute: \"/g/status\",\n\t\t\texpectParam: map[string]string{\"name\": \"\"},\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenURL, func(t *testing.T) {\n\t\t\tc := e.NewContext(httptest.NewRequest(http.MethodGet, tc.whenURL, nil), nil)\n\n\t\t\thandler := e.router.Route(c)\n\n\t\t\tassert.NoError(t, handler(c))\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.pathValues.GetOr(param, \"\"))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\nfunc TestRouterParam_escapeColon(t *testing.T) {\n\t// to allow Google cloud API like route paths with colon in them\n\t// i.e. https://service.name/v1/some/resource/name:customVerb <- that `:customVerb` is not path param. It is just a string\n\te := New()\n\n\te.POST(\"/files/a/long/file\\\\:undelete\", handlerFunc)\n\te.POST(\"/multilevel\\\\:undelete/second\\\\:something\", handlerFunc)\n\te.POST(\"/mixed/:id/second\\\\:something\", handlerFunc)\n\te.POST(\"/v1/some/resource/name:customVerb\", handlerFunc)\n\n\tvar testCases = []struct {\n\t\twhenURL     string\n\t\texpectRoute string\n\t\texpectParam map[string]string\n\t\texpectError string\n\t}{\n\t\t{\n\t\t\twhenURL:     \"/files/a/long/file:undelete\",\n\t\t\texpectRoute: \"/files/a/long/file\\\\:undelete\",\n\t\t\texpectParam: map[string]string{},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/multilevel:undelete/second:something\",\n\t\t\texpectRoute: \"/multilevel\\\\:undelete/second\\\\:something\",\n\t\t\texpectParam: map[string]string{},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/mixed/123/second:something\",\n\t\t\texpectRoute: \"/mixed/:id/second\\\\:something\",\n\t\t\texpectParam: map[string]string{\"id\": \"123\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/files/a/long/file:notMatching\",\n\t\t\texpectRoute: \"\",\n\t\t\texpectError: \"Not Found\",\n\t\t\texpectParam: nil,\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/v1/some/resource/name:PATCH\",\n\t\t\texpectRoute: \"/v1/some/resource/name:customVerb\",\n\t\t\texpectParam: map[string]string{\"customVerb\": \":PATCH\"},\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenURL, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodPost, tc.whenURL, nil)\n\t\t\tc := e.NewContext(req, nil)\n\t\t\thandler := e.router.Route(c)\n\n\t\t\terr := handler(c)\n\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.Param(param))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\nfunc TestRouterMatchAny(t *testing.T) {\n\te := New()\n\n\t// Routes\n\te.GET(\"/\", handlerFunc)\n\te.GET(\"/*\", handlerFunc)\n\te.GET(\"/users/*\", handlerFunc)\n\n\tvar testCases = []struct {\n\t\texpectRoute any\n\t\texpectParam map[string]string\n\t\twhenURL     string\n\t}{\n\t\t{\n\t\t\twhenURL:     \"/\",\n\t\t\texpectRoute: \"/\",\n\t\t\texpectParam: map[string]string{\"*\": \"\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/download\",\n\t\t\texpectRoute: \"/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"download\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/joe\",\n\t\t\texpectRoute: \"/users/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"joe\"},\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenURL, func(t *testing.T) {\n\t\t\tc := e.NewContext(httptest.NewRequest(http.MethodGet, tc.whenURL, nil), nil)\n\n\t\t\thandler := e.router.Route(c)\n\n\t\t\tassert.NoError(t, handler(c))\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.pathValues.GetOr(param, \"\"))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\n// NOTE: this is to document current implementation. Last added route with `*` asterisk is always the match and no\n// backtracking or more precise matching is done to find more suitable match.\n//\n// Current behaviour might not be correct or expected.\n// But this is where we are without well defined requirements/rules how (multiple) asterisks work in route\nfunc TestRouterAnyMatchesLastAddedAnyRoute(t *testing.T) {\n\te := New()\n\n\te.GET(\"/users/*\", handlerFunc)\n\te.GET(\"/users/*/action*\", handlerFunc)\n\n\treq := httptest.NewRequest(http.MethodGet, \"/users/xxx/action/sea\", nil)\n\tc := e.NewContext(req, nil)\n\thandler := e.router.Route(c)\n\n\tassert.NoError(t, handler(c))\n\tassert.Equal(t, \"/users/*/action*\", c.Path())\n\tassert.Equal(t, \"xxx/action/sea\", c.pathValues.GetOr(\"*\", \"\"))\n\n\t// if we add another route then it is the last added and so it is matched\n\te.GET(\"/users/*/action/search\", handlerFunc)\n\n\tc2 := e.NewContext(httptest.NewRequest(http.MethodGet, \"/users/xxx/action/sea\", nil), nil)\n\thandler2 := e.router.Route(c2)\n\n\tassert.NoError(t, handler2(c2))\n\tassert.Equal(t, \"/users/*/action/search\", c2.Path())\n\tassert.Equal(t, \"xxx/action/sea\", c2.pathValues.GetOr(\"*\", \"\"))\n}\n\n// Issue #1739\nfunc TestRouterMatchAnyPrefixIssue(t *testing.T) {\n\te := New()\n\n\t// Routes\n\te.GET(\"/*\", handlerFunc)\n\te.GET(\"/users/*\", handlerFunc)\n\n\tvar testCases = []struct {\n\t\texpectRoute any\n\t\texpectParam map[string]string\n\t\twhenURL     string\n\t}{\n\t\t{\n\t\t\twhenURL:     \"/\",\n\t\t\texpectRoute: \"/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users\",\n\t\t\texpectRoute: \"/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"users\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/\",\n\t\t\texpectRoute: \"/users/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users_prefix\",\n\t\t\texpectRoute: \"/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"users_prefix\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users_prefix/\",\n\t\t\texpectRoute: \"/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"users_prefix/\"},\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenURL, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)\n\t\t\tc := e.NewContext(req, nil)\n\t\t\thandler := e.router.Route(c)\n\n\t\t\tassert.NoError(t, handler(c))\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.pathValues.GetOr(param, \"---none---\"))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\n// TestRouterMatchAnySlash shall verify finding the best route\n// for any routes with trailing slash requests\nfunc TestRouterMatchAnySlash(t *testing.T) {\n\te := New()\n\n\t// Routes\n\te.GET(\"/users\", handlerFunc)\n\te.GET(\"/users/*\", handlerFunc)\n\te.GET(\"/img/*\", handlerFunc)\n\te.GET(\"/img/load\", handlerFunc)\n\te.GET(\"/img/load/*\", handlerFunc)\n\te.GET(\"/assets/*\", handlerFunc)\n\n\tvar testCases = []struct {\n\t\texpectError error\n\t\texpectParam map[string]string\n\t\twhenURL     string\n\t\texpectRoute string\n\t}{\n\t\t{\n\t\t\twhenURL:     \"/\",\n\t\t\texpectRoute: \"\",\n\t\t\texpectParam: map[string]string{\"*\": \"\"},\n\t\t\texpectError: ErrNotFound,\n\t\t},\n\t\t{ // Test trailing slash request for simple any route (see #1526)\n\t\t\twhenURL:     \"/users/\",\n\t\t\texpectRoute: \"/users/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/joe\",\n\t\t\texpectRoute: \"/users/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"joe\"},\n\t\t},\n\t\t// Test trailing slash request for nested any route (see #1526)\n\t\t{\n\t\t\twhenURL:     \"/img/load\",\n\t\t\texpectRoute: \"/img/load\",\n\t\t\texpectParam: map[string]string{\"*\": \"\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/img/load/\",\n\t\t\texpectRoute: \"/img/load/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/img/load/ben\",\n\t\t\texpectRoute: \"/img/load/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"ben\"},\n\t\t},\n\t\t// Test /assets/* any route\n\t\t{ // ... without trailing slash must not match\n\t\t\twhenURL:     \"/assets\",\n\t\t\texpectRoute: \"\",\n\t\t\texpectParam: map[string]string{\"*\": \"\"},\n\t\t\texpectError: ErrNotFound,\n\t\t},\n\n\t\t{ // ... with trailing slash must match\n\t\t\twhenURL:     \"/assets/\",\n\t\t\texpectRoute: \"/assets/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"\"},\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenURL, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)\n\t\t\tc := e.NewContext(req, nil)\n\t\t\thandler := e.router.Route(c)\n\n\t\t\terr := handler(c)\n\t\t\tif tc.expectError != nil {\n\t\t\t\tassert.Equal(t, tc.expectError, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.pathValues.GetOr(param, \"\"))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\nfunc TestRouterMatchAnyMultiLevel(t *testing.T) {\n\te := New()\n\n\t// Routes\n\te.GET(\"/api/users/jack\", handlerFunc)\n\te.GET(\"/api/users/jill\", handlerFunc)\n\te.GET(\"/api/users/*\", handlerFunc)\n\te.GET(\"/api/*\", handlerFunc)\n\te.GET(\"/other/*\", handlerFunc)\n\te.GET(\"/*\", handlerFunc)\n\n\tvar testCases = []struct {\n\t\texpectRoute any\n\t\texpectError error\n\t\texpectParam map[string]string\n\t\twhenURL     string\n\t}{\n\t\t{\n\t\t\twhenURL:     \"/api/users/jack\",\n\t\t\texpectRoute: \"/api/users/jack\",\n\t\t\texpectParam: map[string]string{\"*\": \"\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/api/users/jill\",\n\t\t\texpectRoute: \"/api/users/jill\",\n\t\t\texpectParam: map[string]string{\"*\": \"\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/api/users/joe\",\n\t\t\texpectRoute: \"/api/users/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"joe\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/api/nousers/joe\",\n\t\t\texpectRoute: \"/api/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"nousers/joe\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/api/none\",\n\t\t\texpectRoute: \"/api/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"none\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/api/none\",\n\t\t\texpectRoute: \"/api/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"none\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/noapi/users/jim\",\n\t\t\texpectRoute: \"/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"noapi/users/jim\"},\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenURL, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)\n\t\t\tc := e.NewContext(req, nil)\n\t\t\thandler := e.router.Route(c)\n\n\t\t\terr := handler(c)\n\t\t\tif tc.expectError != nil {\n\t\t\t\tassert.Equal(t, tc.expectError, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.pathValues.GetOr(param, \"\"))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\nfunc TestRouterMatchAnyMultiLevelWithPost(t *testing.T) {\n\te := New()\n\n\t// Routes\n\te.POST(\"/api/auth/login\", handlerFunc)\n\te.POST(\"/api/auth/forgotPassword\", handlerFunc)\n\te.Any(\"/api/*\", handlerFunc)\n\te.Any(\"/*\", handlerFunc)\n\n\tvar testCases = []struct {\n\t\texpectRoute any\n\t\texpectError error\n\t\texpectParam map[string]string\n\t\twhenMethod  string\n\t\twhenURL     string\n\t}{\n\t\t{ // POST /api/auth/login shall choose login method\n\t\t\twhenURL:     \"/api/auth/login\",\n\t\t\twhenMethod:  http.MethodPost,\n\t\t\texpectRoute: \"/api/auth/login\",\n\t\t\texpectParam: map[string]string{\"*\": \"\"},\n\t\t},\n\t\t{ // POST /api/auth/logout shall choose nearest any route\n\t\t\twhenURL:     \"/api/auth/logout\",\n\t\t\twhenMethod:  http.MethodPost,\n\t\t\texpectRoute: \"/api/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"auth/logout\"},\n\t\t},\n\t\t{ // POST to /api/other/test shall choose nearest any route\n\t\t\twhenURL:     \"/api/other/test\",\n\t\t\twhenMethod:  http.MethodPost,\n\t\t\texpectRoute: \"/api/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"other/test\"},\n\t\t},\n\t\t{ // GET to /api/other/test shall choose nearest any route\n\t\t\twhenURL:     \"/api/other/test\",\n\t\t\twhenMethod:  http.MethodGet,\n\t\t\texpectRoute: \"/api/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"other/test\"},\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenURL, func(t *testing.T) {\n\t\t\tmethod := http.MethodGet\n\t\t\tif tc.whenMethod != \"\" {\n\t\t\t\tmethod = tc.whenMethod\n\t\t\t}\n\n\t\t\treq := httptest.NewRequest(method, tc.whenURL, nil)\n\t\t\tc := e.NewContext(req, nil)\n\t\t\thandler := e.router.Route(c)\n\n\t\t\terr := handler(c)\n\t\t\tif tc.expectError != nil {\n\t\t\t\tassert.Equal(t, tc.expectError, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.pathValues.GetOr(param, \"\"))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\nfunc TestRouterMicroParam(t *testing.T) {\n\te := New()\n\te.GET(\"/:a/:b/:c\", handlerFunc)\n\n\treq := httptest.NewRequest(http.MethodGet, \"/1/2/3\", nil)\n\tc := e.NewContext(req, nil)\n\thandler := e.router.Route(c)\n\n\tassert.NoError(t, handler(c))\n\tassert.Equal(t, \"1\", c.pathValues.GetOr(\"a\", \"---none---\"))\n\tassert.Equal(t, \"2\", c.pathValues.GetOr(\"b\", \"---none---\"))\n\tassert.Equal(t, \"3\", c.pathValues.GetOr(\"c\", \"---none---\"))\n}\n\nfunc TestRouterMixParamMatchAny(t *testing.T) {\n\te := New()\n\n\t// Route\n\te.GET(\"/users/:id/*\", handlerFunc)\n\n\treq := httptest.NewRequest(http.MethodGet, \"/users/joe/comments\", nil)\n\tc := e.NewContext(req, nil)\n\thandler := e.router.Route(c)\n\n\tassert.NoError(t, handler(c))\n\tassert.Equal(t, \"/users/:id/*\", c.Path())\n\tassert.Equal(t, \"joe\", c.pathValues.GetOr(\"id\", \"---none---\"))\n\tassert.Equal(t, \"comments\", c.pathValues.GetOr(\"*\", \"---none---\"))\n}\n\nfunc TestRouterMultiRoute(t *testing.T) {\n\te := New()\n\n\t// Routes\n\te.GET(\"/users\", handlerFunc)\n\te.GET(\"/users/:id\", handlerFunc)\n\n\tvar testCases = []struct {\n\t\texpectError error\n\t\texpectParam map[string]string\n\t\twhenMethod  string\n\t\twhenURL     string\n\t\texpectRoute string\n\t}{\n\t\t{\n\t\t\twhenURL:     \"/users\",\n\t\t\texpectRoute: \"/users\",\n\t\t\texpectParam: map[string]string{\"*\": \"\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/1\",\n\t\t\texpectRoute: \"/users/:id\",\n\t\t\texpectParam: map[string]string{\"id\": \"1\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/user\",\n\t\t\texpectRoute: \"\",\n\t\t\texpectParam: map[string]string{\"*\": \"\"},\n\t\t\texpectError: ErrNotFound,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenURL, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)\n\t\t\tc := e.NewContext(req, nil)\n\t\t\thandler := e.router.Route(c)\n\n\t\t\terr := handler(c)\n\t\t\tif tc.expectError != nil {\n\t\t\t\tassert.Equal(t, tc.expectError, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.pathValues.GetOr(param, \"\"))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\nfunc TestRouterPriority(t *testing.T) {\n\te := New()\n\n\t// Routes\n\te.GET(\"/users\", handlerFunc)\n\te.GET(\"/users/new\", handlerFunc)\n\te.GET(\"/users/:id\", handlerFunc)\n\te.GET(\"/users/dew\", handlerFunc)\n\te.GET(\"/users/:id/files\", handlerFunc)\n\te.GET(\"/users/newsee\", handlerFunc)\n\te.GET(\"/users/*\", handlerFunc)\n\te.GET(\"/users/new/*\", handlerFunc)\n\te.GET(\"/*\", handlerFunc)\n\n\tvar testCases = []struct {\n\t\texpectRoute any\n\t\texpectError error\n\t\texpectParam map[string]string\n\t\twhenMethod  string\n\t\twhenURL     string\n\t}{\n\t\t{\n\t\t\twhenURL:     \"/users\",\n\t\t\texpectRoute: \"/users\",\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/new\",\n\t\t\texpectRoute: \"/users/new\",\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/1\",\n\t\t\texpectRoute: \"/users/:id\",\n\t\t\texpectParam: map[string]string{\"id\": \"1\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/dew\",\n\t\t\texpectRoute: \"/users/dew\",\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/1/files\",\n\t\t\texpectRoute: \"/users/:id/files\",\n\t\t\texpectParam: map[string]string{\"id\": \"1\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/new\",\n\t\t\texpectRoute: \"/users/new\",\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/news\",\n\t\t\texpectRoute: \"/users/:id\",\n\t\t\texpectParam: map[string]string{\"id\": \"news\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/newsee\",\n\t\t\texpectRoute: \"/users/newsee\",\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/joe/books\",\n\t\t\texpectRoute: \"/users/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"joe/books\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/new/someone\",\n\t\t\texpectRoute: \"/users/new/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"someone\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/dew/someone\",\n\t\t\texpectRoute: \"/users/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"dew/someone\"},\n\t\t},\n\t\t{ // Route > /users/* should be matched although /users/dew exists\n\t\t\twhenURL:     \"/users/notexists/someone\",\n\t\t\texpectRoute: \"/users/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"notexists/someone\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/nousers\",\n\t\t\texpectRoute: \"/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"nousers\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/nousers/new\",\n\t\t\texpectRoute: \"/*\",\n\t\t\texpectParam: map[string]string{\"*\": \"nousers/new\"},\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenURL, func(t *testing.T) {\n\n\t\t\tmethod := http.MethodGet\n\t\t\tif tc.whenMethod != \"\" {\n\t\t\t\tmethod = tc.whenMethod\n\t\t\t}\n\n\t\t\treq := httptest.NewRequest(method, tc.whenURL, nil)\n\t\t\tc := e.NewContext(req, nil)\n\t\t\thandler := e.router.Route(c)\n\n\t\t\terr := handler(c)\n\t\t\tif tc.expectError != nil {\n\t\t\t\tassert.Equal(t, tc.expectError, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.pathValues.GetOr(param, \"---none---\"))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\nfunc TestRouterIssue1348(t *testing.T) {\n\te := New()\n\n\te.GET(\"/:lang/\", handlerFunc)\n\te.GET(\"/:lang/dupa\", handlerFunc)\n}\n\n// Issue #372\nfunc TestRouterPriorityNotFound(t *testing.T) {\n\te := New()\n\n\t// Add\n\te.GET(\"/a/foo\", handlerFunc)\n\te.GET(\"/a/bar\", handlerFunc)\n\n\tvar testCases = []struct {\n\t\texpectError error\n\t\texpectParam map[string]string\n\t\twhenMethod  string\n\t\twhenURL     string\n\t\texpectRoute string\n\t}{\n\t\t{\n\t\t\twhenURL:     \"/a/foo\",\n\t\t\texpectRoute: \"/a/foo\",\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/a/bar\",\n\t\t\texpectRoute: \"/a/bar\",\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/abc/def\",\n\t\t\texpectRoute: \"\",\n\t\t\texpectError: ErrNotFound,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenURL, func(t *testing.T) {\n\n\t\t\tmethod := http.MethodGet\n\t\t\tif tc.whenMethod != \"\" {\n\t\t\t\tmethod = tc.whenMethod\n\t\t\t}\n\n\t\t\treq := httptest.NewRequest(method, tc.whenURL, nil)\n\t\t\tc := e.NewContext(req, nil)\n\t\t\thandler := e.router.Route(c)\n\n\t\t\terr := handler(c)\n\t\t\tif tc.expectError != nil {\n\t\t\t\tassert.Equal(t, tc.expectError, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.pathValues.GetOr(param, \"---none---\"))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\nfunc TestRouterParamNames(t *testing.T) {\n\te := New()\n\n\t// Routes\n\te.GET(\"/users\", handlerFunc)\n\te.GET(\"/users/:id\", handlerFunc)\n\te.GET(\"/users/:uid/files/:fid\", handlerFunc)\n\n\tvar testCases = []struct {\n\t\texpectRoute any\n\t\texpectError error\n\t\texpectParam map[string]string\n\t\twhenMethod  string\n\t\twhenURL     string\n\t}{\n\t\t{\n\t\t\twhenURL:     \"/users\",\n\t\t\texpectRoute: \"/users\",\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/1\",\n\t\t\texpectRoute: \"/users/:id\",\n\t\t\texpectParam: map[string]string{\"id\": \"1\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/1/files/1\",\n\t\t\texpectRoute: \"/users/:uid/files/:fid\",\n\t\t\texpectParam: map[string]string{\n\t\t\t\t\"uid\": \"1\",\n\t\t\t\t\"fid\": \"1\",\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenURL, func(t *testing.T) {\n\n\t\t\tmethod := http.MethodGet\n\t\t\tif tc.whenMethod != \"\" {\n\t\t\t\tmethod = tc.whenMethod\n\t\t\t}\n\n\t\t\treq := httptest.NewRequest(method, tc.whenURL, nil)\n\t\t\tc := e.NewContext(req, nil)\n\t\t\thandler := e.router.Route(c)\n\n\t\t\terr := handler(c)\n\t\t\tif tc.expectError != nil {\n\t\t\t\tassert.Equal(t, tc.expectError, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.pathValues.GetOr(param, \"---none---\"))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\n// Issue #623 and #1406\nfunc TestRouterStaticDynamicConflict(t *testing.T) {\n\te := New()\n\n\te.GET(\"/dictionary/skills\", handlerFunc)\n\te.GET(\"/dictionary/:name\", handlerFunc)\n\te.GET(\"/users/new\", handlerFunc)\n\te.GET(\"/users/:name\", handlerFunc)\n\te.GET(\"/server\", handlerFunc)\n\te.GET(\"/\", handlerFunc)\n\n\tvar testCases = []struct {\n\t\texpectError error\n\t\texpectParam map[string]string\n\t\twhenMethod  string\n\t\twhenURL     string\n\t\texpectRoute string\n\t}{\n\t\t{\n\t\t\twhenURL:     \"/dictionary/skills\",\n\t\t\texpectRoute: \"/dictionary/skills\",\n\t\t\texpectParam: map[string]string{\"*\": \"\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/dictionary/skillsnot\",\n\t\t\texpectRoute: \"/dictionary/:name\",\n\t\t\texpectParam: map[string]string{\"name\": \"skillsnot\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/dictionary/type\",\n\t\t\texpectRoute: \"/dictionary/:name\",\n\t\t\texpectParam: map[string]string{\"name\": \"type\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/server\",\n\t\t\texpectRoute: \"/server\",\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/new\",\n\t\t\texpectRoute: \"/users/new\",\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/new2\",\n\t\t\texpectRoute: \"/users/:name\",\n\t\t\texpectParam: map[string]string{\"name\": \"new2\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/\",\n\t\t\texpectRoute: \"/\",\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenURL, func(t *testing.T) {\n\t\t\tmethod := http.MethodGet\n\t\t\tif tc.whenMethod != \"\" {\n\t\t\t\tmethod = tc.whenMethod\n\t\t\t}\n\n\t\t\treq := httptest.NewRequest(method, tc.whenURL, nil)\n\t\t\tc := e.NewContext(req, nil)\n\t\t\thandler := e.router.Route(c)\n\n\t\t\terr := handler(c)\n\t\t\tif tc.expectError != nil {\n\t\t\t\tassert.Equal(t, tc.expectError, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.pathValues.GetOr(param, \"\"))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\n// Issue #1348\nfunc TestRouterParamBacktraceNotFound(t *testing.T) {\n\te := New()\n\n\t// Add\n\te.GET(\"/:param1\", handlerFunc)\n\te.GET(\"/:param1/foo\", handlerFunc)\n\te.GET(\"/:param1/bar\", handlerFunc)\n\te.GET(\"/:param1/bar/:param2\", handlerFunc)\n\n\tvar testCases = []struct {\n\t\texpectError error\n\t\texpectParam map[string]string\n\t\tname        string\n\t\twhenMethod  string\n\t\twhenURL     string\n\t\texpectRoute string\n\t}{\n\t\t{\n\t\t\tname:        \"route /a to /:param1\",\n\t\t\twhenURL:     \"/a\",\n\t\t\texpectRoute: \"/:param1\",\n\t\t\texpectParam: map[string]string{\"param1\": \"a\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"route /a/foo to /:param1/foo\",\n\t\t\twhenURL:     \"/a/foo\",\n\t\t\texpectRoute: \"/:param1/foo\",\n\t\t\texpectParam: map[string]string{\"param1\": \"a\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"route /a/bar to /:param1/bar\",\n\t\t\twhenURL:     \"/a/bar\",\n\t\t\texpectRoute: \"/:param1/bar\",\n\t\t\texpectParam: map[string]string{\"param1\": \"a\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"route /a/bar/b to /:param1/bar/:param2\",\n\t\t\twhenURL:     \"/a/bar/b\",\n\t\t\texpectRoute: \"/:param1/bar/:param2\",\n\t\t\texpectParam: map[string]string{\n\t\t\t\t\"param1\": \"a\",\n\t\t\t\t\"param2\": \"b\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"route /a/bbbbb should return 404\",\n\t\t\twhenURL:     \"/a/bbbbb\",\n\t\t\texpectRoute: \"\",\n\t\t\texpectError: ErrNotFound,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tmethod := http.MethodGet\n\t\t\tif tc.whenMethod != \"\" {\n\t\t\t\tmethod = tc.whenMethod\n\t\t\t}\n\n\t\t\treq := httptest.NewRequest(method, tc.whenURL, nil)\n\t\t\tc := e.NewContext(req, nil)\n\t\t\thandler := e.router.Route(c)\n\n\t\t\terr := handler(c)\n\t\t\tif tc.expectError != nil {\n\t\t\t\tassert.Equal(t, tc.expectError, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.pathValues.GetOr(param, \"---none---\"))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\nfunc testRouterAPI(t *testing.T, api []testRoute) {\n\te := New()\n\n\tfor _, route := range api {\n\t\tri, err := e.AddRoute(Route{\n\t\t\tMethod: route.Method,\n\t\t\tPath:   route.Path,\n\t\t\tHandler: func(c *Context) error {\n\t\t\t\treturn nil\n\t\t\t},\n\t\t})\n\t\tassert.NoError(t, err)\n\t\tassert.NotNil(t, ri)\n\t}\n\n\tc := e.NewContext(nil, nil)\n\tfor _, route := range api {\n\t\tt.Run(route.Path, func(t *testing.T) {\n\t\t\tc.SetRequest(httptest.NewRequest(route.Method, route.Path, nil))\n\t\t\te.router.Route(c)\n\n\t\t\ttokens := strings.Split(route.Path[1:], \"/\")\n\t\t\tfor _, token := range tokens {\n\t\t\t\tif token[0] == ':' {\n\t\t\t\t\tassert.Equal(t, c.pathValues.GetOr(token[1:], \"---none---\"), token)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRouterGitHubAPI(t *testing.T) {\n\ttestRouterAPI(t, gitHubAPI)\n}\n\nfunc TestRouter_Match_DifferentParamNamesForSamePlace(t *testing.T) {\n\tvar testCases = []struct {\n\t\texpectError error\n\t\texpectParam map[string]string\n\t\tname        string\n\t\twhenURL     string\n\t\twhenMethod  string\n\t\texpectRoute string\n\t}{\n\t\t{\n\t\t\tname:        \"ok, 1=id + 2=file\",\n\t\t\twhenURL:     \"/users/123/file/payroll.csv\",\n\t\t\twhenMethod:  http.MethodGet,\n\t\t\texpectRoute: \"/users/:id/file/:file\",\n\t\t\texpectParam: map[string]string{\"id\": \"123\", \"file\": \"payroll.csv\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, 1=id2 + 2=file2\",\n\t\t\twhenURL:     \"/users/123/file/payroll.csv\",\n\t\t\twhenMethod:  http.MethodPost,\n\t\t\texpectRoute: \"/users/:id2/file/:file2\",\n\t\t\texpectParam: map[string]string{\"id2\": \"123\", \"file2\": \"payroll.csv\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, 1=uid\",\n\t\t\twhenURL:     \"/users/999/files\",\n\t\t\twhenMethod:  http.MethodGet,\n\t\t\texpectRoute: \"/users/:uid/files\",\n\t\t\texpectParam: map[string]string{\"uid\": \"999\"},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\n\t\t\te.GET(\"/users/create\", handlerFunc)\n\t\t\te.GET(\"/users/:id/file/:file\", handlerFunc)\n\t\t\te.POST(\"/users/:id2/file/:file2\", handlerFunc)\n\t\t\te.GET(\"/users/:uid/files\", handlerFunc)\n\n\t\t\treq := httptest.NewRequest(tc.whenMethod, tc.whenURL, nil)\n\t\t\tc := e.NewContext(req, nil)\n\t\t\thandler := e.router.Route(c)\n\n\t\t\terr := handler(c)\n\t\t\tif tc.expectError != nil {\n\t\t\t\tassert.Equal(t, tc.expectError, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.pathValues.GetOr(param, \"\"))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\n// Issue #2164 - this test is meant to document path parameter behaviour when request url has empty value in place\n// of the path parameter. As tests show the result is different depending on where parameter exists in the route path.\nfunc TestDefaultRouter_PathValuesCanMatchEmptyValues(t *testing.T) {\n\tvar testCases = []struct {\n\t\texpectError error\n\t\texpectParam map[string]string\n\t\tname        string\n\t\twhenURL     string\n\t\texpectRoute string\n\t}{\n\t\t{\n\t\t\tname:        \"ok, route is matched with even empty param is in the middle and between slashes\",\n\t\t\twhenURL:     \"/a//b\",\n\t\t\texpectRoute: \"/a/:id/b\",\n\t\t\texpectParam: map[string]string{\"id\": \"\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, route is matched with even empty param is in the middle\",\n\t\t\twhenURL:     \"/a2/b\",\n\t\t\texpectRoute: \"/a2:id/b\",\n\t\t\texpectParam: map[string]string{\"id\": \"\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok, route is NOT matched with even empty param is at the end\",\n\t\t\twhenURL:     \"/a3/\",\n\t\t\texpectRoute: \"\",\n\t\t\texpectParam: map[string]string{},\n\t\t\texpectError: ErrNotFound,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\n\t\t\te.GET(\"/a/:id/b\", handlerFunc)\n\t\t\te.GET(\"/a2:id/b\", handlerFunc)\n\t\t\te.GET(\"/a3/:id\", handlerFunc)\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)\n\t\t\tc := e.NewContext(req, nil)\n\t\t\thandler := e.router.Route(c)\n\n\t\t\terr := handler(c)\n\t\t\tif tc.expectError != nil {\n\t\t\t\tassert.Equal(t, tc.expectError, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.pathValues.GetOr(param, \"\"))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\n// Issue #729\nfunc TestRouterParamAlias(t *testing.T) {\n\tapi := []testRoute{\n\t\t{http.MethodGet, \"/users/:userID/following\", \"\"},\n\t\t{http.MethodGet, \"/users/:userID/followedBy\", \"\"},\n\t\t{http.MethodGet, \"/users/:userID/follow\", \"\"},\n\t}\n\ttestRouterAPI(t, api)\n}\n\n// Issue #1052\nfunc TestRouterParamOrdering(t *testing.T) {\n\tapi := []testRoute{\n\t\t{http.MethodGet, \"/:a/:b/:c/:id\", \"\"},\n\t\t{http.MethodGet, \"/:a/:id\", \"\"},\n\t\t{http.MethodGet, \"/:a/:e/:id\", \"\"},\n\t}\n\ttestRouterAPI(t, api)\n\tapi2 := []testRoute{\n\t\t{http.MethodGet, \"/:a/:id\", \"\"},\n\t\t{http.MethodGet, \"/:a/:e/:id\", \"\"},\n\t\t{http.MethodGet, \"/:a/:b/:c/:id\", \"\"},\n\t}\n\ttestRouterAPI(t, api2)\n\tapi3 := []testRoute{\n\t\t{http.MethodGet, \"/:a/:b/:c/:id\", \"\"},\n\t\t{http.MethodGet, \"/:a/:e/:id\", \"\"},\n\t\t{http.MethodGet, \"/:a/:id\", \"\"},\n\t}\n\ttestRouterAPI(t, api3)\n}\n\n// Issue #1139\nfunc TestRouterMixedParams(t *testing.T) {\n\tapi := []testRoute{\n\t\t{http.MethodGet, \"/teacher/:tid/room/suggestions\", \"\"},\n\t\t{http.MethodGet, \"/teacher/:id\", \"\"},\n\t}\n\ttestRouterAPI(t, api)\n\tapi2 := []testRoute{\n\t\t{http.MethodGet, \"/teacher/:id\", \"\"},\n\t\t{http.MethodGet, \"/teacher/:tid/room/suggestions\", \"\"},\n\t}\n\ttestRouterAPI(t, api2)\n}\n\n// Issue #1466\nfunc TestRouterParam1466(t *testing.T) {\n\te := New()\n\n\te.POST(\"/users/signup\", handlerFunc)\n\te.POST(\"/users/signup/bulk\", handlerFunc)\n\te.POST(\"/users/survey\", handlerFunc)\n\n\te.GET(\"/users/:username\", handlerFunc)\n\te.GET(\"/interests/:name/users\", handlerFunc)\n\te.GET(\"/skills/:name/users\", handlerFunc)\n\t// Additional routes for Issue 1479\n\te.GET(\"/users/:username/likes/projects/ids\", handlerFunc)\n\te.GET(\"/users/:username/profile\", handlerFunc)\n\te.GET(\"/users/:username/uploads/:type\", handlerFunc)\n\n\tvar testCases = []struct {\n\t\texpectError error\n\t\texpectParam map[string]string\n\t\twhenURL     string\n\t\texpectRoute string\n\t}{\n\t\t{\n\t\t\twhenURL:     \"/users/ajitem\",\n\t\t\texpectRoute: \"/users/:username\",\n\t\t\texpectParam: map[string]string{\"username\": \"ajitem\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/sharewithme\",\n\t\t\texpectRoute: \"/users/:username\",\n\t\t\texpectParam: map[string]string{\"username\": \"sharewithme\"},\n\t\t},\n\t\t{ // route `/users/signup` is registered for POST. so param route `/users/:username` (lesser priority) is matched as it has GET handler\n\t\t\twhenURL:     \"/users/signup\",\n\t\t\texpectRoute: \"/users/:username\",\n\t\t\texpectParam: map[string]string{\"username\": \"signup\"},\n\t\t},\n\t\t// Additional assertions for #1479\n\t\t{\n\t\t\twhenURL:     \"/users/sharewithme/likes/projects/ids\",\n\t\t\texpectRoute: \"/users/:username/likes/projects/ids\",\n\t\t\texpectParam: map[string]string{\"username\": \"sharewithme\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/ajitem/likes/projects/ids\",\n\t\t\texpectRoute: \"/users/:username/likes/projects/ids\",\n\t\t\texpectParam: map[string]string{\"username\": \"ajitem\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/sharewithme/profile\",\n\t\t\texpectRoute: \"/users/:username/profile\",\n\t\t\texpectParam: map[string]string{\"username\": \"sharewithme\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/ajitem/profile\",\n\t\t\texpectRoute: \"/users/:username/profile\",\n\t\t\texpectParam: map[string]string{\"username\": \"ajitem\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/sharewithme/uploads/self\",\n\t\t\texpectRoute: \"/users/:username/uploads/:type\",\n\t\t\texpectParam: map[string]string{\n\t\t\t\t\"username\": \"sharewithme\",\n\t\t\t\t\"type\":     \"self\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/ajitem/uploads/self\",\n\t\t\texpectRoute: \"/users/:username/uploads/:type\",\n\t\t\texpectParam: map[string]string{\n\t\t\t\t\"username\": \"ajitem\",\n\t\t\t\t\"type\":     \"self\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/tree/free\",\n\t\t\texpectRoute: \"\", // not found\n\t\t\texpectParam: map[string]string{\"id\": \"\"},\n\t\t\texpectError: ErrNotFound,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenURL, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)\n\t\t\tc := e.NewContext(req, nil)\n\n\t\t\thandler := e.router.Route(c)\n\n\t\t\terr := handler(c)\n\t\t\tif tc.expectError != nil {\n\t\t\t\tassert.Equal(t, tc.expectError, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.pathValues.GetOr(param, \"\"))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\nfunc TestPathValuesSizeOverMultipleRequests(t *testing.T) {\n\te := New()\n\te.GET(\"/test/:id/:action\", handlerFunc) // max params is 2\n\n\treq := httptest.NewRequest(http.MethodGet, \"/test/1/a\", nil)\n\trec := httptest.NewRecorder()\n\n\tc := e.AcquireContext()\n\tc.Reset(req, rec)\n\tassert.Equal(t, 0, len(*c.pathValues)) // fresh context is empty\n\tassert.Equal(t, 2, cap(*c.pathValues)) // is set max path params amount\n\n\t// imitate some (pre)middleware changing/replacing pathparams to smaller size\n\tc.SetPathValues(PathValues{\n\t\t{Name: \"id\", Value: \"1\"},\n\t})\n\tassert.Equal(t, 1, len(*c.pathValues)) // as SetPathValues was provided\n\tassert.Equal(t, 2, cap(*c.pathValues)) // SetPathValues did not change that to smaller\n\n\thandler := e.router.Route(c)\n\te.ReleaseContext(c)\n\n\tassert.NoError(t, handler(c))\n\tassert.Equal(t, 2, len(*c.pathValues)) // matched route had 2 path params\n\tassert.Equal(t, 2, cap(*c.pathValues)) // was not changed\n\tassert.Equal(t, \"1\", c.Param(\"id\"))\n\tassert.Equal(t, \"a\", c.Param(\"action\"))\n}\n\n// Issue #1655\nfunc TestRouterFindNotPanicOrLoopsWhenContextSetParamValuesIsCalledWithLessValuesThanEchoMaxParam(t *testing.T) {\n\te := New()\n\n\tv0 := e.Group(\"/:version\")\n\tv0.GET(\"/admin\", func(c *Context) error {\n\t\tc.SetPathValues(PathValues{{\n\t\t\tName:  \"version\",\n\t\t\tValue: \"v1\",\n\t\t}})\n\t\treturn nil\n\t})\n\n\tv0.GET(\"/images/view/:id\", handlerHelper(\"iv\", 1))\n\tv0.GET(\"/images/:id\", handlerHelper(\"i\", 1))\n\tv0.GET(\"/view/*\", handlerHelper(\"v\", 1))\n\n\t//If this API is called before the next two one panic the other loops ( of course without my fix ;) )\n\treq := httptest.NewRequest(http.MethodGet, \"/v1/admin\", nil)\n\tc := e.NewContext(req, nil)\n\thandler := e.router.Route(c)\n\n\tassert.NoError(t, handler(c))\n\tassert.Equal(t, \"v1\", c.Param(\"version\"))\n\n\t//panic\n\treq = httptest.NewRequest(http.MethodGet, \"/v1/view/same-data\", nil)\n\tc = e.NewContext(req, nil)\n\thandler = e.router.Route(c)\n\n\tassert.NoError(t, handler(c))\n\tassert.Equal(t, \"same-data\", c.Param(\"*\"))\n\tassert.Equal(t, 1, c.Get(\"v\"))\n\n\t//looping\n\treq = httptest.NewRequest(http.MethodGet, \"/v1/images/view\", nil)\n\tc = e.NewContext(req, nil)\n\thandler = e.router.Route(c)\n\n\tassert.NoError(t, handler(c))\n\tassert.Equal(t, \"view\", c.Param(\"id\"))\n\tassert.Equal(t, 1, c.Get(\"i\"))\n}\n\n// Issue #1653\nfunc TestRouterPanicWhenParamNoRootOnlyChildsFailsFind(t *testing.T) {\n\te := New()\n\n\te.GET(\"/users/create\", handlerFunc)\n\te.GET(\"/users/:id/edit\", handlerFunc)\n\te.GET(\"/users/:id/active\", handlerFunc)\n\n\tvar testCases = []struct {\n\t\texpectError error\n\t\texpectParam map[string]string\n\t\twhenURL     string\n\t\texpectRoute string\n\t}{\n\t\t{\n\t\t\twhenURL:     \"/users/alice/edit\",\n\t\t\texpectRoute: \"/users/:id/edit\",\n\t\t\texpectParam: map[string]string{\"id\": \"alice\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/bob/active\",\n\t\t\texpectRoute: \"/users/:id/active\",\n\t\t\texpectParam: map[string]string{\"id\": \"bob\"},\n\t\t},\n\t\t{\n\t\t\twhenURL:     \"/users/create\",\n\t\t\texpectRoute: \"/users/create\",\n\t\t\texpectParam: nil,\n\t\t},\n\t\t//This panic before the fix for Issue #1653\n\t\t{\n\t\t\twhenURL:     \"/users/createNotFound\",\n\t\t\texpectError: ErrNotFound,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.whenURL, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)\n\t\t\tc := e.NewContext(req, nil)\n\n\t\t\thandler := e.router.Route(c)\n\n\t\t\terr := handler(c)\n\t\t\tif tc.expectError != nil {\n\t\t\t\tassert.Equal(t, tc.expectError, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.pathValues.GetOr(param, \"---none---\"))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\nfunc TestRouter_addEmptyPathToSlashReverse(t *testing.T) {\n\tr := NewRouter(RouterConfig{})\n\t_, err := r.Add(Route{Method: http.MethodGet, Path: \"\", Handler: handlerFunc, Name: \"empty\"})\n\tassert.NoError(t, err)\n\n\treversed, err := r.Routes().Reverse(\"empty\") // empty path is normalized to `/` internally but stays \"\" in route\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"\", reversed)\n}\n\nfunc TestRouter_ReverseNotFound(t *testing.T) {\n\tr := NewRouter(RouterConfig{})\n\t_, err := r.Add(Route{Method: http.MethodGet, Path: \"\", Handler: handlerFunc, Name: \"empty\"})\n\tassert.NoError(t, err)\n\n\treversed, err := r.Routes().Reverse(\"not-existing\")\n\n\tassert.EqualError(t, err, \"route not found\")\n\tassert.Equal(t, \"\", reversed)\n}\n\nfunc TestRoutes_ReverseHandlerName(t *testing.T) {\n\tstatic := func(*Context) error { return nil }\n\tgetUser := func(*Context) error { return nil }\n\tgetAny := func(*Context) error { return nil }\n\tgetFile := func(*Context) error { return nil }\n\n\tvar testCases = []struct {\n\t\tname      string\n\t\twhen      string\n\t\texpect    string\n\t\texpectErr string\n\t\twhenArgs  []any\n\t}{\n\t\t{\n\t\t\tname:     \"ok, HandlerName + args\",\n\t\t\twhen:     HandlerName(getFile),\n\t\t\twhenArgs: []any{\"1\"},\n\t\t\texpect:   \"/group/users/1/files/:fid\",\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, HandlerName\",\n\t\t\twhen:   HandlerName(getFile),\n\t\t\texpect: \"/group/users/:uid/files/:fid\",\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, unnamed fixed name\",\n\t\t\twhen:   \"GET:/static/file\",\n\t\t\texpect: \"/static/file\",\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, unnamed fixed name\",\n\t\t\twhen:   \"GET:/users/:id\",\n\t\t\texpect: \"/users/:id\",\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, unnamed any route\",\n\t\t\twhen:   RouteAny + \":/documents/*\",\n\t\t\texpect: \"/documents/*\",\n\t\t},\n\t\t{\n\t\t\tname:     \"ok, unnamed any route + args\",\n\t\t\twhen:     RouteAny + \":/documents/*\",\n\t\t\twhenArgs: []any{\"index.html\"},\n\t\t\texpect:   \"/documents/index.html\",\n\t\t},\n\t\t{\n\t\t\tname:     \"ok, named route + args\",\n\t\t\twhen:     HandlerName(getFile),\n\t\t\twhenArgs: []any{\"1\", \"abc\"},\n\t\t\texpect:   \"/group/users/1/files/abc\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\t\t\te.GET(\"/static/file\", static)\n\t\t\te.GET(\"/users/:id\", getUser)\n\t\t\te.Any(\"/documents/*\", getAny)\n\n\t\t\tg := e.Group(\"/group\")\n\t\t\tg.GET(\"/roles/:rid/files/:fid\", getFile)\n\t\t\tg.AddRoute(Route{Method: http.MethodGet, Path: \"/users/:uid/files/:fid\", Handler: getFile, Name: HandlerName(getFile)})\n\n\t\t\treversed, err := e.Router().Routes().Reverse(tc.when, tc.whenArgs...)\n\n\t\t\tassert.Equal(t, tc.expect, reversed)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRoutes_Reverse(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname      string\n\t\twhen      string\n\t\texpect    string\n\t\texpectErr string\n\t\twhenArgs  []any\n\t}{\n\t\t{\n\t\t\tname:   \"ok, static\",\n\t\t\twhen:   \"/static\",\n\t\t\texpect: \"/static\",\n\t\t},\n\t\t{\n\t\t\tname:     \"ok, static + args\",\n\t\t\twhen:     \"/static\",\n\t\t\twhenArgs: []any{\"missing param\"},\n\t\t\texpect:   \"/static\",\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, static/*\",\n\t\t\twhen:   \"/static/*\",\n\t\t\texpect: \"/static/*\",\n\t\t},\n\t\t{\n\t\t\tname:     \"ok, static/*\",\n\t\t\twhen:     \"/static/*\",\n\t\t\twhenArgs: []any{\"foo.txt\"},\n\t\t\texpect:   \"/static/foo.txt\",\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, /params/:foo\",\n\t\t\twhen:   \"/params/:foo\",\n\t\t\texpect: \"/params/:foo\",\n\t\t},\n\t\t{\n\t\t\tname:     \"ok, /params/:foo + args\",\n\t\t\twhen:     \"/params/:foo\",\n\t\t\twhenArgs: []any{\"one\"},\n\t\t\texpect:   \"/params/one\",\n\t\t},\n\t\t{\n\t\t\tname:   \"ok, /params/:foo/bar/:qux\",\n\t\t\twhen:   \"/params/:foo/bar/:qux\",\n\t\t\texpect: \"/params/:foo/bar/:qux\",\n\t\t},\n\t\t{\n\t\t\tname:     \"ok, /params/:foo/bar/:qux + args1\",\n\t\t\twhen:     \"/params/:foo/bar/:qux\",\n\t\t\twhenArgs: []any{\"one\"},\n\t\t\texpect:   \"/params/one/bar/:qux\",\n\t\t},\n\t\t{\n\t\t\tname:     \"ok, /params/:foo/bar/:qux + args2\",\n\t\t\twhen:     \"/params/:foo/bar/:qux\",\n\t\t\twhenArgs: []any{\"one\", \"two\"},\n\t\t\texpect:   \"/params/one/bar/two\",\n\t\t},\n\t\t{\n\t\t\tname:     \"ok, /params/:foo/bar/:qux/* + args3\",\n\t\t\twhen:     \"/params/:foo/bar/:qux/*\",\n\t\t\twhenArgs: []any{\"one\", \"two\", \"three\"},\n\t\t\texpect:   \"/params/one/bar/two/three\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tdummyHandler := func(*Context) error { return nil }\n\n\t\t\trouter := NewRouter(RouterConfig{})\n\t\t\trouter.Add(Route{Path: \"/static\", Name: \"/static\", Method: http.MethodGet, Handler: dummyHandler})\n\t\t\trouter.Add(Route{Path: \"/static/*\", Name: \"/static/*\", Method: http.MethodGet, Handler: dummyHandler})\n\t\t\trouter.Add(Route{Path: \"/params/:foo\", Name: \"/params/:foo\", Method: http.MethodGet, Handler: dummyHandler})\n\t\t\trouter.Add(Route{Path: \"/params/:foo/bar/:qux\", Name: \"/params/:foo/bar/:qux\", Method: http.MethodGet, Handler: dummyHandler})\n\t\t\trouter.Add(Route{Path: \"/params/:foo/bar/:qux/*\", Name: \"/params/:foo/bar/:qux/*\", Method: http.MethodGet, Handler: dummyHandler})\n\n\t\t\treversed, err := router.Routes().Reverse(tc.when, tc.whenArgs...)\n\t\t\tassert.Equal(t, tc.expect, reversed)\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRouter_Routes(t *testing.T) {\n\troutes := []*Route{\n\t\t{Method: http.MethodGet, Path: \"/users/:user/events\"},\n\t\t{Method: http.MethodGet, Path: \"/users/:user/events/public\"},\n\t\t{Method: http.MethodPost, Path: \"/repos/:owner/:repo/git/refs\"},\n\t\t{Method: http.MethodPost, Path: \"/repos/:owner/:repo/git/tags\"},\n\t}\n\trouter := NewRouter(RouterConfig{})\n\tfor _, r := range routes {\n\t\t_, err := router.Add(Route{\n\t\t\tMethod: r.Method,\n\t\t\tPath:   r.Path,\n\t\t\tHandler: func(c *Context) error {\n\t\t\t\treturn c.String(http.StatusOK, \"OK\")\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n\n\tassert.Equal(t, len(routes), len(router.Routes()))\n\tfor _, r := range router.Routes() {\n\t\tfound := false\n\t\tfor _, rr := range routes {\n\t\t\tif r.Method == rr.Method && r.Path == rr.Path {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\tt.Errorf(\"Route %s %s not found\", r.Method, r.Path)\n\t\t}\n\t}\n}\n\nfunc TestRouterNoRoutablePath(t *testing.T) {\n\te := New()\n\n\te.router.Add(Route{Path: \"/static\", Name: \"/static\", Method: http.MethodGet, Handler: func(*Context) error { return nil }})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/notFound\", nil)\n\tc := e.NewContext(req, nil)\n\n\te.router.Route(c)\n\t// No routable path, don't set Path.\n\tassert.Equal(t, \"\", c.Path())\n}\n\nfunc benchmarkRouterRoutes(b *testing.B, routes []testRoute, routesToFind []testRoute) {\n\te := New()\n\tr := e.router\n\treq := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\tb.ReportAllocs()\n\n\t// Add routes\n\tfor _, route := range routes {\n\t\te.AddRoute(Route{\n\t\t\tMethod: route.Method,\n\t\t\tPath:   route.Path,\n\t\t\tHandler: func(c *Context) error {\n\t\t\t\treturn nil\n\t\t\t},\n\t\t})\n\t}\n\n\t// Routes adding are performed just once, so it doesn't make sense to see that in the benchmark\n\tb.ResetTimer()\n\n\t// Find routes\n\tfor i := 0; i < b.N; i++ {\n\t\tfor _, route := range routesToFind {\n\t\t\tc := e.contextPool.Get().(*Context)\n\t\t\tc.request = req\n\n\t\t\treq.Method = route.Method\n\t\t\treq.URL.Path = route.Path\n\n\t\t\t_ = r.Route(c)\n\n\t\t\te.contextPool.Put(c)\n\t\t}\n\t}\n}\n\nfunc TestDefaultRouter_Remove(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname       string\n\t\twhenMethod string\n\t\twhenPath   string\n\t\texpectErr  string\n\t\tgivenPaths []string\n\t}{\n\t\t{\n\t\t\tname:     \"ok, static\",\n\t\t\twhenPath: \"/users\",\n\t\t},\n\t\t{\n\t\t\tname:     \"ok, static without slash\",\n\t\t\twhenPath: \"users\",\n\t\t},\n\t\t{\n\t\t\tname:     \"ok, multilevel static\",\n\t\t\twhenPath: \"/users/newsee\",\n\t\t},\n\t\t{\n\t\t\tname:     \"ok, static with path params\",\n\t\t\twhenPath: \"/users/:id/files\",\n\t\t},\n\t\t{\n\t\t\tname:     \"ok, path params\",\n\t\t\twhenPath: \"/users/:id\",\n\t\t},\n\t\t{\n\t\t\tname:     \"ok, any\",\n\t\t\twhenPath: \"/users/new/*\",\n\t\t},\n\t\t{\n\t\t\tname:     \"ok, any root\",\n\t\t\twhenPath: \"/*\",\n\t\t},\n\t\t{\n\t\t\tname:     \"ok, multilevel any\",\n\t\t\twhenPath: \"/users/dew/*\",\n\t\t},\n\t\t{\n\t\t\tname:       \"ok, single root\",\n\t\t\tgivenPaths: []string{\"/users\"},\n\t\t\twhenPath:   \"/users\",\n\t\t},\n\t\t{\n\t\t\tname:       \"nok, no routes, nothing to remove\",\n\t\t\tgivenPaths: []string{},\n\t\t\twhenPath:   \"/users/\",\n\t\t\texpectErr:  \"router has no routes to remove\",\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, route not found (matches partial node)\",\n\t\t\twhenPath:  \"/users/\",\n\t\t\texpectErr: \"could not find route to remove by given path\",\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, route not found (matches partial node)\",\n\t\t\twhenPath:  \"\",\n\t\t\texpectErr: \"could not find route to remove by given path\",\n\t\t},\n\t\t{\n\t\t\tname:      \"nok, route not found\",\n\t\t\twhenPath:  \"/this_is_not_existent\",\n\t\t\texpectErr: \"could not find route to remove by given path\",\n\t\t},\n\t\t{\n\t\t\tname:       \"nok, multilevel static but different method\",\n\t\t\twhenPath:   \"/users/newsee\",\n\t\t\twhenMethod: http.MethodPost,\n\t\t\texpectErr:  \"could not find route to remove by given path and method\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\n\t\t\tpaths := []string{\n\t\t\t\t\"/users\",\n\t\t\t\t\"/users/new\",\n\t\t\t\t\"/users/:id\",\n\t\t\t\t\"/users/dew\",\n\t\t\t\t\"/users/dew/*\",\n\t\t\t\t\"/users/:id/files\",\n\t\t\t\t\"/users/newsee\",\n\t\t\t\t\"/users/new/new\",\n\t\t\t\t\"/users/new/*\",\n\t\t\t\t\"/*\",\n\t\t\t}\n\t\t\tif tc.givenPaths != nil {\n\t\t\t\tpaths = tc.givenPaths\n\t\t\t}\n\t\t\tindex := -1\n\t\t\tfor i, p := range paths {\n\t\t\t\te.GET(p, handlerFunc)\n\t\t\t\tif p == tc.whenPath {\n\t\t\t\t\tindex = i\n\t\t\t\t}\n\t\t\t}\n\t\t\tvar toCheckPaths []string\n\t\t\tif index != -1 {\n\t\t\t\ttoCheckPaths = append(paths[:index], paths[index+1:]...)\n\t\t\t}\n\n\t\t\tmethod := tc.whenMethod\n\t\t\tif method == \"\" {\n\t\t\t\tmethod = http.MethodGet\n\t\t\t}\n\n\t\t\terr := e.Router().Remove(method, tc.whenPath)\n\n\t\t\tif tc.expectErr != \"\" {\n\t\t\t\tassert.EqualError(t, err, tc.expectErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\n\t\t\tfor _, p := range toCheckPaths {\n\t\t\t\treq := httptest.NewRequest(http.MethodGet, p, nil)\n\t\t\t\tc := e.NewContext(req, nil)\n\t\t\t\t_ = e.Router().Route(c)\n\t\t\t\tassert.Equal(t, p, c.Path(), \"after removing %v we matched wrong route. when matching: %v, got: %v\", tc.whenPath, p, c.Path())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDefaultRouter_AddWithoutHandler(t *testing.T) {\n\trouter := NewRouter(RouterConfig{})\n\n\tri, err := router.Add(Route{Method: http.MethodGet, Path: \"/info\", Handler: nil})\n\tassert.EqualError(t, err, \"GET /info: adding route without handler function\")\n\tassert.Equal(t, RouteInfo{}, ri)\n}\n\nfunc TestDefaultRouter_AddDuplicateRouteNotAllowed(t *testing.T) {\n\te := New()\n\trouter := NewRouter(RouterConfig{})\n\te.router = router\n\n\tri, err := router.Add(Route{\n\t\tMethod: http.MethodGet,\n\t\tPath:   \"/info\",\n\t\tHandler: func(c *Context) error {\n\t\t\treturn c.String(http.StatusTeapot, \"OLD\")\n\t\t},\n\t\tName: \"old\",\n\t})\n\tassert.NoError(t, err)\n\tassert.NotNil(t, ri)\n\n\tri, err = router.Add(Route{Method: http.MethodGet, Path: \"/info\", Handler: handlerFunc, Name: \"new\"})\n\tassert.Equal(t, RouteInfo{}, ri)\n\tassert.EqualError(t, err, \"GET /info: adding duplicate route (same method+path) is not allowed\")\n\tassert.Len(t, router.Routes(), 1)\n\n\tstatus, body := request(http.MethodGet, \"/info\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, \"OLD\", body)\n}\n\n// See issue #1531, #1258 - there are cases when path parameter need to be unescaped\nfunc TestDefaultRouter_UnescapePathParamValues(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname                         string\n\t\twhenURL                      string\n\t\texpectPath                   string\n\t\texpectPathValues             PathValues\n\t\tgivenUnescapePathParamValues bool\n\t}{\n\t\t{\n\t\t\tname:                         \"ok, unescape = true\",\n\t\t\tgivenUnescapePathParamValues: true,\n\t\t\twhenURL:                      \"/first/value%20with%20space\",\n\t\t\texpectPath:                   \"/first/:raw\",\n\t\t\texpectPathValues: PathValues{\n\t\t\t\t{\n\t\t\t\t\tName:  \"raw\",\n\t\t\t\t\tValue: \"value with space\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:                         \"ok, multiple, unescape = true\",\n\t\t\tgivenUnescapePathParamValues: true,\n\t\t\twhenURL:                      \"/second/%20/with%20space\",\n\t\t\texpectPath:                   \"/second/:id/:fileName\",\n\t\t\texpectPathValues: PathValues{\n\t\t\t\t{Name: \"id\", Value: \" \"},\n\t\t\t\t{Name: \"fileName\", Value: \"with space\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:                         \"ok, any route, unescape = true\",\n\t\t\tgivenUnescapePathParamValues: true,\n\t\t\twhenURL:                      \"/third/%20%2Fwith%20space\",\n\t\t\texpectPath:                   \"/third/*\",\n\t\t\texpectPathValues: PathValues{\n\t\t\t\t{Name: \"*\", Value: \" /with space\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:                         \"ok, ending with static node, unescape = true\",\n\t\t\tgivenUnescapePathParamValues: true,\n\t\t\twhenURL:                      \"/fourth/%20%2Fwith%20space/static\",\n\t\t\texpectPath:                   \"/fourth/:id/static\",\n\t\t\texpectPathValues: PathValues{\n\t\t\t\t{Name: \"id\", Value: \" /with space\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:                         \"ok, unescape = false\",\n\t\t\tgivenUnescapePathParamValues: false,\n\t\t\twhenURL:                      \"/first/value%20with%20space\",\n\t\t\texpectPath:                   \"/first/:raw\",\n\t\t\texpectPathValues: PathValues{\n\t\t\t\t{\n\t\t\t\t\tName:  \"raw\",\n\t\t\t\t\tValue: \"value%20with%20space\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\t\t\trouter := NewRouter(RouterConfig{UnescapePathParamValues: tc.givenUnescapePathParamValues})\n\t\t\te.router = router\n\t\t\te.contextPathParamAllocSize.Store(2)\n\n\t\t\t_, err := router.Add(Route{Method: http.MethodGet, Path: \"/first/:raw\", Handler: handlerFunc})\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = router.Add(Route{Method: http.MethodGet, Path: \"/second/:id/:fileName\", Handler: handlerFunc})\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = router.Add(Route{Method: http.MethodGet, Path: \"/third/*\", Handler: handlerFunc})\n\t\t\tassert.NoError(t, err)\n\t\t\t_, err = router.Add(Route{Method: http.MethodGet, Path: \"/fourth/:id/static\", Handler: handlerFunc})\n\t\t\tassert.NoError(t, err)\n\n\t\t\ttarget, _ := url.Parse(tc.whenURL)\n\t\t\treq := httptest.NewRequest(http.MethodGet, target.String(), nil)\n\t\t\treq.URL.RawPath = tc.whenURL\n\n\t\t\tc := e.NewContext(req, nil)\n\t\t\t_ = e.Router().Route(c)\n\n\t\t\tassert.Equal(t, tc.expectPath, c.Path())\n\t\t\tassert.Equal(t, tc.expectPathValues, c.PathValues())\n\t\t})\n\t}\n}\n\nfunc TestDefaultRouter_AddDuplicateRouteAllowed(t *testing.T) {\n\te := New()\n\trouter := NewRouter(RouterConfig{AllowOverwritingRoute: true})\n\te.router = router\n\n\tri, err := router.Add(Route{Method: http.MethodGet, Path: \"/info\", Handler: handlerFunc, Name: \"old\"})\n\tassert.NoError(t, err)\n\tassert.Equal(t, ri, RouteInfo{\n\t\tMethod: http.MethodGet,\n\t\tPath:   \"/info\",\n\t\tName:   \"old\",\n\t})\n\n\tri, err = router.Add(Route{\n\t\tMethod: http.MethodGet,\n\t\tPath:   \"/info\",\n\t\tHandler: func(c *Context) error {\n\t\t\treturn c.String(http.StatusTeapot, \"NEW\")\n\t\t},\n\t\tName: \"new\",\n\t})\n\tassert.NoError(t, err)\n\tassert.Equal(t, ri, RouteInfo{\n\t\tMethod: http.MethodGet,\n\t\tPath:   \"/info\",\n\t\tName:   \"new\",\n\t})\n\n\troutes := router.Routes()\n\tassert.Len(t, routes, 1)\n\tassert.Equal(t, \"new\", routes[0].Name)\n\n\tstatus, body := request(http.MethodGet, \"/info\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, \"NEW\", body)\n}\n\nfunc TestDefaultRouter_UseEscapedPathForRouting(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname                               string\n\t\twhenRawPath                        string\n\t\texpectBody                         string\n\t\texpectStatus                       int\n\t\tgivenDoNotUseEscapedPathForRouting bool\n\t}{\n\t\t{\n\t\t\tname:         \"ok, static route\",\n\t\t\twhenRawPath:  \"/what's%20up\",\n\t\t\texpectBody:   \"/what's up\",\n\t\t\texpectStatus: http.StatusTeapot,\n\t\t},\n\t\t{\n\t\t\tname:                               \"nok, rawPath does not match with route path\",\n\t\t\tgivenDoNotUseEscapedPathForRouting: true,\n\t\t\twhenRawPath:                        \"/what's%20up\", // route is \"/what's up\"\n\t\t\texpectBody:                         \"{\\\"message\\\":\\\"Not Found\\\"}\\n\",\n\t\t\texpectStatus:                       http.StatusNotFound,\n\t\t},\n\t\t{\n\t\t\tname:         \"ok, match path param\",\n\t\t\twhenRawPath:  \"/test/what's%20up\",\n\t\t\texpectBody:   \"/test/:param|what's up\",\n\t\t\texpectStatus: http.StatusTeapot,\n\t\t},\n\t\t{\n\t\t\tname:                               \"ok, path param is unescaped as it is in rawPath\",\n\t\t\tgivenDoNotUseEscapedPathForRouting: true,\n\t\t\twhenRawPath:                        \"/test/what's%20up\",\n\t\t\texpectBody:                         \"/test/:param|what's%20up\",\n\t\t\texpectStatus:                       http.StatusTeapot,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\t\t\trouter := NewRouter(RouterConfig{UseEscapedPathForMatching: !tc.givenDoNotUseEscapedPathForRouting})\n\t\t\te.router = router\n\t\t\te.contextPathParamAllocSize.Store(1)\n\n\t\t\tri, err := router.Add(Route{\n\t\t\t\tMethod: http.MethodGet,\n\t\t\t\tPath:   \"/what's up\",\n\t\t\t\tHandler: func(c *Context) error {\n\t\t\t\t\treturn c.String(http.StatusTeapot, c.RouteInfo().Path)\n\t\t\t\t},\n\t\t\t})\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NotNil(t, ri)\n\t\t\tri, err = router.Add(Route{\n\t\t\t\tMethod: http.MethodGet,\n\t\t\t\tPath:   \"/test/:param\",\n\t\t\t\tHandler: func(c *Context) error {\n\t\t\t\t\treturn c.String(http.StatusTeapot, c.RouteInfo().Path+\"|\"+c.Param(\"param\"))\n\t\t\t\t},\n\t\t\t})\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NotNil(t, ri)\n\n\t\t\tstatus, body := request(http.MethodGet, tc.whenRawPath, e)\n\t\t\tassert.Equal(t, tc.expectStatus, status)\n\t\t\tassert.Equal(t, tc.expectBody, body)\n\t\t})\n\t}\n}\n\nfunc TestDefaultRouter_NotFoundHandler(t *testing.T) {\n\te := New()\n\trouter := NewRouter(RouterConfig{\n\t\tNotFoundHandler: func(c *Context) error {\n\t\t\treturn c.String(http.StatusTeapot, \"404\")\n\t\t},\n\t})\n\te.router = router\n\te.GET(\"/test\", func(c *Context) error {\n\t\treturn c.String(http.StatusOK, \"OK\")\n\t})\n\n\tstatus, body := request(http.MethodGet, \"/noFound\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, \"404\", body)\n}\n\nfunc TestDefaultRouter_MethodNotAllowedHandler(t *testing.T) {\n\te := New()\n\trouter := NewRouter(RouterConfig{\n\t\tMethodNotAllowedHandler: func(c *Context) error {\n\t\t\treturn c.String(http.StatusTeapot, \"405\")\n\t\t},\n\t})\n\te.router = router\n\te.GET(\"/test\", func(c *Context) error {\n\t\treturn c.String(http.StatusOK, \"OK\")\n\t})\n\n\tstatus, body := request(http.MethodPost, \"/test\", e)\n\tassert.Equal(t, http.StatusTeapot, status)\n\tassert.Equal(t, \"405\", body)\n}\n\nfunc TestDefaultRouter_OptionsMethodHandler(t *testing.T) {\n\te := New()\n\trouter := NewRouter(RouterConfig{\n\t\tOptionsMethodHandler: func(c *Context) error {\n\t\t\treturn c.String(http.StatusBadRequest, \"not empty\")\n\t\t},\n\t})\n\te.router = router\n\te.GET(\"/test\", func(c *Context) error {\n\t\treturn c.String(http.StatusOK, \"OK\")\n\t})\n\n\tstatus, body := request(http.MethodOptions, \"/test\", e)\n\tassert.Equal(t, http.StatusBadRequest, status)\n\tassert.Equal(t, \"not empty\", body)\n}\n\nfunc TestRouter_RouteWhenNotFoundRouteWithNodeSplitting(t *testing.T) {\n\te := New()\n\tr := e.router\n\n\thf := func(c *Context) error {\n\t\treturn c.String(http.StatusOK, c.RouteInfo().Name)\n\t}\n\tr.Add(Route{Method: http.MethodGet, Path: \"/test*\", Handler: hf, Name: \"0\"})\n\tr.Add(Route{Method: RouteNotFound, Path: \"/test*\", Handler: hf, Name: \"1\"})\n\tr.Add(Route{Method: RouteNotFound, Path: \"/test\", Handler: hf, Name: \"2\"})\n\n\t// Tree before:\n\t// 1    `/`\n\t// 1.1      `*` (any) ID=1\n\t// 1.2      `test` (static) ID=2\n\t// 1.2.1        `*` (any) ID=0\n\n\t// node with path `test` has routeNotFound handler from previous Add call. Now when we insert `/te/st*` into router tree\n\t// This means that node `test` is split into `te` and `st` nodes and new node `/st*` is inserted.\n\t// On that split `/test` routeNotFound handler must not be lost.\n\tr.Add(Route{Method: http.MethodGet, Path: \"/te/st*\", Handler: hf, Name: \"3\"})\n\t// Tree after:\n\t// 1    `/`\n\t// 1.1      `*` (any) ID=1\n\t// 1.2      `te` (static)\n\t// 1.2.1        `st` (static) ID=2\n\t// 1.2.1.1          `*` (any) ID=0\n\t// 1.2.2        `/st` (static)\n\t// 1.2.2.1          `*` (any) ID=3\n\n\t_, body := request(http.MethodPut, \"/test\", e)\n\n\tassert.Equal(t, \"2\", body)\n}\n\nfunc TestRouter_RouteWhenNotFoundRouteAnyKind(t *testing.T) {\n\tvar testCases = []struct {\n\t\texpectRoute any\n\t\texpectParam map[string]string\n\t\tname        string\n\t\twhenURL     string\n\t\texpectID    int\n\t}{\n\t\t{\n\t\t\tname:        \"route not existent /xx to not found handler /*\",\n\t\t\twhenURL:     \"/xx\",\n\t\t\texpectRoute: \"/*\",\n\t\t\texpectID:    4,\n\t\t\texpectParam: map[string]string{\"*\": \"xx\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"route not existent /a/xx to not found handler /a/*\",\n\t\t\twhenURL:     \"/a/xx\",\n\t\t\texpectRoute: \"/a/*\",\n\t\t\texpectID:    5,\n\t\t\texpectParam: map[string]string{\"*\": \"xx\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"route not existent /a/c/dxxx to not found handler /a/c/d*\",\n\t\t\twhenURL:     \"/a/c/dxxx\",\n\t\t\texpectRoute: \"/a/c/d*\",\n\t\t\texpectID:    6,\n\t\t\texpectParam: map[string]string{\"*\": \"xxx\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"route /a/c/df to /a/c/df\",\n\t\t\twhenURL:     \"/a/c/df\",\n\t\t\texpectRoute: \"/a/c/df\",\n\t\t\texpectID:    1,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\t\t\te.contextPathParamAllocSize.Store(1)\n\t\t\tr := e.router\n\n\t\t\tr.Add(Route{Method: http.MethodGet, Path: \"/\", Handler: handlerHelper(\"ID\", 0), Name: \"0\"})\n\t\t\tr.Add(Route{Method: http.MethodGet, Path: \"/a/c/df\", Handler: handlerHelper(\"ID\", 1), Name: \"1\"})\n\t\t\tr.Add(Route{Method: http.MethodGet, Path: \"/a/b*\", Handler: handlerHelper(\"ID\", 2), Name: \"2\"})\n\t\t\tr.Add(Route{Method: http.MethodPut, Path: \"/*\", Handler: handlerHelper(\"ID\", 3), Name: \"3\"})\n\n\t\t\tr.Add(Route{Method: RouteNotFound, Path: \"/a/c/d*\", Handler: handlerHelper(\"ID\", 6), Name: \"6\"})\n\t\t\tr.Add(Route{Method: RouteNotFound, Path: \"/a/*\", Handler: handlerHelper(\"ID\", 5), Name: \"5\"})\n\t\t\tr.Add(Route{Method: RouteNotFound, Path: \"/*\", Handler: handlerHelper(\"ID\", 4), Name: \"4\"})\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)\n\t\t\tc := e.NewContext(req, nil)\n\n\t\t\thandler := r.Route(c)\n\t\t\thandler(c)\n\n\t\t\ttestValue, _ := c.Get(\"ID\").(int)\n\t\t\tassert.Equal(t, tc.expectID, testValue)\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.Param(param))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\nfunc TestRouter_RouteWhenNotFoundRouteParamKind(t *testing.T) {\n\tvar testCases = []struct {\n\t\texpectRoute any\n\t\texpectParam map[string]string\n\t\tname        string\n\t\twhenURL     string\n\t\texpectID    int\n\t}{\n\t\t{\n\t\t\tname:        \"route not existent /xx to not found handler /:file\",\n\t\t\twhenURL:     \"/xx\",\n\t\t\texpectRoute: \"/:file\",\n\t\t\texpectID:    4,\n\t\t\texpectParam: map[string]string{\"file\": \"xx\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"route not existent /a/xx to not found handler /a/:file\",\n\t\t\twhenURL:     \"/a/xx\",\n\t\t\texpectRoute: \"/a/:file\",\n\t\t\texpectID:    5,\n\t\t\texpectParam: map[string]string{\"file\": \"xx\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"route not existent /a/c/dxxx to not found handler /a/c/d:file\",\n\t\t\twhenURL:     \"/a/c/dxxx\",\n\t\t\texpectRoute: \"/a/c/d:file\",\n\t\t\texpectID:    6,\n\t\t\texpectParam: map[string]string{\"file\": \"xxx\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"route /a/c/df to /a/c/df\",\n\t\t\twhenURL:     \"/a/c/df\",\n\t\t\texpectRoute: \"/a/c/df\",\n\t\t\texpectID:    1,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\t\t\te.contextPathParamAllocSize.Store(1)\n\t\t\tr := e.router\n\n\t\t\tr.Add(Route{Method: http.MethodGet, Path: \"/\", Handler: handlerHelper(\"ID\", 0), Name: \"0\"})\n\t\t\tr.Add(Route{Method: http.MethodGet, Path: \"/a/c/df\", Handler: handlerHelper(\"ID\", 1), Name: \"1\"})\n\t\t\tr.Add(Route{Method: http.MethodGet, Path: \"/a/b*\", Handler: handlerHelper(\"ID\", 2), Name: \"2\"})\n\t\t\tr.Add(Route{Method: http.MethodPut, Path: \"/*\", Handler: handlerHelper(\"ID\", 3), Name: \"3\"})\n\n\t\t\tr.Add(Route{Method: RouteNotFound, Path: \"/a/c/d:file\", Handler: handlerHelper(\"ID\", 6), Name: \"6\"})\n\t\t\tr.Add(Route{Method: RouteNotFound, Path: \"/a/:file\", Handler: handlerHelper(\"ID\", 5), Name: \"5\"})\n\t\t\tr.Add(Route{Method: RouteNotFound, Path: \"/:file\", Handler: handlerHelper(\"ID\", 4), Name: \"4\"})\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)\n\t\t\tc := e.NewContext(req, nil)\n\n\t\t\thandler := r.Route(c)\n\t\t\thandler(c)\n\n\t\t\ttestValue, _ := c.Get(\"ID\").(int)\n\t\t\tassert.Equal(t, tc.expectID, testValue)\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.Param(param))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\nfunc TestRouter_RouteWhenNotFoundRouteStaticKind(t *testing.T) {\n\t// note: static not found handler is quite silly thing to have but we still support it\n\tvar testCases = []struct {\n\t\texpectRoute any\n\t\texpectParam map[string]string\n\t\tname        string\n\t\twhenURL     string\n\t\texpectID    int\n\t}{\n\t\t{\n\t\t\tname:        \"route not existent / to not found handler /\",\n\t\t\twhenURL:     \"/\",\n\t\t\texpectRoute: \"/\",\n\t\t\texpectID:    3,\n\t\t\texpectParam: map[string]string{},\n\t\t},\n\t\t{\n\t\t\tname:        \"route /a to /a\",\n\t\t\twhenURL:     \"/a\",\n\t\t\texpectRoute: \"/a\",\n\t\t\texpectID:    1,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\t\t\te.contextPathParamAllocSize.Store(1)\n\t\t\tr := e.router\n\n\t\t\tr.Add(Route{Method: http.MethodPut, Path: \"/\", Handler: handlerHelper(\"ID\", 0), Name: \"0\"})\n\t\t\tr.Add(Route{Method: http.MethodGet, Path: \"/a\", Handler: handlerHelper(\"ID\", 1), Name: \"1\"})\n\t\t\tr.Add(Route{Method: http.MethodPut, Path: \"/*\", Handler: handlerHelper(\"ID\", 2), Name: \"2\"})\n\n\t\t\tr.Add(Route{Method: RouteNotFound, Path: \"/\", Handler: handlerHelper(\"ID\", 3), Name: \"3\"})\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)\n\t\t\tc := e.NewContext(req, nil)\n\n\t\t\thandler := r.Route(c)\n\t\t\thandler(c)\n\n\t\t\ttestValue, _ := c.Get(\"ID\").(int)\n\t\t\tassert.Equal(t, tc.expectID, testValue)\n\t\t\tassert.Equal(t, tc.expectRoute, c.Path())\n\t\t\tfor param, expectedValue := range tc.expectParam {\n\t\t\t\tassert.Equal(t, expectedValue, c.Param(param))\n\t\t\t}\n\t\t\tcheckUnusedParamValues(t, c, tc.expectParam)\n\t\t})\n\t}\n}\n\nfunc TestPathValues_Get(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname     string\n\t\twhenKey  string\n\t\texpect   string\n\t\texpectOK bool\n\t}{\n\t\t{\n\t\t\tname:     \"ok\",\n\t\t\twhenKey:  `tag`,\n\t\t\texpect:   `latest`,\n\t\t\texpectOK: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"nok\",\n\t\t\twhenKey:  `not existent key`,\n\t\t\texpect:   \"\",\n\t\t\texpectOK: false,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tpv := PathValues{\n\t\t\t\t{Name: \"image\", Value: \"docker\"},\n\t\t\t\t{Name: \"tag\", Value: \"latest\"},\n\t\t\t}\n\n\t\t\tresult, ok := pv.Get(tc.whenKey)\n\t\t\tassert.Equal(t, tc.expectOK, ok)\n\t\t\tassert.Equal(t, tc.expect, result)\n\t\t})\n\t}\n}\n\nfunc TestPathValues_GetOr(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname             string\n\t\twhenKey          string\n\t\twhenDefaultValue string\n\t\texpect           string\n\t}{\n\t\t{\n\t\t\tname:             \"ok, existing key\",\n\t\t\twhenKey:          `tag`,\n\t\t\twhenDefaultValue: `tag`,\n\t\t\texpect:           `latest`,\n\t\t},\n\t\t{\n\t\t\tname:             \"nok, no existing key return default value\",\n\t\t\twhenKey:          `not existent key`,\n\t\t\twhenDefaultValue: `ok`,\n\t\t\texpect:           \"ok\",\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tpv := PathValues{\n\t\t\t\t{Name: \"image\", Value: \"docker\"},\n\t\t\t\t{Name: \"tag\", Value: \"latest\"},\n\t\t\t}\n\n\t\t\tresult := pv.GetOr(tc.whenKey, tc.whenDefaultValue)\n\t\t\tassert.Equal(t, tc.expect, result)\n\t\t})\n\t}\n}\n\nfunc BenchmarkRouterStaticRoutes(b *testing.B) {\n\tbenchmarkRouterRoutes(b, staticRoutes, staticRoutes)\n}\n\nfunc BenchmarkRouterStaticRoutesMisses(b *testing.B) {\n\tbenchmarkRouterRoutes(b, staticRoutes, missesAPI)\n}\n\nfunc BenchmarkRouterGitHubAPI(b *testing.B) {\n\tbenchmarkRouterRoutes(b, gitHubAPI, gitHubAPI)\n}\n\nfunc BenchmarkRouterGitHubAPIMisses(b *testing.B) {\n\tbenchmarkRouterRoutes(b, gitHubAPI, missesAPI)\n}\n\nfunc BenchmarkRouterParseAPI(b *testing.B) {\n\tbenchmarkRouterRoutes(b, parseAPI, parseAPI)\n}\n\nfunc BenchmarkRouterParseAPIMisses(b *testing.B) {\n\tbenchmarkRouterRoutes(b, parseAPI, missesAPI)\n}\n\nfunc BenchmarkRouterGooglePlusAPI(b *testing.B) {\n\tbenchmarkRouterRoutes(b, googlePlusAPI, googlePlusAPI)\n}\n\nfunc BenchmarkRouterGooglePlusAPIMisses(b *testing.B) {\n\tbenchmarkRouterRoutes(b, googlePlusAPI, missesAPI)\n}\n\nfunc BenchmarkRouterParamsAndAnyAPI(b *testing.B) {\n\tbenchmarkRouterRoutes(b, paramAndAnyAPI, paramAndAnyAPIToFind)\n}\n"
  },
  {
    "path": "server.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\tstdContext \"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n)\n\nconst (\n\tbanner = \"Echo (v%s). High performance, minimalist Go web framework https://echo.labstack.com\"\n)\n\n// StartConfig is for creating configured http.Server instance to start serve http(s) requests with given Echo instance\ntype StartConfig struct {\n\t// Address specifies the address where listener will start listening on to serve HTTP(s) requests\n\tAddress string\n\n\t// HideBanner instructs Start* method not to print banner when starting the Server.\n\tHideBanner bool\n\t// HidePort instructs Start* method not to print port when starting the Server.\n\tHidePort bool\n\n\t// CertFilesystem is filesystem is used to read `certFile` and `keyFile` when StartTLS method is called.\n\tCertFilesystem fs.FS\n\n\t// TLSConfig is used to configure TLS. If Listener is set, TLSConfig is not used to create the listener.\n\tTLSConfig *tls.Config\n\n\t// Listener is used to start server with the custom listener.\n\tListener net.Listener\n\t// ListenerNetwork is used configure on which Network listener will use.\n\t// If Listener is set, ListenerNetwork is not used.\n\tListenerNetwork string\n\t// ListenerAddrFunc will be called after listener is created and started to listen for connections. This is useful in\n\t// testing situations when server is started on random port `address = \":0\"` in that case you can get actual port where\n\t// listener is listening on.\n\tListenerAddrFunc func(addr net.Addr)\n\n\t// GracefulTimeout is timeout value (defaults to 10sec) graceful shutdown will wait for server to handle ongoing requests\n\t// before shutting down the server.\n\tGracefulTimeout time.Duration\n\t// OnShutdownError is called when graceful shutdown results an error. for example when listeners are not shut down within\n\t// given timeout\n\tOnShutdownError func(err error)\n\n\t// BeforeServeFunc is callback that is called just before server starts to serve HTTP request.\n\t// Use this callback when you want to configure http.Server different timeouts/limits/etc\n\tBeforeServeFunc func(s *http.Server) error\n}\n\n// Start starts given Handler with HTTP(s) server.\nfunc (sc StartConfig) Start(ctx stdContext.Context, h http.Handler) error {\n\treturn sc.start(ctx, h)\n}\n\n// StartTLS starts given Handler with HTTPS server.\n// If `certFile` or `keyFile` is `string` the values are treated as file paths.\n// If `certFile` or `keyFile` is `[]byte` the values are treated as the certificate or key as-is.\nfunc (sc StartConfig) StartTLS(ctx stdContext.Context, h http.Handler, certFile, keyFile any) error {\n\tcertFs := sc.CertFilesystem\n\tif certFs == nil {\n\t\tcertFs = os.DirFS(\".\")\n\t}\n\tcert, err := filepathOrContent(certFile, certFs)\n\tif err != nil {\n\t\treturn err\n\t}\n\tkey, err := filepathOrContent(keyFile, certFs)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcer, err := tls.X509KeyPair(cert, key)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif sc.TLSConfig == nil {\n\t\tsc.TLSConfig = &tls.Config{\n\t\t\tMinVersion: tls.VersionTLS12,\n\t\t\tNextProtos: []string{\"h2\"},\n\t\t\t//NextProtos: []string{\"http/1.1\"}, // Disallow \"h2\", allow http\n\t\t}\n\t}\n\tsc.TLSConfig.Certificates = []tls.Certificate{cer}\n\treturn sc.start(ctx, h)\n}\n\n// start starts handler with HTTP(s) server.\nfunc (sc StartConfig) start(ctx stdContext.Context, h http.Handler) error {\n\tvar logger *slog.Logger\n\tif e, ok := h.(*Echo); ok {\n\t\tlogger = e.Logger\n\t} else {\n\t\tlogger = slog.New(slog.NewJSONHandler(os.Stdout, nil))\n\t}\n\n\tserver := http.Server{\n\t\tHandler:  h,\n\t\tErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),\n\t\t// defaults for GoSec rule G112 // https://github.com/securego/gosec\n\t\t// G112 (CWE-400): Potential Slowloris Attack because ReadHeaderTimeout is not configured in the http.Server\n\t\tReadTimeout:  30 * time.Second,\n\t\tWriteTimeout: 30 * time.Second,\n\t}\n\n\tlistener := sc.Listener\n\tif listener == nil {\n\t\tlistenerNetwork := sc.ListenerNetwork\n\t\tif listenerNetwork == \"\" {\n\t\t\tlistenerNetwork = \"tcp\"\n\t\t}\n\t\tvar err error\n\t\tif sc.TLSConfig != nil {\n\t\t\tlistener, err = tls.Listen(listenerNetwork, sc.Address, sc.TLSConfig)\n\t\t} else {\n\t\t\tlistener, err = net.Listen(listenerNetwork, sc.Address)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif sc.ListenerAddrFunc != nil {\n\t\tsc.ListenerAddrFunc(listener.Addr())\n\t}\n\n\tif sc.BeforeServeFunc != nil {\n\t\tif err := sc.BeforeServeFunc(&server); err != nil {\n\t\t\t_ = listener.Close()\n\t\t\treturn err\n\t\t}\n\t}\n\tif !sc.HideBanner {\n\t\tbannerText := fmt.Sprintf(banner, Version)\n\t\tlogger.Info(bannerText, \"version\", Version)\n\t}\n\tif !sc.HidePort {\n\t\tlogger.Info(\"http(s) server started\", \"address\", listener.Addr().String())\n\t}\n\n\twg := sync.WaitGroup{}\n\tdefer wg.Wait() // wait for graceful shutdown goroutine to finish\n\n\tgCtx, cancel := stdContext.WithCancel(ctx) // end graceful goroutine when Serve returns early\n\tdefer cancel()\n\n\tif sc.GracefulTimeout >= 0 {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tgracefulShutdown(gCtx, &sc, &server, logger)\n\t\t}()\n\t}\n\n\tif err := server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc filepathOrContent(fileOrContent any, certFilesystem fs.FS) (content []byte, err error) {\n\tswitch v := fileOrContent.(type) {\n\tcase string:\n\t\treturn fs.ReadFile(certFilesystem, v)\n\tcase []byte:\n\t\treturn v, nil\n\tdefault:\n\t\treturn nil, ErrInvalidCertOrKeyType\n\t}\n}\n\nfunc gracefulShutdown(shutdownCtx stdContext.Context, sc *StartConfig, server *http.Server, logger *slog.Logger) {\n\t<-shutdownCtx.Done() // wait until shutdown context is closed.\n\t// note: is server if closed by other means this method is still run but is good as no-op\n\n\ttimeout := sc.GracefulTimeout\n\tif timeout == 0 {\n\t\ttimeout = 10 * time.Second\n\t}\n\twaitShutdownCtx, cancel := stdContext.WithTimeout(stdContext.Background(), timeout)\n\tdefer cancel()\n\n\tif err := server.Shutdown(waitShutdownCtx); err != nil {\n\t\t// we end up here when listeners are not shut down within given timeout\n\t\tif sc.OnShutdownError != nil {\n\t\t\tsc.OnShutdownError(err)\n\t\t\treturn\n\t\t}\n\t\tlogger.Error(\"failed to shut down server within given timeout\", \"error\", err)\n\t}\n}\n"
  },
  {
    "path": "server_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"bytes\"\n\tstdContext \"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/net/http2\"\n)\n\nfunc startOnRandomPort(ctx stdContext.Context, e *Echo) (string, error) {\n\taddrChan := make(chan string)\n\terrCh := make(chan error)\n\n\tgo func() {\n\t\terrCh <- (&StartConfig{\n\t\t\tAddress:         \":0\",\n\t\t\tGracefulTimeout: 100 * time.Millisecond,\n\t\t\tListenerAddrFunc: func(addr net.Addr) {\n\t\t\t\taddrChan <- addr.String()\n\t\t\t},\n\t\t}).Start(ctx, e)\n\t}()\n\n\treturn waitForServerStart(addrChan, errCh)\n}\n\nfunc waitForServerStart(addrChan <-chan string, errCh <-chan error) (string, error) {\n\twaitCtx, cancel := stdContext.WithTimeout(stdContext.Background(), 200*time.Millisecond)\n\tdefer cancel()\n\n\t// wait for addr to arrive\n\tfor {\n\t\tselect {\n\t\tcase <-waitCtx.Done():\n\t\t\treturn \"\", waitCtx.Err()\n\t\tcase addr := <-addrChan:\n\t\t\treturn addr, nil\n\t\tcase err := <-errCh:\n\t\t\tif err == http.ErrServerClosed { // was closed normally before listener callback was called. should not be possible\n\t\t\t\treturn \"\", nil\n\t\t\t}\n\t\t\t// failed to start and we did not manage to get even listener part.\n\t\t\treturn \"\", err\n\t\t}\n\t}\n}\n\nfunc doGet(url string) (int, string, error) {\n\tresp, err := http.Get(url)\n\tif err != nil {\n\t\treturn 0, \"\", err\n\t}\n\n\tdefer resp.Body.Close()\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn resp.StatusCode, \"\", err\n\t}\n\treturn resp.StatusCode, string(body), nil\n}\n\nfunc TestStartConfig_Start(t *testing.T) {\n\te := New()\n\te.GET(\"/ok\", func(c *Context) error {\n\t\treturn c.String(http.StatusOK, \"OK\")\n\t})\n\n\taddrChan := make(chan string)\n\terrCh := make(chan error)\n\n\tctx, shutdown := stdContext.WithTimeout(stdContext.Background(), 200*time.Millisecond)\n\tdefer shutdown()\n\tgo func() {\n\t\terrCh <- (&StartConfig{\n\t\t\tAddress: \":0\",\n\t\t\tListenerAddrFunc: func(addr net.Addr) {\n\t\t\t\taddrChan <- addr.String()\n\t\t\t},\n\t\t}).Start(ctx, e)\n\t}()\n\n\taddr, err := waitForServerStart(addrChan, errCh)\n\tassert.NoError(t, err)\n\n\t// check if server is actually up\n\tcode, body, err := doGet(fmt.Sprintf(\"http://%v/ok\", addr))\n\tif err != nil {\n\t\tassert.NoError(t, err)\n\t\treturn\n\t}\n\tassert.Equal(t, http.StatusOK, code)\n\tassert.Equal(t, \"OK\", body)\n\n\tshutdown()\n\n\t<-errCh // we will be blocking here until server returns from http.Serve\n\n\t// check if server was stopped\n\tcode, body, err = doGet(fmt.Sprintf(\"http://%v/ok\", addr))\n\tassert.Equal(t, 0, code)\n\tassert.Equal(t, \"\", body)\n\n\tif err == nil {\n\t\tt.Errorf(\"missing error\")\n\t\treturn\n\t}\n\texpectContains := \"connect: connection refused\"\n\tif runtime.GOOS == \"windows\" {\n\t\texpectContains = \"No connection could be made\"\n\t}\n\tassert.True(t, strings.Contains(err.Error(), expectContains))\n}\n\nfunc TestStartConfig_GracefulShutdown(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname                   string\n\t\texpectBody             string\n\t\texpectGracefulError    string\n\t\twhenHandlerTakesLonger bool\n\t}{\n\t\t{\n\t\t\tname:                   \"ok, all handlers returns before graceful shutdown deadline\",\n\t\t\twhenHandlerTakesLonger: false,\n\t\t\texpectBody:             \"OK\",\n\t\t\texpectGracefulError:    \"\",\n\t\t},\n\t\t{\n\t\t\tname:                   \"nok, handlers do not returns before graceful shutdown deadline\",\n\t\t\twhenHandlerTakesLonger: true,\n\t\t\texpectBody:             \"timeout\",\n\t\t\texpectGracefulError:    stdContext.DeadlineExceeded.Error(),\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\n\t\t\te.GET(\"/ok\", func(c *Context) error {\n\t\t\t\tmsg := \"OK\"\n\t\t\t\tif tc.whenHandlerTakesLonger {\n\t\t\t\t\ttime.Sleep(150 * time.Millisecond)\n\t\t\t\t\tmsg = \"timeout\"\n\t\t\t\t}\n\t\t\t\treturn c.String(http.StatusOK, msg)\n\t\t\t})\n\n\t\t\taddrChan := make(chan string)\n\t\t\terrCh := make(chan error)\n\n\t\t\tctx, shutdown := stdContext.WithTimeout(stdContext.Background(), 50*time.Millisecond)\n\t\t\tdefer shutdown()\n\n\t\t\tshutdownErrChan := make(chan error, 1)\n\t\t\tgo func() {\n\t\t\t\terrCh <- (&StartConfig{\n\t\t\t\t\tAddress:         \":0\",\n\t\t\t\t\tGracefulTimeout: 50 * time.Millisecond,\n\t\t\t\t\tOnShutdownError: func(err error) {\n\t\t\t\t\t\tshutdownErrChan <- err\n\t\t\t\t\t},\n\t\t\t\t\tListenerAddrFunc: func(addr net.Addr) {\n\t\t\t\t\t\taddrChan <- addr.String()\n\t\t\t\t\t},\n\t\t\t\t}).Start(ctx, e)\n\t\t\t}()\n\n\t\t\taddr, err := waitForServerStart(addrChan, errCh)\n\t\t\tassert.NoError(t, err)\n\n\t\t\tcode, body, err := doGet(fmt.Sprintf(\"http://%v/ok\", addr))\n\t\t\tif err != nil {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equal(t, http.StatusOK, code)\n\t\t\tassert.Equal(t, tc.expectBody, body)\n\n\t\t\tvar shutdownErr error\n\t\t\tselect {\n\t\t\tcase shutdownErr = <-shutdownErrChan:\n\t\t\tdefault:\n\t\t\t}\n\t\t\tif tc.expectGracefulError != \"\" {\n\t\t\t\tassert.EqualError(t, shutdownErr, tc.expectGracefulError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, shutdownErr)\n\t\t\t}\n\n\t\t\tshutdown()\n\n\t\t\t<-errCh // we will be blocking here until server returns from http.Serve\n\n\t\t\t// check if server was stopped\n\t\t\tcode, body, err = doGet(fmt.Sprintf(\"http://%v/ok\", addr))\n\t\t\tassert.Error(t, err)\n\t\t\tif err != nil {\n\t\t\t\texpectContains := \"connect: connection refused\"\n\t\t\t\tif runtime.GOOS == \"windows\" {\n\t\t\t\t\texpectContains = \"No connection could be made\"\n\t\t\t\t}\n\t\t\t\tassert.True(t, strings.Contains(err.Error(), expectContains))\n\t\t\t}\n\t\t\tassert.Equal(t, 0, code)\n\t\t\tassert.Equal(t, \"\", body)\n\t\t})\n\t}\n}\n\nfunc TestStartConfig_Start_createListenerError(t *testing.T) {\n\te := New()\n\n\ts := &StartConfig{\n\t\tAddress:         \":0\",\n\t\tListenerNetwork: \"unknown\",\n\t\tBeforeServeFunc: func(s *http.Server) error {\n\t\t\treturn errors.New(\"stop_now\")\n\t\t},\n\t}\n\terr := s.Start(stdContext.Background(), e)\n\tassert.EqualError(t, err, \"listen unknown: unknown network unknown\")\n}\n\nfunc TestStartConfig_StartTLS(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname        string\n\t\taddr        string\n\t\tcertFile    string\n\t\tkeyFile     string\n\t\texpectError string\n\t}{\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\taddr: \":0\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, invalid certFile\",\n\t\t\taddr:        \":0\",\n\t\t\tcertFile:    \"not existing\",\n\t\t\texpectError: \"open not existing: no such file or directory\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, invalid keyFile\",\n\t\t\taddr:        \":0\",\n\t\t\tkeyFile:     \"not existing\",\n\t\t\texpectError: \"open not existing: no such file or directory\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, failed to create cert out of certFile and keyFile\",\n\t\t\taddr:        \":0\",\n\t\t\tkeyFile:     \"_fixture/certs/cert.pem\", // we are passing cert instead of key\n\t\t\texpectError: \"tls: found a certificate rather than a key in the PEM for the private key\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nok, invalid tls address\",\n\t\t\taddr:        \"nope\",\n\t\t\texpectError: \"listen tcp: address nope: missing port in address\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\n\t\t\taddrChan := make(chan string)\n\t\t\terrCh := make(chan error)\n\n\t\t\tctx, shutdown := stdContext.WithTimeout(stdContext.Background(), 200*time.Millisecond)\n\t\t\tdefer shutdown()\n\t\t\tgo func() {\n\t\t\t\tcertFile := \"_fixture/certs/cert.pem\"\n\t\t\t\tif tc.certFile != \"\" {\n\t\t\t\t\tcertFile = tc.certFile\n\t\t\t\t}\n\t\t\t\tkeyFile := \"_fixture/certs/key.pem\"\n\t\t\t\tif tc.keyFile != \"\" {\n\t\t\t\t\tkeyFile = tc.keyFile\n\t\t\t\t}\n\n\t\t\t\ts := &StartConfig{\n\t\t\t\t\tAddress:         tc.addr,\n\t\t\t\t\tGracefulTimeout: 100 * time.Millisecond,\n\t\t\t\t\tListenerAddrFunc: func(addr net.Addr) {\n\t\t\t\t\t\taddrChan <- addr.String()\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\terrCh <- s.StartTLS(ctx, e, certFile, keyFile)\n\t\t\t}()\n\n\t\t\t_, err := waitForServerStart(addrChan, errCh)\n\n\t\t\tif tc.expectError != \"\" {\n\t\t\t\tif _, ok := err.(*os.PathError); ok {\n\t\t\t\t\tassert.Error(t, err) // error messages for unix and windows are different. so name only error type here\n\t\t\t\t} else {\n\t\t\t\t\tassert.EqualError(t, err, tc.expectError)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFilepathOrContent(t *testing.T) {\n\tcert, err := os.ReadFile(\"_fixture/certs/cert.pem\")\n\trequire.NoError(t, err)\n\tkey, err := os.ReadFile(\"_fixture/certs/key.pem\")\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tcert        any\n\t\tkey         any\n\t\texpectedErr error\n\t\tname        string\n\t}{\n\t\t{\n\t\t\tname:        `ValidCertAndKeyFilePath`,\n\t\t\tcert:        \"_fixture/certs/cert.pem\",\n\t\t\tkey:         \"_fixture/certs/key.pem\",\n\t\t\texpectedErr: nil,\n\t\t},\n\t\t{\n\t\t\tname:        `ValidCertAndKeyByteString`,\n\t\t\tcert:        cert,\n\t\t\tkey:         key,\n\t\t\texpectedErr: nil,\n\t\t},\n\t\t{\n\t\t\tname:        `InvalidKeyType`,\n\t\t\tcert:        cert,\n\t\t\tkey:         1,\n\t\t\texpectedErr: ErrInvalidCertOrKeyType,\n\t\t},\n\t\t{\n\t\t\tname:        `InvalidCertType`,\n\t\t\tcert:        0,\n\t\t\tkey:         key,\n\t\t\texpectedErr: ErrInvalidCertOrKeyType,\n\t\t},\n\t\t{\n\t\t\tname:        `InvalidCertAndKeyTypes`,\n\t\t\tcert:        0,\n\t\t\tkey:         1,\n\t\t\texpectedErr: ErrInvalidCertOrKeyType,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\n\t\t\taddrChan := make(chan string)\n\t\t\terrCh := make(chan error)\n\n\t\t\tctx, shutdown := stdContext.WithTimeout(stdContext.Background(), 200*time.Millisecond)\n\t\t\tdefer shutdown()\n\n\t\t\tgo func() {\n\t\t\t\ts := &StartConfig{\n\t\t\t\t\tAddress:         \":0\",\n\t\t\t\t\tCertFilesystem:  os.DirFS(\".\"),\n\t\t\t\t\tGracefulTimeout: 100 * time.Millisecond,\n\t\t\t\t\tListenerAddrFunc: func(addr net.Addr) {\n\t\t\t\t\t\taddrChan <- addr.String()\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\terrCh <- s.StartTLS(ctx, e, tc.cert, tc.key)\n\t\t\t}()\n\n\t\t\t_, err := waitForServerStart(addrChan, errCh)\n\t\t\tif tc.expectedErr != nil {\n\t\t\t\tassert.EqualError(t, err, tc.expectedErr.Error())\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc supportsIPv6() bool {\n\taddrs, _ := net.InterfaceAddrs()\n\tfor _, addr := range addrs {\n\t\t// Check if any interface has local IPv6 assigned\n\t\tif strings.Contains(addr.String(), \"::1\") {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc TestStartConfig_WithListenerNetwork(t *testing.T) {\n\ttestCases := []struct {\n\t\tname    string\n\t\tnetwork string\n\t\taddress string\n\t}{\n\t\t{\n\t\t\tname:    \"tcp ipv4 address\",\n\t\t\tnetwork: \"tcp\",\n\t\t\taddress: \"127.0.0.1:1323\",\n\t\t},\n\t\t{\n\t\t\tname:    \"tcp ipv6 address\",\n\t\t\tnetwork: \"tcp\",\n\t\t\taddress: \"[::1]:1323\",\n\t\t},\n\t\t{\n\t\t\tname:    \"tcp4 ipv4 address\",\n\t\t\tnetwork: \"tcp4\",\n\t\t\taddress: \"127.0.0.1:1323\",\n\t\t},\n\t\t{\n\t\t\tname:    \"tcp6 ipv6 address\",\n\t\t\tnetwork: \"tcp6\",\n\t\t\taddress: \"[::1]:1323\",\n\t\t},\n\t}\n\n\thasIPv6 := supportsIPv6()\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tif !hasIPv6 && strings.Contains(tc.address, \"::\") {\n\t\t\t\tt.Skip(\"Skipping testing IPv6 for \" + tc.address + \", not available\")\n\t\t\t}\n\n\t\t\te := New()\n\t\t\te.GET(\"/ok\", func(c *Context) error {\n\t\t\t\treturn c.String(http.StatusOK, \"OK\")\n\t\t\t})\n\n\t\t\taddrChan := make(chan string)\n\t\t\terrCh := make(chan error)\n\n\t\t\tctx, shutdown := stdContext.WithTimeout(stdContext.Background(), 200*time.Millisecond)\n\t\t\tdefer shutdown()\n\n\t\t\tgo func() {\n\t\t\t\ts := &StartConfig{\n\t\t\t\t\tAddress:         tc.address,\n\t\t\t\t\tListenerNetwork: tc.network,\n\t\t\t\t\tGracefulTimeout: 100 * time.Millisecond,\n\t\t\t\t\tListenerAddrFunc: func(addr net.Addr) {\n\t\t\t\t\t\taddrChan <- addr.String()\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\terrCh <- s.Start(ctx, e)\n\t\t\t}()\n\n\t\t\t_, err := waitForServerStart(addrChan, errCh)\n\t\t\tassert.NoError(t, err)\n\n\t\t\tcode, body, err := doGet(fmt.Sprintf(\"http://%s/ok\", tc.address))\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, http.StatusOK, code)\n\t\t\tassert.Equal(t, \"OK\", body)\n\t\t})\n\t}\n}\n\nfunc TestStartConfig_WithHideBanner(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname       string\n\t\thideBanner bool\n\t}{\n\t\t{\n\t\t\tname:       \"hide banner on startup\",\n\t\t\thideBanner: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"show banner on startup\",\n\t\t\thideBanner: false,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\n\t\t\tbuf := new(bytes.Buffer)\n\t\t\te.Logger = slog.New(slog.NewTextHandler(buf, nil))\n\n\t\t\te.GET(\"/ok\", func(c *Context) error {\n\t\t\t\treturn c.String(http.StatusOK, \"OK\")\n\t\t\t})\n\n\t\t\taddrChan := make(chan string)\n\t\t\terrCh := make(chan error)\n\n\t\t\tctx, shutdown := stdContext.WithTimeout(stdContext.Background(), 200*time.Millisecond)\n\t\t\tdefer shutdown()\n\n\t\t\tgo func() {\n\t\t\t\t_, err := waitForServerStart(addrChan, errCh)\n\t\t\t\terrCh <- err\n\t\t\t\tshutdown()\n\t\t\t}()\n\n\t\t\ts := &StartConfig{\n\t\t\t\tAddress:         \":0\",\n\t\t\t\tHideBanner:      tc.hideBanner,\n\t\t\t\tGracefulTimeout: 100 * time.Millisecond,\n\t\t\t\tListenerAddrFunc: func(addr net.Addr) {\n\t\t\t\t\taddrChan <- addr.String()\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tif err := s.Start(ctx, e); err != http.ErrServerClosed {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.NoError(t, <-errCh)\n\n\t\t\tcontains := strings.Contains(buf.String(), \"High performance, minimalist Go web framework\")\n\t\t\tif tc.hideBanner {\n\t\t\t\tassert.False(t, contains)\n\t\t\t} else {\n\t\t\t\tassert.True(t, contains)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestStartConfig_WithHidePort(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname     string\n\t\thidePort bool\n\t}{\n\t\t{\n\t\t\tname:     \"hide port on startup\",\n\t\t\thidePort: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"show port on startup\",\n\t\t\thidePort: false,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\n\t\t\tbuf := new(bytes.Buffer)\n\t\t\te.Logger = slog.New(slog.NewTextHandler(buf, nil))\n\n\t\t\te.GET(\"/ok\", func(c *Context) error {\n\t\t\t\treturn c.String(http.StatusOK, \"OK\")\n\t\t\t})\n\n\t\t\taddrChan := make(chan string)\n\t\t\terrCh := make(chan error, 1)\n\n\t\t\tctx, shutdown := stdContext.WithTimeout(stdContext.Background(), 200*time.Millisecond)\n\n\t\t\tgo func() {\n\t\t\t\t_, err := waitForServerStart(addrChan, errCh)\n\t\t\t\terrCh <- err\n\t\t\t\tshutdown()\n\t\t\t}()\n\n\t\t\ts := &StartConfig{\n\t\t\t\tAddress:         \":0\",\n\t\t\t\tHidePort:        tc.hidePort,\n\t\t\t\tGracefulTimeout: 100 * time.Millisecond,\n\t\t\t\tListenerAddrFunc: func(addr net.Addr) {\n\t\t\t\t\taddrChan <- addr.String()\n\t\t\t\t},\n\t\t\t}\n\t\t\tif err := s.Start(ctx, e); err != http.ErrServerClosed {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.NoError(t, <-errCh)\n\n\t\t\tcontains := strings.Contains(buf.String(), \"http(s) server started\")\n\t\t\tif tc.hidePort {\n\t\t\t\tassert.False(t, contains)\n\t\t\t} else {\n\t\t\t\tassert.True(t, contains)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestStartConfig_WithBeforeServeFunc(t *testing.T) {\n\te := New()\n\n\te.GET(\"/ok\", func(c *Context) error {\n\t\treturn c.String(http.StatusOK, \"OK\")\n\t})\n\n\ts := &StartConfig{\n\t\tAddress: \":0\",\n\t\tBeforeServeFunc: func(s *http.Server) error {\n\t\t\treturn errors.New(\"is called before serve\")\n\t\t},\n\t}\n\terr := s.Start(stdContext.Background(), e)\n\tassert.EqualError(t, err, \"is called before serve\")\n}\n\nfunc TestStartConfig_WithHTTP2WithCustomTlsConfig(t *testing.T) {\n\tvar testCases = []struct {\n\t\tname         string\n\t\tdisableHTTP2 bool\n\t}{\n\t\t{\n\t\t\tname:         \"HTTP2 enabled default\",\n\t\t\tdisableHTTP2: false,\n\t\t},\n\t\t{\n\t\t\tname:         \"HTTP2 disabled\",\n\t\t\tdisableHTTP2: true,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\te := New()\n\n\t\t\te.GET(\"/ok\", func(c *Context) error {\n\t\t\t\treturn c.String(http.StatusOK, \"OK\")\n\t\t\t})\n\n\t\t\taddrChan := make(chan string)\n\t\t\terrCh := make(chan error, 1)\n\n\t\t\tctx, shutdown := stdContext.WithTimeout(stdContext.Background(), 200*time.Millisecond)\n\t\t\tdefer shutdown()\n\n\t\t\tgo func() {\n\t\t\t\tcertFile := \"_fixture/certs/cert.pem\"\n\t\t\t\tkeyFile := \"_fixture/certs/key.pem\"\n\n\t\t\t\ts := &StartConfig{\n\t\t\t\t\tAddress:         \":0\",\n\t\t\t\t\tGracefulTimeout: 100 * time.Millisecond,\n\t\t\t\t\tListenerAddrFunc: func(addr net.Addr) {\n\t\t\t\t\t\taddrChan <- addr.String()\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tif tc.disableHTTP2 {\n\t\t\t\t\ts.TLSConfig = &tls.Config{\n\t\t\t\t\t\tNextProtos: []string{\"http/1.1\"},\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\terrCh <- s.StartTLS(ctx, e, certFile, keyFile)\n\t\t\t}()\n\n\t\t\taddr, err := waitForServerStart(addrChan, errCh)\n\t\t\tassert.NoError(t, err)\n\t\t\turl := fmt.Sprintf(\"https://%v/ok\", addr)\n\n\t\t\t// do ordinary http(s) request\n\t\t\tclient := &http.Client{Transport: &http.Transport{\n\t\t\t\tTLSClientConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t\t}}\n\t\t\tres, err := client.Get(url)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, http.StatusOK, res.StatusCode)\n\n\t\t\t// do HTTP2 request\n\t\t\tclient.Transport = &http2.Transport{\n\t\t\t\tTLSClientConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t\t}\n\t\t\tresp, err := client.Get(url)\n\t\t\tif err != nil {\n\t\t\t\tif tc.disableHTTP2 {\n\t\t\t\t\tassert.True(t, strings.Contains(err.Error(), `remote error: tls: no application protocol`))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlog.Fatalf(\"Failed get: %s\", err)\n\t\t\t}\n\n\t\t\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\t\t\tdefer resp.Body.Close()\n\t\t\tbody, err := io.ReadAll(resp.Body)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"Failed reading response body: %s\", err)\n\t\t\t}\n\t\t\tassert.Equal(t, \"OK\", string(body))\n\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "version.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nconst (\n\t// Version of Echo\n\tVersion = \"5.0.4\"\n)\n"
  },
  {
    "path": "vhost.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport \"net/http\"\n\n// NewVirtualHostHandler creates instance of Echo that routes requests to given virtual hosts\n// when hosts in request does not exist in given map the request is served by returned Echo instance.\nfunc NewVirtualHostHandler(vhosts map[string]*Echo) *Echo {\n\te := New()\n\te.serveHTTPFunc = func(w http.ResponseWriter, r *http.Request) {\n\t\tif vh, ok := vhosts[r.Host]; ok {\n\t\t\tvh.ServeHTTP(w, r)\n\t\t\treturn\n\t\t}\n\t\te.serveHTTP(w, r) // unknown host in request\n\t}\n\treturn e\n}\n"
  },
  {
    "path": "vhost_test.go",
    "content": "// SPDX-License-Identifier: MIT\n// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors\n\npackage echo\n\nimport (\n\t\"github.com/stretchr/testify/assert\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n)\n\nfunc TestVirtualHostHandler(t *testing.T) {\n\tokHandler := func(c *Context) error { return c.String(http.StatusOK, http.StatusText(http.StatusOK)) }\n\tteapotHandler := func(c *Context) error { return c.String(http.StatusTeapot, http.StatusText(http.StatusTeapot)) }\n\tacceptHandler := func(c *Context) error { return c.String(http.StatusAccepted, http.StatusText(http.StatusAccepted)) }\n\tteapotMiddleware := MiddlewareFunc(func(next HandlerFunc) HandlerFunc { return teapotHandler })\n\n\tok := New()\n\tok.GET(\"/\", okHandler)\n\tok.GET(\"/foo\", okHandler)\n\n\tteapot := New()\n\tteapot.GET(\"/\", teapotHandler)\n\tteapot.GET(\"/foo\", teapotHandler)\n\n\tmiddle := New()\n\tmiddle.Use(teapotMiddleware)\n\tmiddle.GET(\"/\", okHandler)\n\tmiddle.GET(\"/foo\", okHandler)\n\n\tvirtualHosts := NewVirtualHostHandler(map[string]*Echo{\n\t\t\"ok.com\":         ok,\n\t\t\"teapot.com\":     teapot,\n\t\t\"middleware.com\": middle,\n\t})\n\tvirtualHosts.GET(\"/\", acceptHandler)\n\tvirtualHosts.GET(\"/foo\", acceptHandler)\n\n\tvar testCases = []struct {\n\t\tname         string\n\t\twhenHost     string\n\t\twhenPath     string\n\t\texpectBody   string\n\t\texpectStatus int\n\t}{\n\t\t{\n\t\t\tname:         \"No Host Root\",\n\t\t\twhenHost:     \"\",\n\t\t\twhenPath:     \"/\",\n\t\t\texpectBody:   http.StatusText(http.StatusAccepted),\n\t\t\texpectStatus: http.StatusAccepted,\n\t\t},\n\t\t{\n\t\t\tname:         \"No Host Foo\",\n\t\t\twhenHost:     \"\",\n\t\t\twhenPath:     \"/foo\",\n\t\t\texpectBody:   http.StatusText(http.StatusAccepted),\n\t\t\texpectStatus: http.StatusAccepted,\n\t\t},\n\t\t{\n\t\t\tname:         \"OK Host Root\",\n\t\t\twhenHost:     \"ok.com\",\n\t\t\twhenPath:     \"/\",\n\t\t\texpectBody:   http.StatusText(http.StatusOK),\n\t\t\texpectStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:         \"OK Host Foo\",\n\t\t\twhenHost:     \"ok.com\",\n\t\t\twhenPath:     \"/foo\",\n\t\t\texpectBody:   http.StatusText(http.StatusOK),\n\t\t\texpectStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:         \"Teapot Host Root\",\n\t\t\twhenHost:     \"teapot.com\",\n\t\t\twhenPath:     \"/\",\n\t\t\texpectBody:   http.StatusText(http.StatusTeapot),\n\t\t\texpectStatus: http.StatusTeapot,\n\t\t},\n\t\t{\n\t\t\tname:         \"Teapot Host Foo\",\n\t\t\twhenHost:     \"teapot.com\",\n\t\t\twhenPath:     \"/foo\",\n\t\t\texpectBody:   http.StatusText(http.StatusTeapot),\n\t\t\texpectStatus: http.StatusTeapot,\n\t\t},\n\t\t{\n\t\t\tname:         \"Middleware Host\",\n\t\t\twhenHost:     \"middleware.com\",\n\t\t\twhenPath:     \"/\",\n\t\t\texpectBody:   http.StatusText(http.StatusTeapot),\n\t\t\texpectStatus: http.StatusTeapot,\n\t\t},\n\t\t{\n\t\t\tname:         \"Middleware Host Foo\",\n\t\t\twhenHost:     \"middleware.com\",\n\t\t\twhenPath:     \"/foo\",\n\t\t\texpectBody:   http.StatusText(http.StatusTeapot),\n\t\t\texpectStatus: http.StatusTeapot,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodGet, tc.whenPath, nil)\n\t\t\treq.Host = tc.whenHost\n\t\t\trec := httptest.NewRecorder()\n\n\t\t\tvirtualHosts.ServeHTTP(rec, req)\n\n\t\t\tassert.Equal(t, tc.expectStatus, rec.Code)\n\t\t\tassert.Equal(t, tc.expectBody, rec.Body.String())\n\t\t})\n\t}\n}\n"
  }
]