Skip to content

Static files

StaticFileHandler serves a directory tree: it maps request paths to files under a root directory, sends them without copying them through user space when possible, and implements the HTTP caching and range semantics of RFC 9110 (conditional requests, ETag, Last-Modified, single and multiple byte ranges). It is an ordinary handler, registered like any other route.

#include <utility>

StaticFileConfig staticConfig;
staticConfig.enableDirectoryIndex = true;  // HTML listing for directories without index.html

Router router;
router.setDefault(StaticFileHandler("/var/www/site", std::move(staticConfig)));

SingleHttpServer server(HttpServerConfig{}.withPort(8080), std::move(router));
server.run();

The static file example serves any directory given on its command line:

cmake --build build --target aeronet-static-example
./build/examples/aeronet-static-example 8080 ./examples/static-assets
curl -i http://localhost:8080/somefile.txt
curl -i -H "Range: bytes=0-3" http://localhost:8080/somefile.txt

Mapping paths to files

The handler resolves the whole request path under its root directory: with root /var/www/site, GET /css/site.css serves /var/www/site/css/site.css. The route prefix is not removed, so a handler registered on /assets/* with root /var/www/site serves /assets/app.js from /var/www/site/assets/app.js.

  • The path is percent-decoded before resolution, . segments are ignored, and any .. segment is answered 404, so a request cannot leave the root through its path.
  • Hidden files and directories, whose name starts with a dot, are answered 404 and left out of listings, so /.env or /.git/config are not served. .well-known is the exception: RFC 8615 reserves it for public metadata such as ACME challenges or security.txt, so it is served and listed, while the dotfiles it contains stay hidden. Set showHiddenFiles to serve and list every dotfile.
  • Symbolic links are followed as long as their target is inside the root directory: a file or directory reached through a link that leads outside of the root is answered 404. The check resolves the links of the path once per file, then is remembered in the header cache along with the identity of the file (device and inode), so it costs nothing on the following requests and runs again when the path leads to another file.
  • The hidden-file rule also applies to the resolved path: a link to a dotfile (or into a hidden directory) inside the root is answered 404, and so is a Windows short name such as /GIT~1/config for /.git/config. Only the part below the root counts: the root directory itself may be hidden.
  • The path is resolved after the file was opened, so the server also checks that the resolved path designates the opened file (same device and inode, or volume and file index on Windows). Otherwise a link retargeted between the two steps could let a file outside of the root be served under the check of a file inside it.
  • Only regular files and directories are served: a FIFO, a socket or a device file is answered 404.
  • A directory is served through its index file (index.html by default, see withDefaultIndex()), when it has one. Otherwise, it is answered 404, unless enableDirectoryIndex is set: the handler then redirects /dir to /dir/, and lists the directory as an HTML page.
  • Only GET and HEAD are served; other methods receive 405 Method Not Allowed.

Tip

Serve a directory that contains only public files, built for that purpose, rather than a source tree or a home directory: every regular file inside it that is not hidden can be downloaded.

Transfers

Files larger than inlineFileThresholdBytes (128 KiB by default) are sent from the file: with sendfile() on plaintext connections, through kernel TLS when kTLS is active, and otherwise by reading bounded chunks that are encrypted, or framed as HTTP/2 DATA frames. Smaller files are read into the response, which is faster for them. The file is never loaded entirely in memory, whatever its size.

The content type comes from, in order: the contentTypeResolver callback when it returns a non-empty value, the file extension (a table of common types), and defaultContentType (application/octet-stream by default).

File::detectedContentType() exposes the same extension-based detection to your own code (for instance when sending a file with HttpResponse::file() or HttpResponseWriter::file()). Its built-in mapping includes common C and C++ extensions such as c, h, cpp, hpp, and cc.

Files are not compressed: store pre-compressed copies of large text assets if needed, see Compression.

Sending a single file

To send one file from a handler instead of serving a directory, use HttpResponse::file() (fixed response) or HttpResponseWriter::file() (streaming response). The sendfile example exposes two endpoints:

  • GET /static - returns the contents of a file using HttpResponse::file (fixed response).
  • GET /stream - returns the contents of a file using HttpResponseWriter::file (streaming writer API).

Build the examples and run it:

cmake -S . -B build -D CMAKE_BUILD_TYPE=Release
cmake --build build
./build/examples/aeronet-sendfile 8080 /path/to/file

If the file path argument is omitted, the example creates a small temporary file in /tmp and serves it. Fetch the file with curl:

curl -i http://localhost:8080/static
curl -i http://localhost:8080/stream

The example demonstrates both the fixed-response path (the server synthesizes a content-length header) and the streaming writer path. For plaintext sockets, the server uses the kernel sendfile(2) system call for zero-copy transmission. When TLS is enabled, the example exercises the TLS fallback that pread()s into the connection buffer and writes through the TLS transport.

Caching and conditional requests

Each response carries a strong ETag, derived from the file size and modification time, and a Last-Modified header (disable them with addEtag and addLastModified). The handler evaluates If-None-Match, If-Modified-Since, If-Match, and If-Unmodified-Since (disable with enableConditional):

  • a resource that did not change is answered 304 Not Modified, without body;
  • a failed precondition is answered 412 Precondition Failed.

The handler does not set Cache-Control: add it, for instance in a response middleware that matches your asset naming (long lifetimes for fingerprinted files, no-cache for HTML).

To save formatting work, each handler keeps the formatted headers (ETag, Last-Modified, Content-Type) of up to headerCacheCapacity files (1024 by default, least recently used evicted, 0 to disable). Every request still checks the file size and modification time, so a modified file is never served with stale headers. The cache also remembers that the file is inside the root directory: without cache, the symbolic links of the path are resolved on every request.

Ranges

With enableRange (the default), the handler answers Accept-Ranges: bytes and serves byte ranges (RFC 9110 ยง14):

  • A single range (Range: bytes=0-499, bytes=500-, bytes=-500) is answered 206 Partial Content with Content-Range.
  • Several ranges (Range: bytes=0-99,200-299) are answered 206 with a multipart/byteranges body, one part per range with its own Content-Range. Overlapping and adjacent ranges are merged first; when one range remains, it is sent as a single range.
  • An unsatisfiable range is dropped; when no range is satisfiable, the answer is 416 Range Not Satisfiable with Content-Range: bytes */<size>. A malformed Range is answered 416 too, and an unknown range unit is ignored.
  • If-Range with a validator that no longer matches returns the full file with 200.

Multipart range responses are assembled in memory, so two limits protect the server: more than maxMultipartRanges ranges (16 by default) are answered 416, and a response that would exceed maxMultipartBodySize (32 MiB by default) is replaced by the full file with 200.

Configuration

StaticFileConfig Default Effect
withDefaultIndex(name) index.html File served for a directory; empty to disable.
enableDirectoryIndex false List directories without index file as HTML.
maxEntriesToList 10,000 Maximum entries in a listing; longer listings are truncated and carry x-directory-listing-truncated: 1.
showHiddenFiles false Serve and list dotfiles. When false, they are answered 404 and left out of listings, except .well-known.
withDirectoryListingCss(css), directoryIndexRenderer default style, built-in renderer Customize listings.
enableRange, maxMultipartRanges, maxMultipartBodySize true, 16, 32 MiB Range requests.
enableConditional, addEtag, addLastModified true Validators and conditional requests.
contentTypeResolver, withDefaultContentType(type) none, application/octet-stream Content type resolution. The resolver must return views to storage that outlives the handler.
withInlineFileThresholdBytes(n) 128 KiB Files up to this size are read into the response.
withHeaderCacheCapacity(n) 1024 Formatted header cache size.

Tests