gorilla/mux · warning

err.Error()

Error message

err.Error()

What it means

Not an error thrown by gorilla/mux. README.md:264 is part of the SPA static-file handler example: when os.Stat(filepath.Join(staticPath, r.URL.Path)) returns an error other than os.IsNotExist, the example responds http.Error(w, err.Error(), http.StatusInternalServerError). The 'error' is the leaked internal error string written to the response body — a security anti-pattern.

Source

Thrown at README.md:264

// on the SPA handler. If a file is found, it will be served. If not, the
// file located at the index path on the SPA handler will be served. This
// is suitable behavior for serving an SPA (single page application).
func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Join internally call path.Clean to prevent directory traversal
	path := filepath.Join(h.staticPath, r.URL.Path)

	// check whether a file exists or is a directory at the given path
	fi, err := os.Stat(path)
	if os.IsNotExist(err) || fi.IsDir() {
		// file does not exist or path is a directory, serve index.html
		http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath))
		return
	}

	if err != nil {
		// if we got an error (that wasn't that the file doesn't exist) stating the
		// file, return a 500 internal server error and stop
		http.Error(w, err.Error(), http.StatusInternalServerError)
        return
	}

	// otherwise, use http.FileServer to serve the static file
	http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r)
}

func main() {
	router := mux.NewRouter()

	router.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
		// an example API handler
		json.NewEncoder(w).Encode(map[string]bool{"ok": true})
	})

	spa := spaHandler{staticPath: "build", indexPath: "index.html"}
	router.PathPrefix("/").Handler(spa)

View on GitHub (pinned to db9d1d0073)

Solutions

  1. Log err with full detail server-side, then write a generic message (e.g. "internal error") to the client — never err.Error().
  2. Check the staticPath directory permissions and the UID/GID the server runs as; ensure read+execute on every parent directory.
  3. Handle specific syscall errors explicitly (e.g. os.IsPermission, errors.Is(err, syscall.ENAMETOOLONG)) and return 403/414 as appropriate instead of 500.
  4. Add a bounded Allow list or canonicalize with filepath.Clean and reject '..' traversal before stating, so attacker-controlled paths can't surface OS errors.

Example fix

// before (README.md:264)
if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
}

// after: log full detail, return generic body
if err != nil {
    log.Printf("spa stat %q: %v", path, err)
    if errors.Is(err, os.ErrPermission) {
        http.Error(w, "forbidden", http.StatusForbidden)
    } else {
        http.Error(w, "internal error", http.StatusInternalServerError)
    }
    return
}
Defensive patterns

Strategy: validation

Validate before calling

// Validate the joined path and permissions BEFORE os.Stat to avoid leaking OS errors.
func safeSpaPath(root, reqPath, index string) (string, error) {
    cleaned := filepath.Clean("/" + reqPath)            // force under root
    full := filepath.Join(root, cleaned)
    if !strings.HasPrefix(full, filepath.Clean(root)+string(os.PathSeparator)) && full != filepath.Clean(root) {
        return "", os.ErrPermission                      // traversal attempt
    }
    return full, nil
}

full, err := safeSpaPath(h.staticPath, r.URL.Path, h.indexPath)
if err != nil {
    http.Error(w, "forbidden", http.StatusForbidden)
    return
}
fi, err := os.Stat(full)

Try / catch

// After os.Stat: classify, log full detail server-side, send generic body.
fi, err := os.Stat(full)
if os.IsNotExist(err) || (err == nil && fi.IsDir()) {
    http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath))
    return
}
if err != nil {
    log.Printf("spa stat %q: %v", full, err) // full detail to logs only
    switch {
    case errors.Is(err, os.ErrPermission):
        http.Error(w, "forbidden", http.StatusForbidden)
    default:
        http.Error(w, "internal error", http.StatusInternalServerError) // NOT err.Error()
    }
    return
}

Prevention

When it happens

Trigger: os.Stat fails on the joined path for a reason other than 'file not found' — permission denied (EACCES), I/O error, stale NFS handle, ENAMETOOLONG, or read-only filesystem — and the example dumps err.Error() straight into the HTTP response.

Common situations: Deploying the SPA handler with a staticPath the process can't read (wrong user / umask); disk or mount failure under load; path constructed from untrusted input becoming too long; container ran as a user without read access to the assets.

Related errors


AI-assisted analysis of gorilla/mux@db9d1d0073 (2026-08-04). Data as JSON: /data/errors/72465c9847701780.json. Report an issue: GitHub.