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 answered404, so a request cannot leave the root through its path. - Hidden files and directories, whose name starts with a dot, are answered
404and left out of listings, so/.envor/.git/configare not served..well-knownis the exception: RFC 8615 reserves it for public metadata such as ACME challenges orsecurity.txt, so it is served and listed, while the dotfiles it contains stay hidden. SetshowHiddenFilesto 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/configfor/.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.htmlby default, seewithDefaultIndex()), when it has one. Otherwise, it is answered404, unlessenableDirectoryIndexis set: the handler then redirects/dirto/dir/, and lists the directory as an HTML page. - Only
GETandHEADare served; other methods receive405 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 usingHttpResponse::file(fixed response).GET /stream- returns the contents of a file usingHttpResponseWriter::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:
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 answered206 Partial ContentwithContent-Range. - Several ranges (
Range: bytes=0-99,200-299) are answered206with amultipart/byterangesbody, one part per range with its ownContent-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 SatisfiablewithContent-Range: bytes */<size>. A malformedRangeis answered416too, and an unknown range unit is ignored. If-Rangewith a validator that no longer matches returns the full file with200.
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¶
- Path resolution, hidden files, symbolic links, ranges, multipart assembly, conditionals, and header cache: static-file-handler_test.cpp.
- End to end, including multi-range responses and
If-Range: tests/http-core_test.cpp.