caddyserver/caddy · error
cannot read etag from file %s: %v
Error message
cannot read etag from file %s: %v
What it means
Returned by FileServer.getEtagFromFile while looking for a sidecar etag file: for a configured etag_file_extensions suffix, the file exists (stat succeeded, so fs.ErrNotExist did not apply) but reading it failed with another error. Typical causes are permission denial or an I/O error on the sidecar file.
Source
Thrown at modules/caddyhttp/fileserver/staticfiles.go:786
var sb strings.Builder
sb.WriteRune('"')
sb.WriteString(strconv.FormatInt(mtime.UnixNano(), 36))
sb.WriteRune('-')
sb.WriteString(strconv.FormatInt(d.Size(), 36))
sb.WriteRune('"')
return sb.String()
}
// Finds the first corresponding etag file for a given file in the file system and return its content
func (fsrv *FileServer) getEtagFromFile(fileSystem fs.FS, filename string) (string, error) {
for _, suffix := range fsrv.EtagFileExtensions {
etagFilename := filename + suffix
etag, err := fs.ReadFile(fileSystem, etagFilename)
if errors.Is(err, fs.ErrNotExist) {
continue
}
if err != nil {
return "", fmt.Errorf("cannot read etag from file %s: %v", etagFilename, err)
}
// Etags should not contain newline characters
etag = bytes.ReplaceAll(etag, []byte("\n"), []byte{})
return string(etag), nil
}
return "", nil
}
// redirect performs a redirect to a given path. The 'toPath' parameter
// MUST be solely a path, and MUST NOT include a query.
func redirect(w http.ResponseWriter, r *http.Request, toPath string) error {
for strings.HasPrefix(toPath, "//") {
// prevent path-based open redirects
toPath = strings.TrimPrefix(toPath, "/")
}
// preserve the query string if presentView on GitHub (pinned to 50e54ee279)
Solutions
- Fix permissions on the sidecar files so the Caddy process can read them (chown/chmod, or align the container user).
- Regenerate the .etag files with the same tooling/user that serves the content.
- If the read failure is transient (NFS), address the underlying storage issue; the error surfaces per request.
- As a stopgap, remove the offending suffix from etag_file_extensions (normal filesystem etag behavior applies).
Example fix
# before: sidecar unreadable by Caddy user -rw------- root root app.js.etag # after chmod 644 app.js.etag # or chown to the caddy runtime user
Defensive patterns
Strategy: fallback
Validate before calling
# Deploy-time check: every .etag sidecar must be readable by the Caddy user
runuser -u caddy -- find "$WEBROOT" -name '*.etag' ! -readable -print | grep . && { echo 'unreadable etag files'; exit 1; } Prevention
- Generate sidecar .etag files with the same user/permissions as served content.
- Include read-permission checks for sidecar files in your deploy pipeline.
- Mount volumes with permissive-enough modes (or correct fsGroup) in containers.
When it happens
Trigger: Configuring etag_file_extensions (e.g. [".etag"]) and a request hits a file whose companion .etag file exists but is unreadable — wrong ownership/permissions (0600 root file with non-root worker), or transient disk/NFS errors.
Common situations: Deploying pre-built artifacts where .etag files were created by a different user; containerized Caddy reading a mounted volume with restrictive modes; network filesystems returning errors other than not-exist.
Related errors
- encoding config: %v
- malformed If-Match header; expect quoted string
- malformed If-Match header; expect format \"<path> <hash>\"
- parsing browse template: %v
- parsing browse template file: %v
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/af139fb78f431feb.
Report an issue: GitHub.