Static files¶
StaticFiles serves files from a
directory — CSS, JavaScript, images, downloads. All file I/O runs in a thread
pool so the event loop is never blocked, and responses carry ETag,
Last-Modified, and Cache-Control headers and honour conditional and range
requests automatically.
Mounting a static directory¶
The quickest way to serve a folder is app.mount_static,
which constructs a StaticFiles handler and registers it on the app:
from veloce import Veloce
app = Veloce()
app.mount_static(prefix="/static", directory="static")
With a file at static/style.css, a request to /static/style.css now returns
its contents. The prefix is stripped from the request path and the remainder
is resolved, traversal-safely, under directory. Both arguments have defaults
(prefix="/static", directory="static"), so app.mount_static() with no
arguments serves ./static at /static.
The directory must exist and be readable when the handler is constructed —
otherwise a typo would silently 404 every asset. By default a missing or
unreadable directory raises ValueError at wiring time. If you create the
directory after constructing the app (a build/deploy step), pass
must_exist=False to downgrade the check to a warning:
Using the StaticFiles class directly¶
You can also construct StaticFiles
and attach it with app.mount. When you
mount a StaticFiles instance, the mount prefix becomes its serving prefix:
from veloce import StaticFiles, Veloce
app = Veloce()
app.mount("/assets", StaticFiles(directory="static"))
The constructor accepts these options:
| Option | Default | Effect |
|---|---|---|
directory |
required | resolved to an absolute path; it must exist and be readable unless must_exist=False is passed |
prefix |
"/static" |
the serving prefix |
html |
False |
serve index.html for the prefix root and map extensionless paths to their .html files |
directory_index |
False |
generate an HTML listing for a directory with no index.html |
must_exist |
True |
require the directory to exist and be readable at construction |
precompressed |
False |
serve a precompressed sibling when the client advertises a matching Accept-Encoding |
redirect_status |
307 |
status used for the trailing-slash redirect |
The other options are covered below.
Note
mount_static and mount register a static handler; they do not create
a named route. Static handlers are consulted during dispatch before a
request falls through to a 404. Because there is no named route, you build
asset URLs as plain path strings (see URL generation)
rather than with url_for.
Serving an index and HTML pages¶
Set html=True to serve index.html for the prefix root and to map
extensionless paths to their .html files — a request for /about will serve
about.html when present:
from veloce import StaticFiles, Veloce
app = Veloce()
app.mount("/", StaticFiles(directory="public", html=True))
This is enough to host a small static site: public/index.html answers /, and
public/about.html answers both /about.html and /about.
Directory index and trailing-slash redirect¶
from veloce import StaticFiles, Veloce
app = Veloce()
app.mount("/", StaticFiles(directory="public", html=True, redirect_status=308))
In html=True mode a directory that contains an index.html is served as a
page.
A request without a trailing slash (/docs) first redirects to the
slash-terminated form (/docs/) so the browser resolves the page's relative
links against the directory rather than its parent; the slash-terminated request
then serves docs/index.html.
The redirect is 307 Temporary Redirect by
default and preserves the query string. Pass redirect_status=308 for a
cacheable permanent redirect (only 307 and 308 are accepted — a redirect
that changes the request method would be wrong for a GET asset).
When directory_index=True is also set, an index.html still takes precedence
over a generated listing; the listing is only produced for directories that have
no index file.
Custom 404 page¶
Also in html=True mode, if a request matches no file Veloce serves a
404.html from the served root (with status 404 and text/html) when that
file exists. Without a 404.html the handler falls through so the application's
normal 404 handling runs. The 404.html page is subject to the same
symlink-containment checks as any other served file.
Directory listings¶
By default a request that resolves to a directory with no index.html is a
miss. Set directory_index=True to generate an HTML listing instead:
from veloce import StaticFiles, Veloce
app = Veloce()
app.mount("/files", StaticFiles(directory="downloads", directory_index=True))
Warning
Directory listings expose the names of every file in the folder, which is an
information-disclosure risk. It is off by default for that reason — only
enable it for directories whose contents are meant to be browsable. Hidden
entries (names beginning with .) are always omitted from the listing. An
entry whose target resolves outside the served root through a symlink is
also omitted, so a listing never links to a file the server would refuse to
serve.
Precompressed variants¶
from veloce import StaticFiles, Veloce
app = Veloce()
app.mount("/static", StaticFiles(directory="dist", precompressed=True))
Set precompressed=True to serve a precompressed sibling when the client
advertises a matching Accept-Encoding. With app.css on disk alongside
app.css.br and/or app.css.gz, a request for /static/app.css from a client
that accepts br (or gzip) is answered with the compressed file and the
appropriate Content-Encoding, while the Content-Type stays that of the
original (text/css).
The variants must be generated ahead of time (this is a serve-only feature; it
never compresses on the fly). br is preferred over gzip when the client's
quality values tie. ETag, conditional requests, and range requests all key off
the bytes actually sent. The feature is off by default because it adds one
stat per request to probe for the sibling.
Caching and conditional requests¶
Every served file gets an ETag derived from its path, size, and modification
time, plus a Last-Modified header and Cache-Control: public, max-age=3600.
Veloce handles conditional requests for you:
- An
If-None-Matchthat matches the current ETag returns304 Not Modifiedwith an empty body. - An
If-Modified-Sinceno older than the file's modification time returns304.
This follows RFC 9110 §13:
when both headers are present, If-None-Match takes precedence. ETags are
cached per handler (bounded, evicting least-recently-used entries) so repeated
requests for the same file do not recompute the validator.
Range requests¶
StaticFiles advertises Accept-Ranges: bytes and serves single-range
Range requests
with a 206 Partial Content response — useful for resumable downloads and
media seeking. An unsatisfiable range yields 416 Range Not Satisfiable.
Multi-range requests are not supported and fall back to the full response.
Large files¶
Files at or above 1 MiB are streamed to the client in 64 KiB chunks rather than buffered in memory, so a large download — or many concurrent ones — does not inflate the worker's memory by the file size. Smaller files are sent as a single response. These thresholds are class attributes you can tune by subclassing or assigning on the instance:
from veloce import StaticFiles, Veloce
app = Veloce()
files = StaticFiles(directory="media")
files.STREAM_THRESHOLD = 256 * 1024 # stream files >= 256 KiB
app.mount("/media", files)
Security¶
Path resolution is traversal-safe. Requests containing .., absolute path
components, or NUL bytes are rejected with 403 Forbidden before any
filesystem access. After resolving a path, Veloce dereferences symlinks and
confirms the real target is still inside the served directory, so a symlink
planted in the served tree cannot expose files elsewhere on the disk.
Warning
Serve only directories whose contents you intend to make public. Do not
point StaticFiles at an application source tree, a directory holding
secrets, or a user-writable upload folder without separate validation.
URL generation¶
Static handlers are not named routes, so asset URLs are ordinary path strings built from the mount prefix. In templates, reference them directly:
<link rel="stylesheet" href="/static/style.css">
<script src="/static/app.js"></script>
If you prefer to centralise the prefix, register a small route that builds the
path and call it through url_for:
from veloce import Request, Veloce
app = Veloce()
app.mount_static(prefix="/static", directory="static")
STATIC_PREFIX = "/static"
def static_url(filename: str) -> str:
return f"{STATIC_PREFIX}/{filename}"
@app.get("/")
async def index(request: Request):
return {"stylesheet": static_url("style.css")}
Serving a single file from the app's static folder¶
For one-off file serving, app.send_static_file_async
returns a FileResponse for a file in
the app's static_folder (default "static", resolved against the app's
package directory). It is traversal-safe via the same join logic and reads the
file in an executor, so it never blocks the event loop from an async handler:
from veloce import Request, Veloce
app = Veloce()
@app.get("/favicon.ico")
async def favicon(request: Request):
return await app.send_static_file_async("favicon.ico")
Warning
The synchronous app.send_static_file
reads the file on the calling thread and emits a DeprecationWarning when
invoked on a running event loop. From async handlers, use
app.send_static_file_async (above) or
send_from_directory_async.
app.static_folder and app.static_url_path (default "/static") hold the
defaults used when wiring up static serving; assign to them before mounting to
change the conventions for your app.
Next steps¶
- Render HTML that links to these assets — see Templates.
- Stream and download files from handlers with
FileResponse,send_file, and the asyncasync_send_file— see Requests and responses and Flask-style helpers. - Add compression and caching middleware around static responses — see Middleware.
- Full signatures are in the API reference.