gofr-dev/gofr · error

file does not have read permission: %w

Error message

file does not have read permission: %w

What it means

errReadPermissionDenied wraps fs.ErrPermission and is produced by validateFile in pkg/gofr/http/router.go when a static file matched by the router has a mode with no read bit set. It exists so files lacking the read permission bit are reported identically to a real EACCES from os.Open, ensuring both routes reach respondWithFileError with the same answer.

Source

Thrown at pkg/gofr/http/router.go:38

	DefaultSwaggerFileName       = "openapi.json"
	staticServerNotFoundFileName = "404.html"
	staticServerIndexFileName    = "index.html"

	// RouterEnvVar selects the route matcher. Unset (or any unrecognized value)
	// means MatcherMux, so the default behavior is unchanged.
	RouterEnvVar = "GOFR_ROUTER"

	// MatcherMux is gorilla/mux's linear scan — the default.
	MatcherMux = "mux"
	// MatcherTrie is the opt-in segment-trie index, O(path length) in the number
	// of registered routes.
	MatcherTrie = "trie"
)

// errReadPermissionDenied wraps fs.ErrPermission so that a file whose mode carries no read bit is
// reported the same way a real EACCES from os.Open is — the two reach respondWithFileError by
// different routes and must not answer differently.
var errReadPermissionDenied = fmt.Errorf("file does not have read permission: %w", fs.ErrPermission)

// Router is responsible for routing HTTP request.
type Router struct {
	mux.Router
	RegisteredRoutes *[]string

	// useTrie selects the O(path) trie matcher (GOFR_ROUTER=trie) over mux's
	// default O(n) linear scan. When false, ServeHTTP delegates to mux exactly
	// as before, so the default behavior is byte-for-byte unchanged.
	useTrie bool
	// idx is the trie index. It is built once, lazily, on the first request,
	// from the routes registered up to that point. This is correct for GoFr's
	// lifecycle: every route is registered during startup (app.GET/POST/...,
	// the GraphQL route, the static/catch-all handlers) before the server
	// accepts its first request, and GoFr does not add routes afterwards. A
	// route registered after the first request would not be reflected in the
	// trie index — a deliberate trade for a lock-free steady state, matching
	// GoFr's static-routing model. buildIdx guards that one-time build.

View on GitHub (pinned to 187eb24962)

Solutions

  1. chmod the file so the serving process's user has read permission (e.g. chmod 644 file)
  2. Check ownership with ls -l and chown to the user running the gofr service
  3. Verify the static-file path configured on the router points to readable assets
  4. Run the container/process with a user that has read access to the asset directory

Example fix

// before (shell)
-rw-------  1 root root index.html
// after (shell)
chmod 644 index.html  # or chown appuser:appuser index.html
Defensive patterns

Strategy: validation

Validate before calling

info, err := os.Stat(path)
if err != nil {
    return fmt.Errorf("static file unavailable: %w", err)
}
if info.Mode().Perm()&0o400 == 0 {
    return fmt.Errorf("file lacks read permission: %s", path)
}

Type guard

func isReadPermissionDenied(err error) bool {
    return errors.Is(err, fs.ErrPermission)
}

Try / catch

if err := serveFile(w, r, path); err != nil {
    if errors.Is(err, fs.ErrPermission) {
        http.Error(w, "forbidden", http.StatusForbidden)
        return
    }
    http.Error(w, "not found", http.StatusNotFound)
}

Prevention

When it happens

Trigger: Serving static files via the router where the requested file's permission mode lacks the read bit (e.g. mode 0200 or 0600 owned by another user), so validateFile rejects it before/instead of the OS open failing.

Common situations: Deploying static assets with restrictive umask; files copied by CI as root then served by an unprivileged process; Docker image layers with wrong file ownership/mode.

Related errors


AI-assisted analysis of gofr-dev/gofr@187eb24962 (2026-09-01). Data as JSON: /api/errors/b71f3a35fc3a069e. Report an issue: GitHub.