gofr-dev/gofr · error

file does not have read permission

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.

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.

Appendix: 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)