Principles¶
These are the rules servery lives by. They are deliberately opinionated. When a design decision is unclear, re-read Principle 0; it usually settles the question, and where it does not, the scope rubric in §7 does.
0. Zero dependencies. Pure standard library. Forever.¶
The servery CORE has zero third-party (PyPI) runtime dependencies and depends only on the Python standard library. This is non-negotiable and outranks every other principle here. It is the soul of the project, not a nice-to-have.
If pip install servery pulls in anything other than servery itself, we have
failed — no matter how good the feature was.
Refinement (transport tiers). The zero-PyPI mandate is on the core. The
optional, opt-in HTTP/2 and HTTP/3 transport tiers (docs/TRANSPORTS.md) may
use vetted libraries behind extras (servery[http2], servery[http3]) — but only
after preferring two cheaper sources first: a stdlib path, and binding
already-present OS libraries via ctypes (stdlib) rather than adding a PyPI
dependency (e.g. system OpenSSL libcrypto/libssl or Windows CNG for QUIC
crypto). The order of preference is therefore: stdlib → OS library via ctypes
→ vetted PyPI extra (explicit opt-in only). A bare pip install servery stays
empty-dependencies forever; the core never imports any of that.
This is the point, not a constraint we tolerate. The entire value proposition
is "you already have everything you need." Every dependency we could add is a
dependency the user could have added themselves to http.server; adding it for
them is not the product.
The rule: when a desirable feature appears to need a dependency, the answer is one of exactly three things, in order of preference:
- Find a stdlib path. The standard library is enormous —
ssl,hmac,secrets,email,urllib,mimetypes,socketserver,http,base64,hashlib,gzip,zipfile,tarfile,json,html,string.Template— most of what a file server needs is already there. - Scope the feature down to the part that is reachable with the stdlib, and document the boundary.
- Use a narrowly scoped optional extra or drop the feature. An extra must remain outside the default/core install, publish its complete dependency and native-code surface, and justify why stdlib and OS-library paths are insufficient. A missing feature is cheaper than a betrayed core promise.
Never add a required third-party dependency to the core. Optional transport extras follow the explicit tier policy above; there is no hidden "just this once" exception for the default install.
Consequences we accept on purpose¶
The zero-dep mandate has real, sharp consequences. We name them up front so nobody re-litigates them later:
- No full-fidelity Markdown or syntax highlighting. The stdlib has no
Markdown parser and no lexer library, so servery will never match CommonMark
or Pygments — and it will not vendor either to try. What it does ship (since
v1.7, behind
--preview) is a small in-house subset:servery._markdowncovers the common block and inline constructs and escapes all raw HTML;servery._highlighttokenizes Python with the stdlib's owntokenizeand everything else with a bounded per-language scanner. This is option 2 of the rule above — scope the feature down to the part that is reachable with the stdlib, and document the boundary — the same call already made for the QR encoder (_qr.py), HPACK (http2/hpack.py), and RSA/DER (_certgen.py). A GFM-exact renderer is still out. - No
cgi/cgi.FieldStoragefor uploads. Thecgimodule was removed in Python 3.13. Since we target 3.13+ (see §3),cgiis simply not available to us, and we would not use it even on older interpreters. Therefore multipart/form-dataupload parsing must be hand-rolled — boundary splitting against theContent-Typeboundary parameter, with per-part headers parsed viaemail.parser/email.message, andurllib.parse.parse_qslfor simple url-encoded forms. This is a deliberate, owned piece of code, not an accident. It must be written carefully (streaming where possible, strict on boundaries, bounded in memory). - Self-signed TLS certs ARE a stdlib path (a §0 rule-1 win, not a rule-2/3
loss). The stdlib
sslmodule can use TLS but has no X.509/keygen API, so this once looked like a "scope it down to a documentedopensslcommand" consequence. It is not: pure-Python RSA-2048 + a hand-rolled DER encoder + PKCS#1 v1.5 signing (pow/hashlib/secrets) mint a self-signed cert with zero dependencies (_certgen.py), and--tls-self-signedships it. Note the discipline — only keygen + signing-our-own-cert once at startup is hand-rolled; the TLS handshake/record encryption stay in OpenSSL viassl, and the side-channel concerns of hand-rolled crypto don't apply to one-shot self-cert generation. A narrow pure-Python ACME HTTP-01 acquisition path now ships without an extra. The production boundary is continuous renewal: scheduled jittered retry, atomic persistence, expiry visibility, and live TLS replacement must pass the production-edge checklist before ACME is described as unattended. Protocol code remains stdlib; noservery[acme]extra is required. - No async framework, no template engine, no rich-text/UI toolkit. Our
listing UI is server-rendered HTML/CSS built with stdlib string tooling
(
html.escape,string.Template), shipped inline. No build step, no asset pipeline. - We build on the real stdlib base. servery extends
http.server.SimpleHTTPRequestHandlerand serves viaThreadingHTTPServer/ThreadingHTTPSServer(the stdlib already gives us threading, HTTPS viassl,If-Modified-Sincehandling, and directory redirects). We add what stdlib lacks — rich listing, auth, upload, andRangesupport (stdlib does not honorRangetoday) — rather than reinventing the HTTP plumbing.
1. Safe by default, honest about limits¶
The default profile is a dev / LAN / ad-hoc-sharing tool. The separate production profile is intended to become a directly exposed, single-service edge only after every production release gate passes. We do not transfer that responsibility to a required reverse proxy or process manager, and we do not describe unfinished production machinery as shipped.
- Bind to localhost by default. Serving the whole network is an explicit,
opt-in choice (
--host 0.0.0.0), not the default.http.server's historical default of binding broadly is a footgun we decline to inherit. - Path-traversal protection. No request path may escape the served root.
Resolve and verify every translated path stays within the configured
directory; reject
.., encoded traversal, and absolute-path tricks. - Careful symlink handling. Decide explicitly (and configurably) whether symlinks may point outside the root; default to not following links out of the served tree. Never let a symlink become a traversal bypass.
- Constant-time auth comparison. Compare credentials with
hmac.compare_digest, never==. Generate any tokens/nonces withsecrets. - Loud about HTTP Basic Auth without TLS. Basic Auth over plain HTTP sends credentials in base64 (i.e. effectively in the clear). If auth is enabled without TLS, we warn loudly at startup. We never imply auth-over-HTTP is private.
- Upload is opt-in and bounded. Writing files is off unless explicitly enabled. When on, enforce size limits, refuse path traversal in filenames, and never overwrite outside the upload target.
Production boundary: bounded admission, overload recovery, worker supervision, graceful replacement, observability, certificate renewal, and hostile-input assurance are in scope for the production profile. A general WAF, multi-tenant isolation, distributed coordination, and arbitrary upstream routing remain out. Until the production checklist passes, the honest current posture remains safe defaults for trusted networks; production target in progress—not a recommendation that another edge is required.
1a. Standards-compliant by default (RFC 9110 / 9111 / 9112)¶
servery is a conformant HTTP/1.1 origin server, not an HTTP/1.0 toy. Where the stdlib base is RFC 2616-era and HTTP/1.0-by-default, servery closes the gap to modern HTTP semantics with the stdlib alone:
- HTTP/1.1 with persistent connections (
protocol_version = "HTTP/1.1"), honoringConnection: close, with every streamed body correctly framed (chunked orConnection: close) — RFC 9112. - Correct conditionals and validators: the full
If-Match/If-Unmodified-Since/If-None-Match/If-Modified-Sinceprecedence ladder, a weakETag,304/412with validator echo, andRange/206/416— RFC 9110 §13/§14, §8.8. - Correct caching, dates, and metadata:
Cache-Control, IMF-fixdateDate,Content-Type/Content-Length,Content-Dispositionwith RFC 6266/8187 filenames.
The map of exactly what each RFC requires, what the base already gives us, and
what servery adds lives in STANDARDS.md. This principle is subordinate to
Principle 0: any compliance target reachable only by adding a core dependency
is out of the core and recorded as such. The core therefore speaks HTTP/1.1
and its TLS ALPN advertises only http/1.1. HTTP/2 (RFC 9113) and HTTP/3 (RFC
9114) are not part of the zero-dep core, but they are no longer flatly out: they
are optional, opt-in transport tiers (docs/TRANSPORTS.md) — h2 is feasible in
pure stdlib (the preferred path) with an optional h2 backend; h3 is offered via
aioquic or an experimental ctypes→OpenSSL ≥ 3.5 native backend. ALPN/Alt-Svc
advertise h2/h3 only when the corresponding tier is enabled. Standards
conformance never outranks the zero-dep mandate; it is what we achieve within it.
1b. Secure web-facing defaults¶
servery renders HTML listings containing user-controlled filenames, so it is an XSS sink by construction; its defaults must be safe for a web-facing surface out of the box — not opt-in hardening:
X-Content-Type-Options: nosniffon every response (a.txtmust not be sniffed intotext/html).- Context-correct output escaping:
html.escape(name)withquote=Truefor every value that could land in HTML text or an attribute,urllib.parse.quotefor URL targets, and control-character stripping in filenames — never the base'squote=False. - Defense-in-depth headers on servery-generated pages: a tight
Content-Security-Policy(listings/error pages only, never on served user HTML),Referrer-Policy, andStrict-Transport-Securityonly under TLS. - Safe operational defaults: a per-request socket timeout (Slowloris mitigation) on by default; fail-closed path resolution (404, never a 403 leak).
These are all send_header/stdlib calls — zero-dep — and on by default, with
a --no-security-headers escape hatch. This principle alone does not establish
production readiness. Production also requires the bounded-resource,
lifecycle, observability, renewal, and assurance gates in Principle 1 and the
production-edge checklist. Like Principle 1a, it is subordinate to Principle 0:
every default here is reachable with the standard library alone.
2. File server, not framework — scope discipline¶
servery serves a directory or hosts one operator-provided WSGI/ASGI application. It does not help you build that application.
- No servery-defined routes or framework API, no app object, no middleware system, and no plugin API for constructing application dispatch. The operator may supply a WSGI/ASGI callable whose routes and dependencies belong to it.
- The built-in content model remains {directory, files, listing, browser}. Dynamic behavior comes only from the single callable the operator explicitly supplies; servery does not grow a second application model.
- Internal abstractions (our own listing template, our own upload parser) are fine. Exposing them as an extension surface for building apps is not.
This discipline is what keeps servery finishable and honestly describable. The moment we add routing, we are a worse Flask; we will not.
3. Python version support policy¶
Minimum supported Python: 3.13. We track the CPython upstream support window and support every non-EOL CPython at or above our minimum.
Rationale (as of mid-2026):
- Python 3.9 is already EOL (October 2025). 3.10 and 3.11 reach EOL in October 2026 — i.e. within months of this writing. Building a brand-new project on versions that are EOL or about to be is poor stewardship.
- The
cgimodule was removed in 3.13. Targeting 3.13+ means we live in the post-cgiworld natively: we hand-roll multipart parsing once, for the versions we actually support, instead of carrying a conditionalcgipath for legacy interpreters. The constraint and the floor reinforce each other. - 3.13 began the new support cadence (two years full support, three years security) and is supported through October 2029, giving servery a long runway without us chasing a moving floor.
- A young file-server project gains little from supporting interpreters its users are being told to leave. We would rather use modern stdlib cleanly than straddle five-year-old versions.
Policy mechanics:
- We declare
requires-python = ">=3.13". - We test against every supported CPython from the floor up (3.13 and each newer release as it ships).
- We raise the floor only deliberately, in a minor release, with a note in the changelog — never silently.
- (Open question for the requirements authors: if a concrete user need for 3.11/3.12 emerges, is the cost of a hand-rolled-multipart-only backport worth lowering the floor? Default answer today: no.)
3a. Free-threading is a first-class target¶
servery must run correctly and well on the free-threaded (no-GIL) CPython
builds (3.13t / 3.14t), not merely tolerate them. A threaded file server is
exactly the workload free-threading is meant to speed up, and the multiplexing
HTTP/2 backend (docs/TRANSPORTS.md) makes thread-safety load-bearing rather than
incidental.
- No module-level mutable state. Shared state lives on the
Config(frozen) or on per-request/per-connection objects — never in module globals that concurrent threads could race. - Do not rely on the GIL for correctness. Anything previously "safe because the GIL made it atomic" (dict mutation, counter increments, lazy caches) must be made explicitly safe — immutable, thread-local, or guarded by a lock — because on a free-threaded build that atomicity is gone.
- Test on free-threaded builds. The suite (
ARCHITECTURE.md§7) runs on 3.13t/3.14t in CI alongside the default builds; concurrency-sensitive paths (listing, range, upload, and any h2 stream table) get explicit multi-threaded tests.
This is subordinate to Principle 0 — it is reached with the stdlib alone
(threading, concurrent.futures) — and reinforces Principle 5: code that holds
no hidden shared state is also the code that is easiest to read and hack.
4. CLI-and-importable ergonomics¶
servery is equally a command and a library. Neither is an afterthought.
- Three entry points, same behavior:
python -m servery, theserveryconsole script, andimport servery. The module and the script are thin wrappers over the same public API. - The library is the product, the CLI is a view of it. Configuration lives
in plain objects/params; the CLI parses argv (via stdlib
argparse) into exactly those params. Anything you can do from the command line you can do from Python. - Sensible zero-config defaults. Bare
serveryserves the current directory on localhost with the rich listing and no auth/upload/TLS. Every nicety is one obvious flag away. - Composable for embedders. Someone should be able to
import servery, construct a handler/server, and drop it into their ownsocketserversetup — without us having become a framework to allow it. - Friendly failure. Clear startup messages (bound address, whether auth/TLS are on, the loud no-TLS-auth warning). Errors say what to do, not just what broke.
5. Boring, readable, hackable¶
Because we cannot reach for dependencies, our own code is the asset. It must be the kind of pure-Python that a user could read in an afternoon and patch themselves — which is, after all, why they chose a pure-Python tool. Prefer clarity over cleverness; prefer the obvious stdlib call over a hand-optimized trick.
6. Stable, small surface¶
A small tool earns trust by not churning. Keep the public API and CLI flags small and stable; deprecate slowly and loudly; treat each new flag as a cost. Every feature we don't add is a feature we never have to maintain, document, or secure.
7. The scope rubric — how we decide if a feature is in¶
Run every proposed feature through this filter, in order. A feature must pass all gates to be in scope.
- Zero-dependency gate. Can it be built with the standard library alone, without vendoring a parser/engine/toolkit? If no → out (or scope it down to the stdlib-reachable subset). This gate is absolute; nothing overrides it.
- Server-lane gate. Is it about serving files or correctly hosting and operating one supplied WSGI/ASGI application, or is it about building that application (routes, app logic, middleware)? Framework construction is out; server lifecycle and protocol adaptation are in.
- Safe-default gate. Does it preserve the safe LAN default and, when used by the production profile, carry measurable resource, lifecycle, failure, and observability gates? If not, redesign it before inclusion.
- Smallness gate. Does the benefit justify the permanent maintenance, documentation, and security surface? When in doubt → out; the default answer to "should we add this?" is no.
Worked examples:
- Sortable listing with sizes/dates → stdlib (
os.scandir,datetime, string templating) ✅, file-server-lane ✅, safe ✅, clearly justified ✅ → in (it's the headline feature). - Basic auth → stdlib (
base64,hmac.compare_digest) ✅, file-server-lane ✅, safe-by-default with loud no-TLS warning ✅ → in. - Upload → stdlib (hand-rolled multipart via
email.parser) ✅, file-server-lane ✅, safe if opt-in + bounded + traversal-checked ✅ → in, opt-in. - HTTPS → stdlib (
ssl,ThreadingHTTPSServer) ✅ → in. - Range requests → stdlib ✅, file-server-lane ✅ (stdlib lacks it; we add it) → in.
- Markdown rendering / syntax highlighting → a conformant one needs a
non-stdlib parser ✗ → out; a bounded in-house subset is stdlib-only ✅,
file-server-lane ✅ (looking inside a file you are about to download), safe if
opt-in + escaped + URL-allowlisted ✅ → in, opt-in (
--preview, v1.7). - User-defined routes / app endpoints → framework-lane ✗ → out.
- Built-in TLS via a vendored crypto lib → dependency ✗ → out (use stdlib
ssl; that's the only TLS we ship).