Track C — Web & Network (stdlib-net) · Zero Dependency Hackathon
A small HTTP/1.1 server, written in Go, that serves static files and a couple
of JSON endpoints. Built entirely on net/http, os, path/filepath, mime,
sync/atomic, context, and log — nothing else.
./publicContent-Type headers by file extension../../etc/passwd style)/ (index), /about (JSON info), /health
(JSON status + uptime + live request counter)/stats — self-updating HTML page pushed over
Server-Sent Events (/stats/events), showing uptime, requests served,
requests/sec, live goroutine count, and heap usage updating once a second
with no page refresh. Built entirely on net/http’s http.Flusher — no
WebSocket library, no polling JS loop.CONCURRENCY.md)SIGINT/SIGTERM — drains in-flight requests
before exiting instead of killing them mid-responseRequires Go 1.21+ (uses only the standard library, no go mod tidy needed —
go.mod has no require block).
make build # builds ./webserver
make run # builds and runs on :8080, serving ./public
make test # runs the test suite
make deps-proof # regenerates deps-proof.txt
Or directly:
go build -o webserver .
./webserver -addr :8080 -public ./public
Flags:
| Flag | Default | Purpose |
|---|---|---|
-addr |
:8080 |
listen address |
-public |
./public |
directory to serve static files from |
-shutdown-timeout |
5s |
grace period for in-flight requests |
Try it:
curl http://localhost:8080/
curl http://localhost:8080/health
curl http://localhost:8080/about
curl -I http://localhost:8080/css/style.css # check Content-Type
curl http://localhost:8080/nope # 404
curl -N http://localhost:8080/stats/events # watch raw SSE ticks
Or open http://localhost:8080/stats in a browser to see the live dashboard.
.
├── main.go entrypoint: flags, server, graceful shutdown
├── internal/server/
│ ├── router.go route table, /health and /about handlers, shared atomic counter
│ ├── static.go static file handler: path safety, content-type, Range requests, 404s
│ ├── middleware.go logging + panic-recovery middleware + NewHandler wiring
│ └── stats.go live dashboard: SSE stream (/stats/events) + HTML page (/stats)
├── tests/
│ ├── server_test.go stdlib-only test suite, imports internal/server directly
│ └── loadtest/main.go standalone sustained-load tool (stdlib, no ab/wrk needed)
├── public/ sample static site served by default
├── STDLIB.md package-you'd-normally-install -> stdlib-you-used-instead
├── CONCURRENCY.md honest writeup of the concurrency model + real load test results
├── Makefile
└── deps-proof.txt output proving zero third-party deps
Handler logic lives in internal/server (not the root package) specifically
so tests/server_test.go can import "webserver/internal/server" and test
the real code path — main.go and the tests both call the exact same
server.NewHandler(...), so there’s no drift between what’s tested and
what’s deployed.
/stats is a live, self-updating dashboard — the one part of this project
that goes beyond “serve files correctly.” It streams a JSON snapshot every
second over Server-Sent Events (/stats/events) using nothing but
net/http’s http.Flusher interface: no WebSocket package, no polling
setInterval loop on the client, no external JS. The browser opens a
single long-lived HTTP connection via the standard EventSource API and
the server writes data: {...}\n\n lines to it, flushing after each one.
This also surfaced a real bug worth knowing about: the logging middleware
wraps http.ResponseWriter in a statusRecorder to capture the status
code, but Go only promotes methods declared on an embedded interface
type — Flush() isn’t part of http.ResponseWriter, so the wrapped
writer silently stopped satisfying http.Flusher even though the
underlying writer supported it. First request to /stats/events returned
“streaming not supported.” Fixed by adding an explicit Flush() passthrough
on statusRecorder (see internal/server/middleware.go), and
TestStatsEventsStreams in the test suite now specifically guards against
this regressing silently again.
Not just written — run, for real, in this repo:
go test ./tests/... — 12 tests covering routing, 404s, content-type,
path-traversal blocking, byte-range requests, method rejection,
concurrency, and the live SSE stats stream. All passing.go test -race ./tests/... — the same suite under Go’s race detector.
Clean, zero data races detected.go run ./tests/loadtest -conns 50 -duration 12s — a real sustained
concurrent load test (see CONCURRENCY.md for full numbers): 81,382
requests, 0 failures, 6,780 req/sec, p99 latency 12.6ms, with the
request counter landing exactly on the expected value afterward.curl: verified headers, status codes,
Content-Type per extension, 404 body, path-traversal rejection, and
graceful shutdown behavior on SIGTERM (in-flight request completes,
process exits cleanly, confirmed via log output).internal/server/security.go covers the “HTTP acknowledgement / security
hardening” piece, stdlib-only (net/http, crypto/sha256,
crypto/subtle, os):
X-Content-Type-Options:
nosniff, X-Frame-Options: DENY, a real Content-Security-Policy,
Referrer-Policy: no-referrer, and Permissions-Policy. HSTS is only
sent when r.TLS != nil, since advertising it over plaintext HTTP would
be misleading (see “No HTTPS/TLS” below).http.MaxBytesReader (1 MiB cap), applied globally via WithMaxBodySize.
A client sending an oversized body gets a real 413, not an assumption.POST /admin/echo, using stdlib
r.BasicAuth() with a constant-time credential comparison
(crypto/subtle.ConstantTimeCompare over SHA-256 digests, so it doesn’t
leak timing info for mismatched-length guesses). Demo credentials only
(admin / changeme, overridable via ADMIN_USER/ADMIN_PASS env
vars) — this is not a real auth system, and isn’t presented as one.All of it is covered by tests/security_test.go: headers asserted on both
a JSON route and a static file response, 401 on missing/wrong credentials,
200 with the correct ones, 413 on an oversized body actually sent over the
wire, and 405 on the wrong method. Nothing here is a claim without a test
behind it.
CSP trade-off, stated plainly: the /stats dashboard uses an inline
<script>/<style> block (see internal/server/stats.go), so the CSP
needs 'unsafe-inline' for script-src/style-src or that page breaks.
A stricter nonce-based CSP would need moving that markup into public/
first — left as a known gap, not silently worked around.
This is a hackathon build, not a production web server. Known gaps:
ListenAndServeTLS is
straightforward (still stdlib, crypto/tls) but wasn’t in scope yet.X-Forwarded-For, so RemoteAddr in logs is whatever the
direct TCP peer is).CONCURRENCY.md’s “what’s still genuinely untested.”/about is a JSON API route; public/about.html is a separate static
page reachable at /about.html (not /about — the JSON route wins that
exact path). Intentional, but worth knowing if you’re demoing it live.This repo now covers Setup, Core Server, Static Files, Concurrency +
Polish, Testing & Docs, and HTTP acknowledgement / security hardening —
the security piece originally left for a teammate is now implemented in
internal/server/security.go and covered by tests/security_test.go (see
“The security pass” above).