Static files¶
EDRI's API Listener serves files from a directory on disk — stylesheets, scripts, images, fonts — for any request URL that no @api(...) event claims. This page describes how a request reaches a static file, what the response contains, and where the limits are.
Quick start¶
Point EDRI_ASSETS_PATH at a directory (default: assets, relative to the process working directory):
export EDRI_ASSETS_PATH=assets
assets/
└── static/
├── css/
│ └── style.css
├── js/
│ └── app.js
└── images/
└── logo.png
The URL path maps one-to-one onto the directory:
URL |
File |
|---|---|
|
|
|
|
|
|
Reference them from templates with root-absolute paths:
<link rel="stylesheet" href="/static/css/style.css">
<script src="/static/js/app.js" defer></script>
<img src="/static/images/logo.png" alt="Logo">
Relative paths (css/style.css) resolve against the current page URL and break on nested routes such as /user/group/456/12. There is no template tag for static URLs; {% url %} only generates URLs for events.
How a request reaches a static file¶
flowchart TD
Req["HTTP request<br/>GET /static/css/style.css"] --> Route{"URL matches an<br/>@api route?<br/>(any method)"}
Route -->|yes| Event["event handling<br/>(405 if method not registered)"]
Route -->|no| Json{"JSON client?<br/>X-Requested-With: XMLHttpRequest<br/>or Accept lists application/json"}
Json -->|yes| J404["404 (JSON)"]
Json -->|no| Path{"path valid and<br/>inside EDRI_ASSETS_PATH?"}
Path -->|no| E400["400 (HTML)"]
Path -->|yes| File{"regular file?"}
File -->|no| E404["404 (HTML)"]
File -->|yes| Method{"GET or HEAD?"}
Method -->|yes| Serve["200 + file contents<br/>(headers only for HEAD)"]
Method -->|no| E405["405 (HTML)<br/>Allow: GET, HEAD"]
The lookup happens in the Listener, before any event is created. Routes always win, and the static fallback has three consequences:
A route on the path shadows the file. If an event is registered at
/cssforPOST, aGET /cssreturns405 Method Not Allowed, not a file namedassets/css.A dynamic segment shadows a whole level. With an event at
/docs/{slug}, the URL/docs/readme.txtis routed to that event withslug="readme.txt";assets/docs/readme.txtis never served. Keep assets under a prefix no event uses (for example/static/).JSON clients never get static files. A request carrying
X-Requested-With: XMLHttpRequest, or whoseAcceptheader listsapplication/json(includingapplication/json, text/plain, */*, the default of axios), receives a JSON404even when the file exists. A browser navigation,<link>,<script>,<img>, or a plainfetch()without these headers is served normally. To load a JSON data file from JavaScript, sendAccept: */*or expose the data through a REST event.
Path rules¶
The path comes from the ASGI
path(already percent-decoded by the server); the query string is ignored, so/static/app.js?v=42servesapp.js.Only regular files are served. A directory — including
/— returns404; there is noindex.htmlfallback and no directory listing.The resolved path must stay inside
EDRI_ASSETS_PATH. The following return400 Bad Request:paths that escape the directory (
/../secret.txt,/a%2F..%2F..%2Fsecret.txt);symbolic links whose target lies outside the directory;
paths containing a backslash.
Symbolic links whose target stays inside the directory are followed.
A relative
EDRI_ASSETS_PATHis resolved against the process working directory on every request. Use an absolute path when the working directory of the deployed process is not under your control. The variable itself is read once, whenedri.config.settingis imported, so set it before importing EDRI.
Response¶
Static files accept GET and HEAD; other methods on an existing file return 405 Method Not Allowed with Allow: GET, HEAD. A served file returns 200 OK with:
Header |
Value |
|---|---|
|
from the file extension (see below) |
|
file size in bytes |
|
|
CORS headers |
the same |
HEAD returns the same headers without a body. For GET, the body is streamed in 1 MiB chunks; file reads run in a worker thread and do not block the Listener's event loop.
Content-Type is fixed for common web formats, independent of the Python version and the host's /etc/mime.types:
Extension |
Content-Type |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Other extensions are looked up with Python's mimetypes. A file mimetypes does not recognise, or one with a compression suffix such as .gz, is served as application/octet-stream. Because of nosniff, browsers do not guess a different type: a stylesheet or script with an unrecognised extension is rejected rather than executed.
Missing features to account for:
No caching headers. No
Cache-Control,ETag, orLast-Modifiedis sent and conditional requests (If-None-Match,If-Modified-Since) are not evaluated; every request transfers the full file.EDRI_API_CACHE_CONTROLapplies to event responses, not static files. Use versioned file names or query strings (app.js?v=42) and set cache headers at a reverse proxy.No
Rangesupport. Partial content and resumable downloads are available only for file-download events — see Downloading files.No compression. Files are sent as stored. A pre-compressed
style.css.gzis served as a separateapplication/octet-streamfile withoutContent-Encoding.
Access control¶
Static files bypass the Broker, so middleware never sees these requests. Authentication and authorization middleware (Middleware) does not protect anything under EDRI_ASSETS_PATH; every file there is public to anyone who can reach the Listener.
Serve protected files through an event instead: authorize the request in middleware or in the Manager, then return the file as a download response (Downloading files). That path also provides Range, ETag, and If-Range support.
Production¶
The built-in static serving is suitable for development and for small applications. For production traffic, let a reverse proxy or CDN serve the static prefix directly and forward everything else to EDRI. That adds caching headers, compression, and Range support without involving the Listener:
location /static/ {
alias /srv/app/assets/static/;
expires 30d;
}
location / {
proxy_pass http://127.0.0.1:8080;
}
Keeping assets under a dedicated prefix such as /static/ makes this split a single location block.
Errors¶
Situation |
Status |
Format |
|---|---|---|
File not found, or path is a directory |
|
HTML error page |
Path escapes |
|
HTML error page |
Method other than |
|
HTML error page, |
JSON client ( |
|
JSON error body |
Path claimed by an event route for another method |
|
HTML error page, |
See also¶
HTML API — HTML API and Jinja2 templates
Downloading files — file downloads through events (Range, ETag, access control)
Content Negotiation — how the JSON/HTML choice is made
Middleware — request middleware (not applied to static files)
Environment Variables —
EDRI_ASSETS_PATHDeployment — production setup