{"record":{"id":"af139fb78f431feb","repo":"caddyserver/caddy","slug":"cannot-read-etag-from-file-s-v","errorCode":null,"errorMessage":"cannot read etag from file %s: %v","messagePattern":"cannot read etag from file (.+?): (.+?)","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"modules/caddyhttp/fileserver/staticfiles.go","lineNumber":786,"sourceCode":"\tvar sb strings.Builder\n\tsb.WriteRune('\"')\n\tsb.WriteString(strconv.FormatInt(mtime.UnixNano(), 36))\n\tsb.WriteRune('-')\n\tsb.WriteString(strconv.FormatInt(d.Size(), 36))\n\tsb.WriteRune('\"')\n\treturn sb.String()\n}\n\n// Finds the first corresponding etag file for a given file in the file system and return its content\nfunc (fsrv *FileServer) getEtagFromFile(fileSystem fs.FS, filename string) (string, error) {\n\tfor _, suffix := range fsrv.EtagFileExtensions {\n\t\tetagFilename := filename + suffix\n\t\tetag, err := fs.ReadFile(fileSystem, etagFilename)\n\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\tcontinue\n\t\t}\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"cannot read etag from file %s: %v\", etagFilename, err)\n\t\t}\n\n\t\t// Etags should not contain newline characters\n\t\tetag = bytes.ReplaceAll(etag, []byte(\"\\n\"), []byte{})\n\n\t\treturn string(etag), nil\n\t}\n\treturn \"\", nil\n}\n\n// redirect performs a redirect to a given path. The 'toPath' parameter\n// MUST be solely a path, and MUST NOT include a query.\nfunc redirect(w http.ResponseWriter, r *http.Request, toPath string) error {\n\tfor strings.HasPrefix(toPath, \"//\") {\n\t\t// prevent path-based open redirects\n\t\ttoPath = strings.TrimPrefix(toPath, \"/\")\n\t}\n\t// preserve the query string if present","sourceCodeStart":768,"sourceCodeEnd":804,"githubUrl":"https://github.com/caddyserver/caddy/blob/50e54ee279aa1e504fe218ca49ab6ae16c100410/modules/caddyhttp/fileserver/staticfiles.go#L768-L804","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"# before: sidecar unreadable by Caddy user\n-rw------- root root app.js.etag\n# after\nchmod 644 app.js.etag  # or chown to the caddy runtime user","handlingStrategy":"fallback","validationCode":"# Deploy-time check: every .etag sidecar must be readable by the Caddy user\nrunuser -u caddy -- find \"$WEBROOT\" -name '*.etag' ! -readable -print | grep . && { echo 'unreadable etag files'; exit 1; }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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."],"tags":["caddy","fileserver","etag","permissions","io"],"backgroundTag":null,"analyzedSha":"50e54ee279aa1e504fe218ca49ab6ae16c100410","analyzedAt":"2026-08-15T09:20:21.641Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}