valyala/fasthttp · error

directory with unexpected suffix found

Error message

directory with unexpected suffix found: %q: suffix: %q

What it means

The FS handler opened a path that turned out to be a directory even though its name carries the compressed-file suffix (e.g. .gz/.br). fasthttp refuses to serve a directory as a compressed file because suffix and content contradict. Depending on mustCompress it returns this error, or errDirIndexRequired to fall back to directory index handling.

Solutions

  1. Remove or rename the directory that carries a compressed suffix (e.g. 'app.js.gz')
  2. Rebuild static assets so .gz entries are real files, not directories
  3. Adjust Root/PathRewrite so the resolved path points at a file
  4. Disable compression options if you do not intend to serve pre-compressed files

Example fix

// before
$ mkdir dist/bundle.js.gz
// after
$ gzip -k dist/bundle.js  # creates a real file dist/bundle.js.gz
Defensive patterns

Strategy: validation

Validate before calling

if fi, err := os.Stat(path); err == nil && fi.IsDir() { return 404 } // also ensure no dir ends in .gz/.br

Try / catch

if err := serveFS(ctx); err != nil { if strings.Contains(err.Error(), "unexpected suffix") { /* fix path or 404 */ } }

Prevention

When it happens

Trigger: Request path resolves to a directory whose name ends in h.compressedFileSuffixes[fileEncoding] while mustCompress is true — e.g. a literal directory named 'data.gz' exists and FS is configured with Compress/CompressBrotli.

Common situations: Accidentally creating a directory named like a gzip asset (mkdir out/app.js.gz instead of gzip); build scripts leaving compressed-named dirs; path rewrite rules mapping routes to directories with such names.

Related errors


AI-assisted analysis of valyala/fasthttp@c96f600972 (2026-08-31). Data as JSON: /api/errors/caac24cce1cb0618. Report an issue: GitHub.

Appendix: source

Thrown at fs.go:1905

		// If the file is not found and the path is empty, let's return errDirIndexRequired error.
		if filePath == "" && (errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrInvalid)) {
			return nil, errDirIndexRequired
		}

		return nil, err
	}

	fileInfo, err := f.Stat()
	if err != nil {
		_ = f.Close()
		return nil, fmt.Errorf("cannot obtain info for file %q: %w", filePath, err)
	}

	if fileInfo.IsDir() {
		_ = f.Close()
		if mustCompress {
			return nil, fmt.Errorf("directory with unexpected suffix found: %q: suffix: %q",
				filePath, h.compressedFileSuffixes[fileEncoding])
		}
		return nil, errDirIndexRequired
	}

	if mustCompress {
		fileInfoOriginal, err := fs.Stat(h.filesystem, filePathOriginal)
		if err != nil {
			_ = f.Close()
			return nil, fmt.Errorf("cannot obtain info for original file %q: %w", filePathOriginal, err)
		}

		// Only re-create the compressed file if there was more than a second between the mod times.
		// On macOS the gzip seems to truncate the nanoseconds in the mod time causing the original file
		// to look newer than the gzipped file.
		if fileInfoOriginal.ModTime().Sub(fileInfo.ModTime()) >= time.Second {
			// The compressed file became stale. Re-create it.
			_ = f.Close()

View on GitHub (pinned to c96f600972)