{"name":"static_server.nu","source":"// examples/static_server.nu — production-style static file server.\n//\n// One-file demo that exercises the full HTTP server stack shipped\n// through Phases 1, 4, 6, 7 and 8 of HTTP_SERVER_PLAN.md:\n//\n//     tcp_listen / server_new / server_run         (Phase 1 + 4)\n//     router_get + named/wildcard captures         (Phase 6)\n//     serve_static + mime_for_ext                  (Phase 7)\n//     with_cors_default                            (Phase 6 helper)\n//     with_metrics + metrics_handler               (Phase 8)\n//     with_access_log                              (Phase 8)\n//     signal_install_shutdown (Ctrl+C / SIGTERM)   (Phase 8)\n//\n// Build & run:\n//\n//     ./build.sh                # or .\\build.bat on Windows\n//     ./nurl.sh examples/static_server.nu\n//\n// Probe from another terminal:\n//\n//     curl -i http://127.0.0.1:18080/                 # public/index.html\n//     curl -i http://127.0.0.1:18080/api/health       # JSON\n//     curl -i http://127.0.0.1:18080/metrics          # Prometheus text\n//     curl -i http://127.0.0.1:18080/../etc/passwd    # 403 (path traversal)\n//     curl -i http://127.0.0.1:18080/no-such-file     # 404\n//\n//     # Then Ctrl+C in the server's terminal — server prints\n//     # \"static server: clean shutdown\" and exits with status 0.\n//\n// On first run the server creates `./public/index.html` so a fresh\n// checkout has something to serve. Drop your own files into `./public/`\n// (CSS, images, etc.) — `serve_static` figures the Content-Type out\n// from the extension and reads the file off disk per request.\n\n// One-line batteries-included pull-in. `http_full.nu` aggregates the\n// whole HTTP stack — request parser, response builder, server,\n// router, static, auth, cookies, middleware, multipart, signals,\n// HTTP client. Per-module includes like `$ \\`stdlib/ext/http_server.nu\\``\n// still work for callers who want a leaner include surface.\n$ `stdlib/ext/http_full.nu`\n$ `stdlib/std/fs.nu`\n\n// ── Boot-time setup ──────────────────────────────────────────────────\n//\n// Plant a tiny index.html under ./public on first run so the example\n// has something to demonstrate without checking files into the repo.\n// Errors are tolerated — if the directory already exists or can't be\n// written, we proceed and let serve_static's 404 path handle it.\n\n@ setup_public_dir → v {\n  ? ! ( file_exists `public/index.html` ) {\n    : ! v IoErr dr ( dir_create `public` )\n    ?? dr { T → {} F _ → {} }\n    : s body `<!doctype html>\\n<html><head><meta charset=\"utf-8\"><title>NURL static server</title></head>\\n<body>\\n<h1>NURL static server is running.</h1>\\n<p>Try <a href=\"/api/health\">/api/health</a> or <a href=\"/metrics\">/metrics</a>.</p>\\n<p>Drop more files into <code>./public/</code> and refresh.</p>\\n</body></html>\\n`\n    : ! v IoErr wr ( write_file `public/index.html` body )\n    ?? wr {\n      T → { ( nurl_eprint `[boot] wrote public/index.html\\n` ) }\n      F _ → { ( nurl_eprint `[boot] could not write public/index.html (continuing)\\n` ) }\n    }\n  } {}\n}\n\n// ── Route handlers ───────────────────────────────────────────────────\n\n@ h_health HttpRequest req Params params → HttpResponse {\n  // Start from the text-response shape, then override Content-Type.\n  // `response_set_header` deduplicates by name (case-insensitive), so\n  // the second call REPLACES the `text/plain` default rather than\n  // appending a duplicate header.\n  : HttpResponse r ( response_text 200 `{\"ok\":true,\"server\":\"nurl-static-demo\"}\\n` )\n  ( response_set_header r `Content-Type` `application/json; charset=utf-8` )\n  ^ r\n}\n\n// `/` and `/*path` both fall through to serve_static. The router only\n// passes the request along — serve_static reads `req.path` itself,\n// strips the leading '/', joins under `public/`, rejects `..`\n// segments, picks Content-Type from the extension, returns 404 if the\n// file is missing.\n@ h_static HttpRequest req Params params → HttpResponse {\n  ^ ( serve_static `public` req )\n}\n\n// ── main ─────────────────────────────────────────────────────────────\n\n@ main → i {\n  ( setup_public_dir )\n\n  : ! TcpListener NetErr lr ( tcp_listen `127.0.0.1` 18080 )\n  ?? lr {\n    T listener → {\n      // Counter bag — captured by `with_metrics` AND by the\n      // `/metrics` route handler. Both keep the same handle alive.\n      : Metrics m ( metrics_new )\n\n      // Router build-up.\n      : Router r ( router_new )\n\n      ( router_get r `/metrics`\n        \\ HttpRequest req Params params → HttpResponse { ^ ( metrics_handler m req ) } )\n\n      ( router_get r `/api/health`\n        \\ HttpRequest req Params params → HttpResponse { ^ ( h_health req params ) } )\n\n      // Static fallback — `/` first so it picks up index.html, then\n      // `/*path` for any deeper file under public/.\n      ( router_get r `/`\n        \\ HttpRequest req Params params → HttpResponse { ^ ( h_static req params ) } )\n      ( router_get r `/*path`\n        \\ HttpRequest req Params params → HttpResponse { ^ ( h_static req params ) } )\n\n      // Middleware compose, innermost-first:\n      //     base    →  router_handle (dispatch)\n      //     cors    →  CORS headers + OPTIONS preflight\n      //     metered →  Prometheus counters update\n      //     logged  →  one stderr line per request\n      // The OUTERMOST wrapper (`logged`) is what the server sees.\n      : ( @ HttpResponse HttpRequest ) base\n        \\ HttpRequest req → HttpResponse { ^ ( router_handle r req ) }\n      : ( @ HttpResponse HttpRequest ) cors    ( with_cors_default base    )\n      : ( @ HttpResponse HttpRequest ) metered ( with_metrics      m  cors )\n      : ( @ HttpResponse HttpRequest ) logged  ( with_access_log   metered )\n\n      // Wire Ctrl+C / SIGTERM to a clean listener shutdown. Must come\n      // AFTER tcp_listen so the runtime sees a valid handle.\n      ( signal_install_shutdown listener )\n\n      ( nurl_print `static server listening on http://127.0.0.1:18080/\\n` )\n      ( nurl_print `  • /                  → public/index.html\\n` )\n      ( nurl_print `  • /<path>            → public/<path>\\n` )\n      ( nurl_print `  • /api/health        → JSON\\n` )\n      ( nurl_print `  • /metrics           → Prometheus text-exposition\\n` )\n      ( nurl_print `Ctrl+C to shut down cleanly.\\n` )\n\n      : HttpServer srv ( server_new listener logged )\n      : ! v NetErr rr ( server_run srv )\n\n      // Cleanup. Order matters — clear the signal slot first so a\n      // late signal doesn't dereference a freed listener handle, then\n      // free everything else.\n      ( signal_clear_shutdown )\n      ( server_stop srv )\n      ( router_free r )\n      ( metrics_free m )\n\n      ?? rr {\n        T _ → {\n          ( nurl_print `static server: clean shutdown\\n` )\n          ^ 0\n        }\n        F e → {\n          ( nurl_eprint `[srv] runtime error: ` )\n          ( nurl_eprint ( net_err_name e ) )\n          ( nurl_eprint `\\n` )\n          ^ 1\n        }\n      }\n    }\n    F e → {\n      ( nurl_eprint `[boot] could not bind 127.0.0.1:18080: ` )\n      ( nurl_eprint ( net_err_name e ) )\n      ( nurl_eprint `\\n` )\n      ^ 1\n    }\n  }\n}\n","bytes":7453}