Contents

HTTP demos

A tour of the web-app patterns you can build with the `contrib/http` stack. Each demo is one examples/http_*.mere file plus a small static HTML frontend.

All demos share the same build recipe:


./_build/default/bin/mere.exe -w examples/<name>.mere > /tmp/<name>.wat
wat2wasm --enable-tail-call /tmp/<name>.wat -o /tmp/<name>.wasm
node scripts/run_http_server.js /tmp/<name>.wasm
# open http://localhost:8080/ in a browser

The Node runner (scripts/run_http_server.js) provides the extern imports (fs, crypto, http, curl). Every demo listens on port 8080, so run one at a time. The outbound-only http_client_auth demo has no server component; it also runs cleanly under the plain scripts/run_wasm.js runner (they share the same http_fetch env).

The router API

The routing table is a declarative route_entry list fed to router routes not_found. Three entry constructors:


route         "GET"  "/"            home_h                 // exact
route_pattern "GET"  "/post/:slug"  post_h                 // :captures
route_prefix  "/admin"              admin_subtable         // mount subtree

segment each. Handler is str list -> str -> str (captures in source order, then req). Arity must match: /post/:slug matches /post/hello but not /post/hello/extra.

prefix. Inner entries are declared relative to the mount point ("/" = mount root, "/login" = /login). If the inner table has no match, dispatch falls through to the next outer entry.

Query strings are stripped before matching (/search?q=hi matches /search), and the raw request line is still passed to handlers that need to peek at headers or the query.

Full API + rationale in `contrib/http/router.mere`. The `http_router_demo` example exercises all three variants (including two-capture /user/:name/pet/:pet).

The catalog

demopatternline count
todo_appcookie session + KV persistence + rate-limited login328
feed_readeroutbound HTTP + RSS/Atom parsing + per-user cache350
webhook_receiverHMAC-SHA256 signature verify + Slack forward250
ci_dashboardworkflow_run event ingest + status aggregation240
link_shortenerdynamic route via not_found (regex-validated code)260
mini_blogmarkdown render + RSS 2.0 producer (round-trip with feed_reader)320
wikianonymous-edit wiki + append-only revision history380
chatServer-Sent Events broadcast + long-lived HTTP140
jwt_apiHS256 bearer auth (stateless — tokens survive restart)240
file_uploadmultipart/form-data parser + byte-identical download260
rest_notesREST verbs (PUT/PATCH/DELETE) + ETag / If-Match concurrency300
csv_exportchunked Transfer-Encoding streaming110
blogmarkdown blog on Postgres — route_prefix "/admin" + route_pattern "/post/:slug" end-to-end560
client_authoutbound HTTP — http_fetch_h, Bearer auth, per-call timeout, response header read80
gh_starsCLI — GitHub repo star count via http_fetch_h (optional GITHUB_TOKEN Bearer) + rate-limit header echo110
metrics_demoPrometheus-style /metrics endpoint — counters + gauges + with_metrics middleware. /metrics gated by with_basic_auth "metrics" "scraper" "s3cret" (Prometheus / Grafana Agent speak Basic Auth natively). Via contrib/http/metrics + contrib/http/basic_auth75
pubsub_chatmulti-instance SSE chat — POST publishes to Redis, JS-side subscriber bridges into sse_broadcast so listeners on any instance see every message (verified: 2 instances on :8080 + :8081, POST → both SSE streams)100
csrf_demoSynchronizer-token CSRF on a form POST — session-bound token, csrf_hidden_input in the form, csrf_validate guard on /update. Missing/wrong token → 403, correct → 30390
cache_demoCache-Control postures — /asset immutable / /login no-store / /page ETag + If-None-Match 304 round-trip. Via contrib/http/cache65
admin_dashIntegration dogfood — one admin console exercises router route_prefix + session + csrf + basic_auth + metrics (with_metrics middleware) + cache + redis_lock. Multi-instance race verified: instance A wins the 500 ms job, B gets 409 "contended"200
ws_chatRFC 6455 WebSocket hub — clients connect to /ws/chat, glue auto-relays messages between peers. POST /announce calls ws_broadcast for server-side pushes. Verified: A→B via native WebSocket, /announce reaches both90

todo_app

Signup / login / logout with sha256-hashed passwords. Per-user persistent todos stored in a log-structured KV. Cookie session, brute-force throttle on the login endpoint (contrib/http/ratelimit). The whole contrib/http stack layered in one file: access_log → security_headers → cors → static → router.

Source: examples/http_todo_app.mere

feed_reader

The only demo that actively uses contrib/http/client.mere's outbound http_fetch. Users subscribe to feed URLs; a refresh call fetches each with curl (spawnSync), parses via contrib/xml + contrib/feed, and caches entries per user.

Verified against real feeds: news.ycombinator.com/rss, blog.rust-lang.org/feed.xml. 40+ entries survive server restart.

Source: examples/http_feed_reader.mere

webhook_receiver

GitHub-style signed webhook ingress. POST /webhooks/github reads the X-Hub-Signature-256 header and verifies HMAC-SHA256 over the raw body. Passing events go to an audit log and optionally forward to a configured Slack URL.

Verified: unsigned / wrong-sig → 401, valid push/pull_request/issues events → 200 + forwarded downstream.

Source: examples/http_webhook_receiver.mere

ci_dashboard

Same ingress pattern as webhook_receiver but focused on GitHub workflow_run events. Extracts nested JSON fields (via a small scan-based finder) and maintains a per-(repo, branch, workflow) status board.

Source: examples/http_ci_dashboard.mere

First demo with a dynamic route: GET / is not in the fixed route table; the not_found fallback validates the code as [A-Za-z0-9_-]+, looks it up in KV, and returns a 302 redirect while incrementing a per-code hit counter.

Source: examples/http_link_shortener.mere

mini_blog

Producer side of the XML round-trip. Admin login + POST creates markdown posts; /feed.xml emits RSS 2.0 that contrib/feed (the consumer side used by feed_reader) parses cleanly.

  • Admin: POST /api/login, POST /api/posts
  • Public: GET /, GET /post/, GET /feed.xml

Source: examples/http_mini_blog.mere

wiki

Anonymous-edit wiki. Every save is a new page//v/ key in KV — nothing is ever overwritten, so the past state is always retrievable and the "history" page is just a filtered key listing.

  • POST /api/pages {slug, title, body, author}
  • GET /page/, GET /page//v/, GET /page//history

Source: examples/http_wiki.mere

chat

Long-lived HTTP + SSE broadcast. GET /sse/chat is intercepted at the Node glue layer (contrib/http/http.glue.js) and held open; a POST to /api/messages fans out via sse_broadcast to every open EventSource.

Two curl -N /sse/chat clients receive all events from a third process in matching order — verified.

  • POST /api/messages, GET /sse/chat, GET /api/messages

Source: examples/http_chat.mere

jwt_api

Stateless bearer auth via HS256 JWTs. No server-side session table — the token payload itself carries the user claim. Signing secret is persisted in KV so restarts don't invalidate outstanding tokens.

  • POST /api/login{ token }
  • GET /api/me, GET /api/tasks, POST /api/tasks — all with Authorization: Bearer

Verified: tampered signature → 401, forged payload claiming sub=admin → 401, restart preserves tokens.

Source: examples/http_jwt_api.mere

file_upload

multipart/form-data parser written in pure Mere (contrib/http/multipart.mere). Uploaded files land in KV and are served back via GET /files/ with the right Content-Type + Content-Disposition. Text files round-trip byte-identically.

  • POST /api/upload — form-data with file field
  • GET /files/ — inline download

Source: examples/http_file_upload.mere

rest_notes

Proper REST verbs (PUT / PATCH / DELETE) + ETag optimistic concurrency. Each response carries ETag: "v"; every mutation requires If-Match: to match the server's current value or returns 412 Precondition Failed. Two clients editing the same note can't blindly clobber each other.

  • POST /api/notes, GET/PUT/PATCH/DELETE /api/notes/

Status codes matrix: 201/200/204/400/404/405/412/428.

Source: examples/http_rest_notes.mere

csv_export

Pair to chat's SSE: chat pushes small events to many subscribers, this pushes one large payload to one client. Both hijack the default "buffer then end" response cycle; the CSV handler calls http_stream_start + repeated http_stream_write to emit rows one at a time via chunked transfer encoding.

  • GET /api/logs.csv?rows=N — streamed CSV

100 000 rows / 4.3 MB in ~100 ms on the demo machine, with no full- body allocation on the Mere heap.

Source: examples/http_csv_export.mere

Shared contrib

Everything the demos import is under `contrib/`:

  • contrib/http/ — 22 modules including router (exact / :capture / prefix), client (outbound curl-based fetch with request + response headers, per-call timeout), metrics (Prometheus-style counters + gauges + auto-counting middleware), session (in-memory cookie session store — random 16-hex ids, HttpOnly; SameSite=Lax defaults), basic_auth (RFC 7617 middleware), csrf (synchronizer-token guard on top of session), cache (Cache-Control postures + ETag/If-None-Match 304), websocket (RFC 6455 hub — server-side broadcast + client auto-relay), json_body, escape, cookie, security, access_log, cors, static, multipart, sse, stream, and the Node glue
  • contrib/kv/ — log-structured KV + pipe-separated pack/unpack
  • contrib/xml/, contrib/feed/, contrib/markdown/, contrib/json/ — parsers / renderers
  • contrib/auth/jwt.mere — HS256 sign / verify
  • contrib/webhook/github.mere — GitHub signed webhook helpers
  • contrib/log/log.mere — JSON lines logger

The refactoring sweep that extracted these has cut ~350 lines of duplicated boilerplate out of the demos — a new HTTP handler typically opens with 10 imports + 20 lines of business logic.

Generated by Mere SSG (contrib/site/build.mere)