valyala/fasthttp · error

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

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.

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)

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.