Configuration: static file serving

This page documents directives that configure static file serving, directory listings, compression, caching behavior, and custom error pages for requests resolved to the filesystem (via root).

Info

Static file serving is handled by the http-static module. For related features, see Routing and URL processing, HTTP cache, HTTP response control, URL rewriting, and HTTP compression.

Directives

Index and directory listings

  • index <filename: string>...
    • This directive specifies one or more filenames to try when a request path resolves to a directory. Files are tried in order; the first existing file replaces the directory path in the file context. Only applies when the resolved path is a directory and no path_info is present. Default: index index.html index.htm index.xhtml
  • directory_listing [bool: boolean] (http-static)
    • This directive specifies whether auto-generated HTML directory listings are enabled when a request path resolves to a directory and no index file is found. Default: directory_listing false

Configuration example:

example.com {
    root /srv/www/example
    index index.html index.htm
    directory_listing
}
Note
  • Only generates a listing if no index file was found for the directory.
  • Dotfiles (names starting with .) are excluded from the listing, except .maindesc which is read as a description.
  • A .maindesc file in the directory, if present, is displayed as a <pre> block below the file table.

Caching headers

  • etag [bool: boolean] (http-static)
    • This directive specifies whether ETag generation for static file responses is enabled. ETags are weak ETags (W/"...") generated from an xxHash3 hash of the file path, size, and modification time. Default: etag true
  • file_cache_control <value: string> (http-static)
    • This directive specifies the Cache-Control response header for all static file responses. The value is passed through as-is. Default: not set

Configuration example:

example.com {
    root /srv/www/example
    etag
    file_cache_control "public, max-age=3600"
}
Note
  • When compression is used, a suffix is appended to the ETag (e.g. W/"abc123-br" for Brotli).
  • If-None-Match requests that match the current ETag return 304 Not Modified.
  • Pre-compressed sidecar files receive their own ETag based on the sidecar file’s own metadata.

MIME types

  • mime_type <extension: string> <mime-type: string> (http-static)
    • This directive maps a file extension (with or without leading dot) to a MIME type. Custom MIME type mappings override the built-in database for matching extensions. Multiple mime_type directives can be used to map different extensions. Default: built-in MIME database

Configuration example:

example.com {
    root /srv/www/example
    mime_type ".wasm" "application/wasm"
    mime_type ".webmanifest" "application/manifest+json"
}
Note
  • If the extension is not found in custom mappings, the built-in database is used as a fallback.
  • If neither custom nor built-in mappings match, the response is sent with no Content-Type header.

Error pages

  • error_page <status-code: integer>... <file-path: string>
    • This directive specifies one or more HTTP status codes followed by a file path to serve as the error response body. The last argument is always the file path; all preceding arguments are status codes. Default: built-in error pages
  • error_page_placeholders [bool: boolean]
    • When enabled, {{trace.id}} and {{trace.spanid}} in the error page file are replaced with the request’s trace ID and span ID. Default: false

Configuration example:

example.com {
    root /srv/www/example
    error_page 404 /custom/404.html
    error_page 500 502 503 504 /custom/50x.html
    error_page_placeholders true
}
Note
  • Only applies when an error response is being generated and no custom response has already been set.
  • The file path is absolute or relative to the current working directory.
  • If the specified error page file does not exist, the directive is skipped and the built-in error page is used.
  • Multiple status codes can be mapped to the same error page in a single directive.
  • Placeholder substitution reads the file into memory and replaces {{trace.id}} and {{trace.spanid}} with the current request’s trace context before serving. The zerocopy/sendfile optimization is bypassed when substitution is active.
  • disable_symlinks [bool: boolean | string: "if_not_owner"]
    • This directive controls whether symbolic links are allowed during file path resolution. When a symlink is encountered while traversing the request path, the behavior depends on this setting:
      • false (default): Allow all symlinks without restriction.
      • true: Reject all symbolic links with a 403 Forbidden response. Symlinks are detected during path traversal without following them, mitigating symlink-based escape attacks.
      • "if_not_owner": Allow symlinks only if owned by the same user as the target file (Unix only; treated as true on non-Unix systems).
    • Default: disable_symlinks false
Warning

Symlink-based attacks can bypass directory boundaries. If your root directory contains untrusted symlinks or is in a shared hosting environment, enable disable_symlinks on to protect against escape attacks.

Note
  • Symlink detection uses symlink_metadata(), which does not follow the symlink, so no file I/O is performed on the symlink target.
  • When enabled, symlinks are detected at each path component level during traversal, not just at the final target.
  • if_not_owner mode is Unix-specific and requires the symlink and target to have the same owner UID.

Configuration example:

example.com {
    root /srv/www/example
    disable_symlinks
}

# Allow symlinks only in a specific virtual host
uploads.example.com {
    root /srv/uploads
    disable_symlinks if_not_owner
}

# Allow symlinks (default, backward compatible)
legacy.example.com {
    root /srv/www/legacy
    disable_symlinks false
}

File handle reuse

Ferron reuses file handles (and I/O errors) for static file responses to reduce file I/O overhead for at most 200 milliseconds after the first request. This is done by caching file metadata and the open file handle in memory. The cache is keyed by the file path and is invalidated when the file is modified, deleted, or replaced.

Observability

Metrics

Static file serving

MetricTypeAttributesDescription
ferron.static.files_servedCounterferron.compression ("identity", "gzip", "br", "deflate", "zstd"), ferron.cache_hit ("true" or "false")Number of static files served
ferron.static.bytes_sentHistogramferron.compression ("identity", "gzip", "br", "deflate", "zstd"), ferron.cache_hit ("true" or "false")Bytes sent for static file responses. Buckets: 1KB, 10KB, 100KB, 1MB, 10MB, 100MB
ferron.static.responsesCounterhttp.response.status.code (HTTP response status code), ferron.static.outcome (static file serving outcome)Static-file responses across normal, conditional, range, and error paths

Access log fields

The static file serving module and the file resolution stage contribute the following fields to the HTTP access log line:

FieldTypeDescription
ferron.static.file_pathstringAbsolute file path served.
ferron.static.file_path_precompressedstringThe precompressed file path (if applicable).
ferron.static.dir_pathstringDirectory path when a listing is served.
ferron.file_resolve.request_pathstringDecoded request path being resolved (error paths only).
ferron.file_resolve.root_pathstringConfigured document root (error paths only).
ferron.file_resolve.outcomestringResolution outcome: forbidden, bad_request, or error (error paths only).
ferron.file_resolve.last_candidate_pathstringLast filesystem path attempted before failure (error paths only).

Trace spans

The file resolution span (ferron.pipeline.file_resolve) captures the resolution process before any file-serving stage runs:

AttributeTypeDescription
ferron.file_resolve.request_pathstringThe decoded request URI path.
ferron.file_resolve.root_pathstringThe configured document root.
ferron.file_resolve.outcomestringresolved, not_found, forbidden, bad_request, or error.
ferron.file_resolve.resolved_pathstringThe resolved filesystem path (success only).
ferron.file_resolve.last_candidate_pathstringThe last path attempted before failure (error only).

The static file stage sets the following attributes on its ferron.stage.static_file span:

AttributeTypeDescription
http.response.status_codeintHTTP status code of the file response.
ferron.static.file_pathstringThe file path relative to the document root.
ferron.static.file_path_precompressedstringThe precompressed file path (if applicable).
ferron.static.precompressedboolWhether a precompressed variant of the file was served.

The directory listing stage sets the following attributes on its ferron.stage.directory_listing span:

AttributeTypeDescription
http.response.status_codeintHTTP status code of the response.
ferron.static.dir_pathstringThe directory path being listed.

Best practices

The following best-practice check is reported by ferron doctor for directives on this page.

  • directory_listing enabled — Auto-generated directory indexes expose file structure. Enable only for intentionally public file listings.