| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
A production-grade, high-performance static web file server written in Go. Built on fasthttp for maximum throughput — ~141k req/sec, 55% faster than Bun's native static server.
# Go install (requires Go 1.26+)
go install github.com/BackendStack21/static-web/cmd/static-web@latest
# Serve the current directory
static-web .
# Serve a build output directory on port 3000
static-web --port 3000 ./dist
# Scaffold a config file
static-web initFor the full flag reference, subcommand documentation, and installation options, see CLI.md.
static-web --help| Feature | Detail |
|---|---|
| In-memory LRU cache | Size-bounded, byte-accurate; ~28 ns/op lookup with 0 allocations. Optional startup preload for instant cache hits. |
| Compression | On-the-fly gzip; pre-compressed .gz/.br/.zst sidecar support; priority: br > zstd > gzip |
| HTTP/2 | Automatic ALPN negotiation when TLS is configured |
| Conditional requests | ETag, 304 Not Modified, If-Modified-Since, If-None-Match |
| Range requests | Byte ranges via custom parseRange/serveRange implementation for video and large files |
| TLS 1.2 / 1.3 | Modern cipher suites; configurable cert/key paths |
| Security headers | X-Content-Type-Options, X-Frame-Options, Content-Security-Policy, Referrer-Policy, Permissions-Policy |
| HSTS | Strict-Transport-Security on all HTTPS responses; configurable max-age |
| HTTP→HTTPS redirect | Automatic 301 redirect on the HTTP port when TLS is active |
| Method whitelist | Only GET, HEAD, OPTIONS are accepted (TRACE/PUT/POST blocked) |
| Dotfile protection | Blocks .env, .git/, etc. by default |
| Directory listing | Optional HTML directory index with breadcrumb navigation, sorted entries, human-readable sizes, and dotfile filtering |
| Symlink escape prevention | EvalSymlinks re-verified against root; symlinks pointing outside root are blocked |
| CORS | Configurable per-origin or wildcard (* emits literal *, never reflected) |
| Graceful shutdown | SIGTERM/SIGINT drains in-flight requests with configurable timeout |
| Live cache flush | SIGHUP flushes both the in-memory file cache and the path-safety cache without downtime |
HTTP request
│
▼
┌─────────────────┐
│ recoveryMiddleware │ ← panic → 500, log stack
└────────┬────────┘
│
┌────────▼────────┐
│ loggingMiddleware │ ← logs method/path/status/duration
└────────┬────────┘
│
┌────────▼────────────────────────────────────────┐
│ security.Middleware │
│ • Method whitelist (GET/HEAD/OPTIONS only) │
│ • Security headers (set BEFORE path check) │
│ • PathSafe: null bytes, path.Clean, EvalSymlinks│
│ • Path-safety cache (bounded LRU, pre-warmed) │
│ • Dotfile blocking │
│ • CORS (preflight + per-origin or wildcard *) │
│ • Injects validated path into ctx.SetUserValue │
└────────┬────────────────────────────────────────┘
│
┌────────▼────────────────────────────────────────┐
│ handler.FileHandler │
│ • Cache hit → direct ctx.SetBody() fast path │
│ • Range/conditional → custom serveRange() │
│ • Cache miss → os.Stat → disk read → cache put │
│ • Large files (> max_file_size) bypass cache │
│ • Encoding negotiation: brotli > zstd > gzip > plain │
│ • Preloaded files served instantly on startup │
│ • Custom 404 page (path-validated) │
└─────────────────────────────────────────────────┘
│
┌────────▼────────────────────────────────────────┐
│ compress.Middleware (post-processing) │
│ • Compresses response body after handler runs │
│ • Skips 1xx/204/304, non-compressible types │
│ • Respects q=0 explicit denial │
└─────────────────────────────────────────────────┘
GET /app.js
│
├─ cache.Get("/app.js") hit?
│ YES → serveFromCache (direct ctx.SetBody, no syscall) → done
│
└─ NO → resolveIndexPath → cache.Get(canonicalURL) hit?
YES → serveFromCache → done
NO → os.Stat → os.ReadFile → cache.Put → serveFromCache
When preload = true, every eligible file is loaded into cache at startup. The path-safety cache (bounded LRU) is also pre-warmed, so the very first request for any preloaded file skips both filesystem I/O and EvalSymlinks. Symlink targets are validated against the root during the preload walk — symlinks pointing outside root are skipped.
Measured on Apple M-series, localhost (no Docker), serving 3 small static files via bombardier -c 100 -n 100000:
| Server | Avg Req/sec | p50 Latency | p99 Latency | Throughput |
|---|---|---|---|---|
| static-web (fasthttp + preload) | ~141,000 | 619 µs | 2.46 ms | 469 MB/s |
| Bun (native static serve) | ~90,000 | 1.05 ms | 2.33 ms | 306 MB/s |
| static-web (old net/http) | ~76,000 | 1.25 ms | 3.15 ms | — |
With preload = true and the fasthttp engine, static-web delivers ~141k req/sec — 55% faster than Bun's native static serving, while offering full security headers, TLS, and compression out of the box.
Measured on Apple M2 Pro (go test -bench=. -benchtime=5s):
| Benchmark | ops/s | ns/op | allocs/op |
|---|---|---|---|
| BenchmarkCacheGet | 35–42 M | 28–29 | 0 |
| BenchmarkCacheGetParallel | 6–8 M | 139–148 | 0 |
Every request URL is validated through PathSafe before any filesystem access:
Set on every response including 4xx/5xx errors:
| Header | Default Value |
|---|---|
| X-Content-Type-Options | nosniff |
| X-Frame-Options | SAMEORIGIN |
| Content-Security-Policy | default-src 'self' |
| Referrer-Policy | strict-origin-when-cross-origin |
| Permissions-Policy | geolocation=(), microphone=(), camera=() |
| Strict-Transport-Security | max-age=31536000 (HTTPS only) |
Only GET, HEAD, and OPTIONS are accepted. All other methods (including TRACE, PUT, POST, DELETE, PATCH) receive 405 Method Not Allowed. This means TRACE-based XST attacks are impossible by design.
| Mitigation | Value |
|---|---|
| ReadTimeout | 10 s (covers full read phase including headers — Slowloris protection) |
| WriteTimeout | 10 s |
| IdleTimeout | 75 s (keep-alive) |
| MaxRequestBodySize | 1024 bytes (static file server needs no large request bodies) |
| MaxConnsPerIP | Configurable (default 0 = unlimited) |
Copy config.toml.example to config.toml and edit as needed. The server starts without a config file using sensible defaults.
| Key | Type | Default | Description |
|---|---|---|---|
| addr | string | :8080 | HTTP listen address |
| tls_addr | string | :8443 | HTTPS listen address |
| redirect_host | string | — | Canonical host used for HTTP→HTTPS redirects |
| tls_cert | string | — | Path to TLS certificate (PEM) |
| tls_key | string | — | Path to TLS private key (PEM) |
| read_timeout | duration | 10s | Full request read deadline (covers headers; Slowloris protection) |
| write_timeout | duration | 10s | Response write deadline |
| idle_timeout | duration | 75s | Keep-alive idle timeout |
| shutdown_timeout | duration | 15s | Graceful drain window |
| max_conns_per_ip | int | 0 | Max concurrent connections per IP (0 = unlimited) |
| Key | Type | Default | Description |
|---|---|---|---|
| root | string | ./public | Directory to serve |
| index | string | index.html | Index file for directory requests |
| not_found | string | — | Custom 404 page (relative to root) |
| max_serve_file_size | int | 1073741824 | Max file size to serve in bytes (0 = unlimited; default 1 GB). Files exceeding this limit receive 413. |
| Key | Type | Default | Description |
|---|---|---|---|
| enabled | bool | true | Toggle in-memory LRU cache |
| preload | bool | false | Load all eligible files into cache at startup |
| max_bytes | int | 268435456 | Cache size cap (bytes) |
| max_file_size | int | 10485760 | Max file size to cache (bytes) |
| ttl | duration | 0 | Entry TTL (0 = no expiry; flush with SIGHUP) |
| gc_percent | int | 0 | Go GC target percentage (0 = use Go default of 100) |
| Key | Type | Default | Description |
|---|---|---|---|
| enabled | bool | true | Enable compression |
| min_size | int | 1024 | Minimum bytes to compress |
| level | int | 5 | gzip level (1–9) |
| precompressed | bool | true | Serve .gz/.br/.zst sidecar files |
| max_compress_size | int | 10485760 | Max body size for on-the-fly gzip compression in bytes (0 = unlimited; default 10 MB) |
| Key | Type | Default | Description |
|---|---|---|---|
| immutable_pattern | string | — | Glob for immutable assets |
| static_max_age | int | 3600 | Cache-Control max-age for non-HTML (seconds) |
| html_max_age | int | 0 | Cache-Control max-age for HTML (seconds) |
| enable_etags | bool | true | Enable ETag generation and If-None-Match validation for cache revalidation |
| Key | Type | Default | Description |
|---|---|---|---|
| block_dotfiles | bool | true | Block .-prefixed path components |
| directory_listing | bool | false | Enable directory index listing |
| cors_origins | []string | [] | Allowed CORS origins (["*"] for wildcard) |
| csp | string | default-src 'self' | Content-Security-Policy value |
| referrer_policy | string | strict-origin-when-cross-origin | Referrer-Policy value |
| permissions_policy | string | geolocation=(), microphone=(), camera=() | Permissions-Policy value |
| hsts_max_age | int | 31536000 | HSTS max-age in seconds (HTTPS only; 0 disables) |
| hsts_include_subdomains | bool | false | Add includeSubDomains to HSTS header |
All environment variables override the corresponding TOML setting. Useful for containers.
| Variable | Config Field |
|---|---|
| STATIC_SERVER_ADDR | server.addr |
| STATIC_SERVER_TLS_ADDR | server.tls_addr |
| STATIC_SERVER_REDIRECT_HOST | server.redirect_host |
| STATIC_SERVER_TLS_CERT | server.tls_cert |
| STATIC_SERVER_TLS_KEY | server.tls_key |
| STATIC_SERVER_READ_TIMEOUT | server.read_timeout |
| STATIC_SERVER_WRITE_TIMEOUT | server.write_timeout |
| STATIC_SERVER_IDLE_TIMEOUT | server.idle_timeout |
| STATIC_SERVER_SHUTDOWN_TIMEOUT | server.shutdown_timeout |
| STATIC_SERVER_MAX_CONNS_PER_IP | server.max_conns_per_ip |
| STATIC_FILES_ROOT | files.root |
| STATIC_FILES_INDEX | files.index |
| STATIC_FILES_NOT_FOUND | files.not_found |
| STATIC_FILES_MAX_SERVE_FILE_SIZE | files.max_serve_file_size |
| STATIC_CACHE_ENABLED | cache.enabled |
| STATIC_CACHE_PRELOAD | cache.preload |
| STATIC_CACHE_MAX_BYTES | cache.max_bytes |
| STATIC_CACHE_MAX_FILE_SIZE | cache.max_file_size |
| STATIC_CACHE_TTL | cache.ttl |
| STATIC_CACHE_GC_PERCENT | cache.gc_percent |
| STATIC_COMPRESSION_ENABLED | compression.enabled |
| STATIC_COMPRESSION_MIN_SIZE | compression.min_size |
| STATIC_COMPRESSION_LEVEL | compression.level |
| STATIC_COMPRESSION_MAX_COMPRESS_SIZE | compression.max_compress_size |
| STATIC_HEADERS_ENABLE_ETAGS | headers.enable_etags |
| STATIC_SECURITY_BLOCK_DOTFILES | security.block_dotfiles |
| STATIC_SECURITY_CSP | security.csp |
| STATIC_SECURITY_CORS_ORIGINS | security.cors_origins (comma-separated) |
Set tls_cert and tls_key to enable HTTPS:
[server]
addr = ":80"
tls_addr = ":443"
redirect_host = "static.example.com"
tls_cert = "/etc/ssl/certs/server.pem"
tls_key = "/etc/ssl/private/server.key"When TLS is configured:
Place .gz, .br, and .zst sidecar files alongside originals. The server serves them automatically when the client signals support:
public/ app.js app.js.gz ← served for Accept-Encoding: gzip app.js.br ← served for Accept-Encoding: br (preferred) app.js.zst ← served for Accept-Encoding: zstd (fastest decompress) style.css style.css.gz style.css.br style.css.zst
Generate sidecars from the Makefile:
make precompress # runs gzip, brotli, and zstd on all .js/.css/.html/.json/.svgNote: On-the-fly brotli encoding is not implemented. Only .br sidecar files are served with brotli encoding. Zstandard is available both as pre-compressed sidecar files and on-the-fly compression.
| Signal | Action |
|---|---|
| SIGTERM | Graceful shutdown (drains in-flight requests up to shutdown_timeout) |
| SIGINT | Graceful shutdown |
| SIGHUP | Flush in-memory file cache and path-safety cache; re-reads config pointer in main |
Note: SIGHUP reloads the config pointer in main but the live middleware chain holds references to the old config. A full restart is required for config changes to take effect. SIGHUP is useful for flushing both the file cache and the path-safety cache without downtime.
make build # compile → bin/static-web
make release # compile stripped binary → bin/static-web
make install # install to $(GOPATH)/bin
make run # build + run with ./config.toml
make test # go test -race ./...
make bench # go test -bench=. -benchtime=5s ./...
make lint # go vet ./...
make precompress # generate .gz/.br sidecars for public/
make clean # remove bin/go test -race ./... # full suite with race detector
go test -run TestPathSafe ./internal/security/... # specific test
go test -bench=BenchmarkCacheGet -benchtime=10s ./internal/cache/All PRs must pass:
go build ./... # clean compile
go vet ./... # static analysis
go test -race ./... # all tests, race-free| Limitation | Detail |
|---|---|
| Brotli on-the-fly | Not implemented. Only pre-compressed .br sidecar files are served. |
| SIGHUP config reload | Reloads the config struct pointer in main only. Live middleware chains hold old references — full restart required for config changes to propagate. |
| Back | FazBrowse Home | New Git URL |