Static Serving
qaws serves files from one configured directory. It does not list directories, does not fall back to a single-page app shell, and does not proxy requests.
Methods
| Method | Behavior |
|---|---|
GET | Sends headers and body. |
HEAD | Sends the same headers as GET without the body. |
| Other methods | Returns 405 Method Not Allowed. |
Request bodies are out of scope. qaws is a static file server, not an application server.
Directory indexes
A request for / resolves to index.html inside the serve directory:
GET / -> ./public/index.htmlA request for a directory resolves to that directory’s index.html:
GET /docs/ -> ./public/docs/index.htmlIf trailing slash redirects are enabled, a directory request without the slash
redirects with 308 Permanent Redirect:
GET /docs -> 308 Location: /docs/Missing files
Missing files return 404 Not Found.
qaws does not provide SPA fallback behavior. If /app/settings is missing, it
returns 404 even when /index.html exists.
Directory listing
Directory listing is disabled. If a directory does not have an index.html,
qaws does not expose its contents.
Path normalization
qaws normalizes request paths before opening files. It rejects traversal and suspicious path forms instead of joining raw request text to the serve root.
Examples:
| Request | Result |
|---|---|
/ | Serves index.html. |
/assets/app.css | Serves the file if it exists. |
/../build.zig | Rejected. |
/assets/../../secret | Rejected. |
| paths containing backslashes | Rejected. |
Dotfiles
Dotfile path segments are denied by default:
/.env -> 403
/assets/.secret -> 403.well-known is allowed for common public metadata:
/.well-known/security.txtMIME types
qaws sends common static MIME types and falls back to
application/octet-stream.
Common examples:
| Extension | Content-Type |
|---|---|
.html | text/html; charset=utf-8 |
.css | text/css; charset=utf-8 |
.js | application/javascript; charset=utf-8 |
.json | application/json; charset=utf-8 |
.png | image/png |
.jpg, .jpeg | image/jpeg |
.svg | image/svg+xml |
.wasm | application/wasm |
| unknown | application/octet-stream |
Conditional requests
Successful file responses include Last-Modified when
http.last_modified is enabled and a weak representation ETag when
http.etag is enabled. The ETag contains the selected file’s nanosecond mtime,
size, and representation variant.
curl -i http://127.0.0.1:8080/
curl -i -H 'If-None-Match: W/"example"' http://127.0.0.1:8080/If-None-Match, including *, takes precedence over If-Modified-Since.
A matching validator returns 304 Not Modified without a body. ETags differ
between identity, gzip, and Brotli representations of the same logical path.
Byte ranges
With http.range_requests enabled, qaws accepts one normal, open-ended, or
suffix byte range:
curl -i -H 'Range: bytes=0-1023' http://127.0.0.1:8080/video.bin
curl -i -H 'Range: bytes=4096-' http://127.0.0.1:8080/video.bin
curl -i -H 'Range: bytes=-512' http://127.0.0.1:8080/video.binA satisfiable range returns 206 Partial Content with Content-Range. An
unsatisfiable range returns 416 Range Not Satisfiable with
Content-Range: bytes */<size>. A syntactically valid multipart range set is
ignored and receives the normal 200 representation because multipart
responses are not implemented.
Date-based If-Range can permit or suppress a range. qaws emits weak ETags, so
an ETag does not strongly satisfy If-Range. Ranges address the bytes of the
selected encoded representation, not the decoded identity file.
Precompressed sidecars
With http.precompressed enabled, a request for /assets/app.js may select
/assets/app.js.br or /assets/app.js.gz according to Accept-Encoding
tokens and q-values. Brotli wins over gzip, and gzip wins over identity when
qualities are equal.
curl -I -H 'Accept-Encoding: br, gzip' http://127.0.0.1:8080/assets/app.jsThe response keeps the original path’s MIME type and adds the selected
Content-Encoding plus Vary: Accept-Encoding. Metadata, ETag, content
length, and ranges describe the sidecar itself. If every available encoding,
including identity, has quality zero, qaws returns 406 Not Acceptable.
Direct requests for .br or .gz files serve those files normally without
recursive sidecar negotiation or an inferred Content-Encoding.
HEAD behavior
HEAD follows the same selection and conditional rules as GET, including
ETags, sidecars, ranges, redirects, and errors, but never emits a response
body. Content-Length continues to describe the body a corresponding GET
would send.
Default headers
qaws includes:
Server: qaws/0.2.7
X-Content-Type-Options: nosniff
ETag: W/"<mtime-ns>-<size>-identity"
Accept-Ranges: bytesnosniff is enabled by default to reduce browser MIME sniffing surprises.
Encoded responses also include Content-Encoding and
Vary: Accept-Encoding; partial responses include Content-Range.