labstack/echo · warning

failed to unescape path variable: %w

Error message

failed to unescape path variable: %w

What it means

Returned by StaticDirectoryHandler (echo.go:657-661) when url.PathUnescape of the wildcard path param fails. The client sent a %-sequence that is not valid percent-encoding (e.g. %zz or a truncated %2). The handler only runs this unescape when disablePathUnescaping is false.

Source

Thrown at echo.go:660

// StaticDirectoryHandler creates handler function to serve files from provided file system
// When disablePathUnescaping is set then file name from path is not unescaped and is served as is.
//
// Note: when disablePathUnescaping=false, the handler decodes the wildcard param before serving.
// If route guards (e.g. e.GET("/admin/*", forbidden)) are used to restrict parts of the
// filesystem, an encoded separator (%2F) or encoded dot-dot (%2E%2E) in the URL can resolve to
// a path that the router never matched against the guard route. Enabling
// RouterConfig.UseEscapedPathForMatching does NOT fix this — it changes which path the router
// uses for matching but still lets path.Clean resolve ".." segments into a guarded directory.
// Do not rely on route guards alone to restrict a filesystem served by this handler.
// See https://github.com/labstack/echo/security/advisories/GHSA-vfp3-v2gw-7wfq
func StaticDirectoryHandler(fileSystem fs.FS, disablePathUnescaping bool) HandlerFunc {
	return func(c *Context) error {
		p := c.Param("*")
		if !disablePathUnescaping { // when router is already unescaping we do not want to do is twice
			tmpPath, err := url.PathUnescape(p)
			if err != nil {
				return fmt.Errorf("failed to unescape path variable: %w", err)
			}
			p = tmpPath
		}

		// fs.FS.Open() already assumes that file names are relative to FS root path and considers name with prefix `/`
		// as invalid
		// Use path.Clean (not filepath.Clean): fs.FS paths are always forward-slash, so a backslash must stay a literal
		// character rather than being interpreted as a separator on Windows (which would resolve a file across a boundary
		// the router never matched on, the same Windows backslash traversal class as GHSA-pgvm-wxw2-hrv9).
		name := path.Clean(strings.TrimPrefix(p, "/"))
		fi, err := fs.Stat(fileSystem, name)
		if err != nil {
			return ErrNotFound
		}

		// If the request is for a directory and does not end with "/" redirect to path which ends with "/"
		p = c.Request().URL.Path
		if fi.IsDir() && len(p) > 0 && p[len(p)-1] != '/' {

View on GitHub (pinned to 05489dc173)

Solutions

  1. Let the default HTTPErrorHandler turn this into a 404/400
  2. Override e.HTTPErrorHandler to map the error to a clean 400 and avoid echoing raw path bytes
  3. If serving untrusted paths, review whether disablePathUnescaping or router-level unescaping fits your threat model

Example fix

// before: rely on the default handler, which leaks detail
// after: custom error handler masks the cause
e.HTTPErrorHandler = func(err error, c echo.Context) {
    if strings.Contains(err.Error(), "unescape path variable") {
        _ = c.String(http.StatusBadRequest, "bad path encoding")
        return
    }
    c.DefaultHTTPErrorHandler(err, c)
}
Defensive patterns

Strategy: try-catch

Validate before calling

// nothing to validate pre-handler; reject malformed escapes at the edge (reverse proxy / WAF) when possible

Try / catch

e.HTTPErrorHandler = func(err error, c echo.Context) {
    if errors.Is(err, echo.ErrNotFound) || strings.Contains(err.Error(), "unescape path variable") {
        _ = c.String(http.StatusBadRequest, "bad request")
        return
    }
    c.DefaultHTTPErrorHandler(err, c)
}

Prevention

When it happens

Trigger: A request to a static route whose wildcard segment contains malformed percent-encoding, while disablePathUnescaping is false (the default behavior).

Common situations: Bots/scanners sending crafted URLs; clients with buggy URL encoders; double-encoding mishaps that produce invalid sequences.

Related errors


AI-assisted analysis of labstack/echo@05489dc173 (2026-08-04). Data as JSON: /data/errors/b8eb09638475f27e.json. Report an issue: GitHub.