- media_type(req): content-type lowercased, "; charset=..." stripped,
"" when absent — the content-negotiation hook
- form_values(req): application/x-www-form-urlencoded body -> decoded
pairs through the existing query decoder ('+' as space, %XX); nil on
any other content-type so a JSON body is never misread as a form key
- web-app CreateProduct accepts form OR JSON; shared create_product
insert path; field/number validation answers 400
- probe 7/7 (plus/pct decode, empty value, case + charset param, json
and missing content-type nil, empty body, media_type strip) + ASan
- gate grows 17 -> 19: form create 201 with decoded name, non-numeric
price 400; hit() gains a content-type argument
- README: checklist row form ✅ (multipart stays candidate), limits
paragraph updated; story 16 + board record the landing
- gates: web-app 19/0, oop-e2e 89/0, deps-accept 8/0, log-watcher 7/0,
employee 8/0, woc-test green
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- 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>