feat(framework): v1 polish — helpers, 405+Allow, HEAD, Logging, set_header
- App.get/post/put/delete_(pattern, take h: Handler) — the take-interface shape probe-proven release + ASan before landing; retires plan-16 deviation 1; delete_ because delete is the query keyword - dispatch matches path-first: wrong method on a known path answers 405 with Allow in registration order; unknown path stays 404 - HEAD routed as GET, body suppressed, Content-Length names the body a GET would carry (serialize gains head_only) - Logging middleware (request line to stderr) ships in router/ - set_header(mut r, name, value) — the builder escape hatch - web-app registers through the helpers (dogfood); README documents all - gate grows 14 -> 16: 405+Allow, HEAD-vs-GET content-length equality - all gates green: web-app 16/0, woc-test 540/0, oop-e2e 89/0, deps-accept 8/0, log-watcher 7/0, employee 8/0 - board/story: iteration 17 parked (spec+plan ready on library-internal), 16 carries the v1-polish landing, order list updated Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
81a9f882b5
commit
d84b72f0b4
11 changed files with 184 additions and 64 deletions
|
|
@ -19,41 +19,25 @@ Statuses: ✅ **done** · 🔄 **in progress** · ⬜ **pending** · ⏸ **hold*
|
|||
|
||||
## ▶ NEXT PLAN
|
||||
|
||||
**The web framework becomes a first-class library — iteration 17.** The goal
|
||||
shifted 2026-08-20: the previous NEXT PLAN ("make log-watcher executable —
|
||||
nothing else") was met in full 2026-08-15 (milestone record below), and the
|
||||
driving workload is now `docs/examples/writeonce-framework` consumed by
|
||||
`docs/examples/web-app` through `[deps]`. Iteration 17's forks are settled
|
||||
(decisions + framework/compiler/VM/GC impact in
|
||||
[the iteration](stories/language-runtime-database/17-library-projects-internal.md));
|
||||
what remains, in order:
|
||||
**Framework v1 — a polished micro-framework (routing, middleware,
|
||||
`Req`/`Resp`), nothing MVC-scale.** Directive 2026-08-20: iteration 17
|
||||
(library kind + `internal/`) is **parked** — spec + plan approved and ready
|
||||
on branch `library-internal` — and the framework itself is the work. The
|
||||
v1-polish slice landed the same day (branch `framework-v1`): registration
|
||||
helpers `get/post/put/delete_` (the take-Handler shape, probe-proven),
|
||||
405 + `Allow` on wrong-method hits, HEAD served as GET with the body
|
||||
suppressed, a `Logging` middleware, `set_header`; `just web-app` grew to
|
||||
16/0. En route it exposed and fixed a real emitter bug: a Text-typed
|
||||
single-segment interpolation of a place (`"${r.method}"`, `r` a loop
|
||||
borrow) crossed `let`/assignment boundaries uncopied — aliased the field,
|
||||
crashed the release build; `copy_place_text` now sees through `Interp`
|
||||
exactly as `drop_fresh_text` does, pinned by
|
||||
`tests/corpus/run/interp-borrowed-field`.
|
||||
|
||||
1. Merge the `web-framework` branch to master (iterations 15–17 docs + code,
|
||||
local only). Why first: everything iteration 17 edits exists ONLY on that
|
||||
branch — the framework and web-app sources, the `[deps]` resolution code
|
||||
in `compiler/bin/main.ml` (17's `internal/` rule lands in that exact
|
||||
code), and the `just web-app` regression gate. Starting 17 unmerged means
|
||||
stacking a branch on an unreviewed branch; merged, 17 is a small clean
|
||||
diff off master.
|
||||
2. Spec + plan from the settled decisions (prose only, per convention).
|
||||
3. `kind = "library"` in `wo.toml` (default `"program"`): `woc <dir>` on a
|
||||
library runs the FULL pipeline as a check — parse, typecheck, borrow
|
||||
check, GC inference — with no entry required; an explicit build still
|
||||
works when a `main` exists (dual lib+bin).
|
||||
4. The `internal/` rule at the `[deps]` boundary: a consumer `use` of a dep
|
||||
path containing the segment `internal` is a new WO-E1xx at the `use`,
|
||||
naming the dependency; inside the dep it stays legal.
|
||||
5. Framework reorg: request-parser and serve-loop plumbing move under
|
||||
`internal/`; the public surface (`Handler`, `Middleware`, `App`,
|
||||
`Req`/`Resp`, builders) does not move.
|
||||
6. Gate: `just web-app` stays 14/0, plus new checks — library check without
|
||||
an entry succeeds, a consumer `internal` import is refused, and the
|
||||
iteration-16 `--emit` verification workaround is deleted.
|
||||
|
||||
The VM and GC are untouched by design — visibility is compile-time name
|
||||
resolution, libraries compile whole-program into the consumer's image, and
|
||||
GC inference stays whole-program (app usage may promote dep classes; that is
|
||||
intended).
|
||||
Next per the implementation order (17 parked): finish the half-done
|
||||
database branches — 9c (ipc-attach: manifest + binding) and 9d
|
||||
(keypair-auth: manifest) — both need their forks brainstormed before
|
||||
planning.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -167,8 +151,8 @@ that sequences its tasks. Read one, approve, then the next starts.
|
|||
| 13 | [Compile-time metaprogramming](stories/language-runtime-database/13-compile-time-metaprogramming.md) | ⬜ needs a spec first |
|
||||
| 14 | [skillhost host workload](stories/language-runtime-database/14-skillhost-host-workload.md) | ⬜ gaps recorded (branch query-grammar found skillhost needs no new query grammar); each gap a candidate iteration |
|
||||
| 15 | [deps: `wo.toml [deps]`](stories/language-runtime-database/15-deps-package-manager.md) | ✅ **landed 2026-08-18** (branch web-framework): [deps] inline tables, git-binary fetch, wo.lock pinning, offline-when-locked, --update-deps, WO-E106/E107; `just deps-accept` 8/0 |
|
||||
| 16 | [web framework](stories/language-runtime-database/16-web-framework.md) | ✅ **landed 2026-08-19** — writeonce-framework (HTTP/1.1 + router + Handler/Middleware) consumed by web-app through [deps]; `just web-app` 14/0; h2c parked (§C) behind 8/9f/11 |
|
||||
| 17 | [library projects + `internal/`](stories/language-runtime-database/17-library-projects-internal.md) | ⬜ **forks settled 2026-08-20, awaiting spec/plan**: kind = "library" manifest key; Go internal/ rule, dep-boundary-only; lib+bin dual; VM/GC untouched by design (impact analysis in the iteration) |
|
||||
| 16 | [web framework](stories/language-runtime-database/16-web-framework.md) | ✅ **landed 2026-08-19** — writeonce-framework (HTTP/1.1 + router + Handler/Middleware) consumed by web-app through [deps]; h2c parked (§C) behind 8/9f/11. **v1 polish landed 2026-08-20** (branch framework-v1): get/post/put/delete_ helpers, 405+Allow, HEAD, Logging middleware, set_header; `just web-app` 16/0; fixed the interp-borrowed-field emitter crash en route |
|
||||
| 17 | [library projects + `internal/`](stories/language-runtime-database/17-library-projects-internal.md) | ⏸ **PARKED 2026-08-20** (developer directive; framework v1 first) — forks settled, spec + plan approved and ready on branch `library-internal`: kind = "library" key; Go internal/ rule, dep-boundary-only; lib+bin dual; VM/GC untouched by design |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -176,10 +160,11 @@ that sequences its tasks. Read one, approve, then the next starts.
|
|||
|
||||
| Track | Item | Where |
|
||||
| -------- | --------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
||||
| Language | Iteration 17 — web framework as first-class library: spec/plan from the settled forks, then `kind = "library"` + `internal/` + framework reorg | [iteration 17](stories/language-runtime-database/17-library-projects-internal.md) |
|
||||
| Language | nothing active — the framework v1-polish slice landed 2026-08-20 (branch framework-v1, awaiting merge); next per the order: brainstorm 9c/9d's forks | [order](#implementation-order-re-sequenced-2026-08-20--framework-goal) |
|
||||
|
||||
Off-goal work is parked; the goal shifted 2026-08-20 from log-watcher (met)
|
||||
to the web framework as a first-class library.
|
||||
Off-goal work is parked; the goal (2026-08-20) is the web framework as a
|
||||
polished micro-framework v1 — iteration 17 (library kind + `internal/`) is
|
||||
parked with its spec + plan ready on branch `library-internal`.
|
||||
|
||||
### Landed 2026-08-14 — the compile-and-run milestone
|
||||
|
||||
|
|
@ -353,9 +338,10 @@ parked behind 8/9f/11; the post-12 parked list stays parked by the
|
|||
2026-08-08 scope directive. (Iteration 7 dropped from this list — landed
|
||||
2026-08-15.)
|
||||
|
||||
1. **17** — THE goal slice: `kind = "library"` + `internal/` + framework
|
||||
reorg; forks settled, compiler-driver-only, no runtime deps. Merge the
|
||||
`web-framework` branch first.
|
||||
1. ~~**17**~~ — **PARKED 2026-08-20** (developer directive: framework v1
|
||||
first); spec + plan approved, ready on branch `library-internal` for
|
||||
whenever it unparks. The framework v1-polish slice took its place and
|
||||
landed the same day (branch framework-v1, `just web-app` 16/0).
|
||||
2. **9c then 9d** — finish the half-done branches (ipc-attach: manifest +
|
||||
binding; keypair-auth: manifest) before they rot; 9d folds into 9c's
|
||||
plan; both must precede 10.
|
||||
|
|
|
|||
|
|
@ -108,10 +108,10 @@ fn main(args: multi Text) -> Int {
|
|||
|
||||
let app = App { middleware: [], routes: [] };
|
||||
app.use_mw(Mw { m: Auth { token: token } });
|
||||
app.add(Route { method: "GET", pattern: "/products", h: ListProducts { pad: 0 } });
|
||||
app.add(Route { method: "GET", pattern: "/products/:name", h: ShowProduct { pad: 0 } });
|
||||
app.add(Route { method: "POST", pattern: "/products", h: CreateProduct { pad: 0 } });
|
||||
app.add(Route { method: "POST", pattern: "/orders", h: CreateOrder { pad: 0 } });
|
||||
app.add(Route { method: "DELETE", pattern: "/products/:name", h: DeleteProduct { pad: 0 } });
|
||||
app.get("/products", ListProducts { pad: 0 });
|
||||
app.get("/products/:name", ShowProduct { pad: 0 });
|
||||
app.post("/products", CreateProduct { pad: 0 });
|
||||
app.post("/orders", CreateOrder { pad: 0 });
|
||||
app.delete_("/products/:name", DeleteProduct { pad: 0 });
|
||||
return app.serve("127.0.0.1", port);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,16 @@ writeonce-framework = { git = "https://github.com/shoneyj/writeonce-framework",
|
|||
other client, so closing is the correct shape until shards/fibers (8/11).
|
||||
A proxy in front simply reconnects.
|
||||
- **Router** (`router/`): method + path table with `:param` captures into
|
||||
`req.params`; first match wins; no match is the framework's 404.
|
||||
`req.params`; first match wins; a known path with the wrong method is
|
||||
**405 with the `Allow` header** (registration order); no matching path is
|
||||
the framework's 404. **HEAD is served free**: routed as GET, body
|
||||
suppressed, `Content-Length` still names the body a GET would carry.
|
||||
- **Registration helpers**: `app.get/post/put/delete_(pattern, handler)`
|
||||
push the route for you (`delete_` because `delete` is the query
|
||||
keyword); `app.add(Route { ... })` stays for anything else. A
|
||||
request-line `Logging` middleware ships in `router/`, and
|
||||
`set_header(resp, name, value)` is the escape hatch for headers the
|
||||
builders don't set.
|
||||
- **Handlers without closures**: the language has no function values by
|
||||
doctrine, so a route handler is a class satisfying the `Handler` interface
|
||||
(`fn handle(req: Req) -> Resp`), dispatched structurally — a
|
||||
|
|
|
|||
|
|
@ -25,22 +25,44 @@ pub class App {
|
|||
push(self.routes, r);
|
||||
}
|
||||
|
||||
-- Registration helpers — the ctor-literal-into-take shape, per method.
|
||||
fn get(pattern: Text, take h: Handler) {
|
||||
push(self.routes, Route { method: "GET", pattern: pattern, h: h });
|
||||
}
|
||||
|
||||
fn post(pattern: Text, take h: Handler) {
|
||||
push(self.routes, Route { method: "POST", pattern: pattern, h: h });
|
||||
}
|
||||
|
||||
fn put(pattern: Text, take h: Handler) {
|
||||
push(self.routes, Route { method: "PUT", pattern: pattern, h: h });
|
||||
}
|
||||
|
||||
fn delete_(pattern: Text, take h: Handler) {
|
||||
push(self.routes, Route { method: "DELETE", pattern: pattern, h: h });
|
||||
}
|
||||
|
||||
fn dispatch(mut req: Req) -> Resp {
|
||||
for mw in self.middleware {
|
||||
let short = mw.m.before(req);
|
||||
if short != nil { return short; }
|
||||
}
|
||||
-- Path-first matching so a wrong-method hit on a known path answers
|
||||
-- 405 with the Allow header (registration order) instead of a 404.
|
||||
let allow = "";
|
||||
for r in self.routes {
|
||||
if r.method == req.method {
|
||||
let params: map<Text, Text> = {};
|
||||
if route_match(r.pattern, req.path, params) {
|
||||
let params: map<Text, Text> = {};
|
||||
if route_match(r.pattern, req.path, params) {
|
||||
if r.method == req.method {
|
||||
-- captures land on the borrowed request itself (mut) — a failed
|
||||
-- match above never touched it, since captures collect locally
|
||||
for k, v in params { req.params[k] = v; }
|
||||
return r.h.handle(req);
|
||||
}
|
||||
if allow == "" { allow = "${r.method}"; } else { allow = "${allow}, ${r.method}"; }
|
||||
}
|
||||
}
|
||||
if allow != "" { return method_not_allowed(allow); }
|
||||
return not_found();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,13 +20,16 @@ fn status_text(code: Int) -> Text {
|
|||
case 400: return "Bad Request";
|
||||
case 401: return "Unauthorized";
|
||||
case 404: return "Not Found";
|
||||
case 405: return "Method Not Allowed";
|
||||
case 409: return "Conflict";
|
||||
case 500: return "Internal Server Error";
|
||||
default: return "Status";
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize(resp: Resp, keep: Bool) -> Text {
|
||||
-- head_only: HEAD answers — full status line + headers (Content-Length of
|
||||
-- the body a GET would have sent) with the body itself suppressed.
|
||||
pub fn serialize(resp: Resp, keep: Bool, head_only: Bool) -> Text {
|
||||
let head = "HTTP/1.1 ${resp.status} ${status_text(resp.status)}\r\n";
|
||||
for k, v in resp.headers {
|
||||
head = head .. "${k}: ${v}\r\n";
|
||||
|
|
@ -37,6 +40,7 @@ pub fn serialize(resp: Resp, keep: Bool) -> Text {
|
|||
head = head .. "Connection: close\r\n";
|
||||
}
|
||||
head = head .. "Content-Length: ${len(resp.body)}\r\n\r\n";
|
||||
if head_only { return head; }
|
||||
return head .. resp.body;
|
||||
}
|
||||
|
||||
|
|
@ -54,13 +58,18 @@ pub fn serve(host: Text, port: Int, d: Dispatcher) -> Int {
|
|||
if p == nil { alive = false; continue; }
|
||||
if p.closed { alive = false; continue; }
|
||||
if p.ok == false {
|
||||
try net.write(c, serialize(bad_request("malformed request"), false)) catch (e) {}
|
||||
try net.write(c, serialize(bad_request("malformed request"), false, false)) catch (e) {}
|
||||
alive = false;
|
||||
continue;
|
||||
}
|
||||
let r = p.req;
|
||||
if r == nil { alive = false; continue; }
|
||||
carry = p.rest;
|
||||
-- HEAD is GET with the body suppressed: route and dispatch as GET,
|
||||
-- serialize with head_only so Content-Length still names the body a
|
||||
-- GET would have carried (RFC 9110 §9.3.2).
|
||||
let is_head = r.method == "HEAD";
|
||||
if is_head { r.method = "GET"; }
|
||||
-- Connection policy for a single-threaded server: serve PIPELINED
|
||||
-- requests on one connection (bytes already buffered), but close when
|
||||
-- the client would idle — a parked keep-alive connection would block
|
||||
|
|
@ -72,7 +81,7 @@ pub fn serve(host: Text, port: Int, d: Dispatcher) -> Int {
|
|||
if to_lower(conn) == "close" { keep = false; }
|
||||
}
|
||||
let resp = try d.dispatch(r) catch (e) server_error();
|
||||
try net.write(c, serialize(resp, keep)) catch (e) { alive = false; }
|
||||
try net.write(c, serialize(resp, keep, is_head)) catch (e) { alive = false; }
|
||||
if keep == false { alive = false; }
|
||||
}
|
||||
net.close(c);
|
||||
|
|
|
|||
|
|
@ -61,6 +61,13 @@ pub fn conflict(msg: Text) -> Resp {
|
|||
return Resp { status: 409, headers: h, body: "{\"error\":\"${msg}\"}" };
|
||||
}
|
||||
|
||||
pub fn method_not_allowed(allow: Text) -> Resp {
|
||||
let h: map<Text, Text> = {};
|
||||
h["content-type"] = "application/json";
|
||||
h["allow"] = allow;
|
||||
return Resp { status: 405, headers: h, body: "{\"error\":\"method not allowed\"}" };
|
||||
}
|
||||
|
||||
pub fn server_error() -> Resp {
|
||||
let h: map<Text, Text> = {};
|
||||
h["content-type"] = "application/json";
|
||||
|
|
@ -72,3 +79,9 @@ pub fn redirect(location: Text) -> Resp {
|
|||
h["location"] = location;
|
||||
return Resp { status: 302, headers: h, body: "" };
|
||||
}
|
||||
|
||||
-- Set (or overwrite) one header on a built response — the escape hatch for
|
||||
-- anything the builders don't cover: cache-control, etag, custom headers.
|
||||
pub fn set_header(mut r: Resp, name: Text, value: Text) {
|
||||
r.headers[name] = value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,17 @@ pub class Mw {
|
|||
m: Middleware
|
||||
}
|
||||
|
||||
-- Request-line logging, the one middleware every framework ships: method +
|
||||
-- path to stderr, never short-circuits. `pad` is the record-class ctor
|
||||
-- convention (every stateless handler carries one Int field).
|
||||
pub class Logging {
|
||||
pad: Int
|
||||
fn before(req: Req) -> ?Resp {
|
||||
print_err("${req.method} ${req.path}");
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
-- Does `pattern` match `path`? Fills `params` with :name captures.
|
||||
-- Segments split on '/', empties dropped (so "/a//b" == "/a/b" and the
|
||||
-- root "/" is the empty segment list). First mismatch wins; a :segment
|
||||
|
|
|
|||
|
|
@ -90,11 +90,15 @@ list, and a pointer to the plan document that already sequences its tasks.
|
|||
Iterations 3 → 4 → 5 → 6 → 7 are the committed line; nothing off that
|
||||
line lands before iteration 7's acceptance. Then 7b, then 8–12 (with 9b after the database engine).
|
||||
- **Goal shifted 2026-08-20: log-watcher (iteration 7, landed 2026-08-15)
|
||||
to the web framework as a first-class library.** Implementation order for
|
||||
everything still pending: 17 → 9c/9d → 9e → 8 → 9f → 11 (+ h2c unparks) →
|
||||
10 → 12 → 14/9g → 13 + parked drain. Rationale and forcing dependencies
|
||||
live in [`docs/00-status.md`](../../00-status.md) under "Implementation
|
||||
order".
|
||||
to the web framework.** Same day, refined by directive: the framework
|
||||
stays a polished MICRO-framework (routing, middleware, `Req`/`Resp`) —
|
||||
nothing MVC-scale — and iteration 17 (library kind + `internal/`) is
|
||||
**parked** with spec + plan ready on branch `library-internal`. The
|
||||
v1-polish slice landed 2026-08-20 (`just web-app` 16/0). Implementation
|
||||
order for everything still pending: 9c/9d → 9e → 8 → 9f → 11 (+ h2c
|
||||
unparks) → 10 → 12 → 14/9g → 13 + parked drain. Rationale and forcing
|
||||
dependencies live in [`docs/00-status.md`](../../00-status.md) under
|
||||
"Implementation order".
|
||||
- **Iteration 7b (inserted 2026-08-11)** sits after the critical path
|
||||
deliberately: it delays nothing on the log-watcher line, and it must precede
|
||||
iteration 8 because the collector should be settled before shards multiply.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,17 @@
|
|||
> stored in containers now MOVE (`run/container-owned-move`), and a dep's
|
||||
> internal `use` paths resolve dep-relatively.
|
||||
>
|
||||
> **v1 polish LANDED 2026-08-20** (branch `framework-v1`, developer
|
||||
> directive: ship routing/middleware/req-resp as a polished micro-framework,
|
||||
> nothing MVC-scale): registration helpers `get/post/put/delete_` (the
|
||||
> take-Handler shape, probe-proven — deviation 1 of the 16 plan retired),
|
||||
> 405 + `Allow` on wrong-method path hits, HEAD served as GET with the body
|
||||
> suppressed (RFC 9110 §9.3.2), a request-line `Logging` middleware,
|
||||
> `set_header`; `just web-app` 16/0. It exposed and fixed a third compiler
|
||||
> gap: a Text single-segment interpolation of a place crossed
|
||||
> `let`/assignment boundaries uncopied — `copy_place_text` now sees through
|
||||
> `Interp` (`run/interp-borrowed-field`).
|
||||
>
|
||||
> The framework is written IN writeonce and imported
|
||||
> like any dependency (iteration 15 is the prerequisite). TLS terminates at a
|
||||
> reverse proxy — browsers get TLS+ALPN+h2 from nginx/caddy while the
|
||||
|
|
|
|||
|
|
@ -7,8 +7,13 @@
|
|||
> as an iteration, do not implement yet).
|
||||
>
|
||||
> **Refined 2026-08-20: all four forks SETTLED** (developer decisions, no code
|
||||
> changed). See "Settled decisions" and "Impact analysis" below. Next step is
|
||||
> the spec/plan; implementation stays parked until asked.
|
||||
> changed). See "Settled decisions" and "Impact analysis" below.
|
||||
>
|
||||
> **⏸ PARKED 2026-08-20** (developer directive: framework v1 work first).
|
||||
> The spec and plan were written and approved before parking; both sit ready
|
||||
> on branch `library-internal`
|
||||
> (`docs/superpowers/specs/2026-08-20-library-kind-internal-design.md`,
|
||||
> `docs/superpowers/plans/2026-08-20-library-kind-internal.md`).
|
||||
|
||||
## Why this iteration exists
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,57 @@ expect "unknown product is 404" "$(hit GET /products/none)" 404
|
|||
expect "create order (FK)" "$(hit POST /orders '{"product":"mug","qty":2}')" 201
|
||||
expect "delete restricted by FK (409), server survives" "$(hit DELETE /products/mug)" 409 "reference"
|
||||
|
||||
# ---- 11. pipelined keep-alive: two requests, one connection ----
|
||||
# ---- 11. 405 on a known path with the wrong method (Allow header) ----
|
||||
ma="$(timeout 5 python3 - "$PORT" <<'PYEOF'
|
||||
import socket, sys
|
||||
port = int(sys.argv[1])
|
||||
s = socket.create_connection(("127.0.0.1", port), timeout=3)
|
||||
s.sendall(b"PUT /products HTTP/1.1\r\nhost: a\r\nauthorization: Bearer s3cr3t\r\nconnection: close\r\ncontent-length: 0\r\n\r\n")
|
||||
d = b""
|
||||
while True:
|
||||
got = s.recv(4000)
|
||||
if not got: break
|
||||
d += got
|
||||
head = d.split(b"\r\n\r\n")[0].decode()
|
||||
status = head.splitlines()[0].split(" ")[1]
|
||||
allow = [l.split(":", 1)[1].strip() for l in head.splitlines() if l.lower().startswith("allow")]
|
||||
print(status + "|" + (allow[0] if allow else ""))
|
||||
PYEOF
|
||||
)"
|
||||
[[ "$ma" == "405|GET, POST" ]] && ok "405 + Allow on wrong method" \
|
||||
|| bad "405" "got $ma (want 405|GET, POST)"
|
||||
|
||||
# ---- 12. HEAD: GET's headers, no body ----
|
||||
hd="$(timeout 5 python3 - "$PORT" <<'PYEOF'
|
||||
import socket, sys
|
||||
port = int(sys.argv[1])
|
||||
def raw(req):
|
||||
s = socket.create_connection(("127.0.0.1", port), timeout=3)
|
||||
s.sendall(req)
|
||||
d = b""
|
||||
while True:
|
||||
got = s.recv(4000)
|
||||
if not got: break
|
||||
d += got
|
||||
s.close()
|
||||
return d
|
||||
tail = b" /products HTTP/1.1\r\nhost: a\r\nauthorization: Bearer s3cr3t\r\nconnection: close\r\ncontent-length: 0\r\n\r\n"
|
||||
g = raw(b"GET" + tail)
|
||||
h = raw(b"HEAD" + tail)
|
||||
ghead, _, gbody = g.partition(b"\r\n\r\n")
|
||||
hhead, _, hbody = h.partition(b"\r\n\r\n")
|
||||
def cl(head):
|
||||
return [l.split(b":")[1].strip() for l in head.splitlines() if l.lower().startswith(b"content-length")][0]
|
||||
status = hhead.decode().splitlines()[0].split(" ")[1]
|
||||
print(f"{status}|{(cl(h[:len(hhead)]) == cl(g[:len(ghead)]))}|{len(hbody)}|{len(gbody)}")
|
||||
PYEOF
|
||||
)"
|
||||
IFS='|' read -r hs same hb gb <<<"$hd"
|
||||
[[ "$hs" == "200" && "$same" == "True" && "$hb" == "0" && "$gb" != "0" ]] \
|
||||
&& ok "HEAD answers GET's Content-Length with no body" \
|
||||
|| bad "HEAD" "status=$hs same-length=$same head-body=$hb get-body=$gb"
|
||||
|
||||
# ---- 13. pipelined keep-alive: two requests, one connection ----
|
||||
n="$(timeout 5 python3 - "$PORT" <<'PYEOF'
|
||||
import socket, sys
|
||||
port = int(sys.argv[1])
|
||||
|
|
@ -117,14 +167,14 @@ PYEOF
|
|||
[[ "$n" == "2" ]] && ok "pipelined keep-alive (2 responses, 1 connection)" \
|
||||
|| bad "pipelining" "expected 2 responses, got $n"
|
||||
|
||||
# ---- 12. SIGTERM stops it ----
|
||||
# ---- 14. SIGTERM stops it ----
|
||||
kill -TERM "$SRV"
|
||||
stopped=1
|
||||
for _ in $(seq 1 30); do kill -0 "$SRV" 2>/dev/null || { stopped=0; break; }; sleep 0.1; done
|
||||
[[ $stopped -eq 0 ]] && ok "SIGTERM stops the server" || bad "stop" "still running"
|
||||
SRV=""
|
||||
|
||||
# ---- 13. restart persistence (WAL replay) ----
|
||||
# ---- 15. restart persistence (WAL replay) ----
|
||||
WA_TOKEN=s3cr3t WO_DATA="$DATA" "$W/app/target/web-app" "$PORT" >>"$W/srv.out" 2>&1 &
|
||||
SRV=$!
|
||||
sleep 0.5
|
||||
|
|
|
|||
Loading…
Reference in a new issue