duckbrain-memories-post-urllib-request-missing-data-body-400-validation-error
Done. Solution, evidence, and reproduction artifacts are in place.
Root cause (client-side, not the server): the tick's Python writer built body = json.dumps(payload).encode() and set Content-Type: application/json, but never passed data=body to urllib.request.Request(url, method="POST"). A data-less urllib POST transmits a zero-length body — verified on the wire:
POST /api/memories HTTP/1.1
Content-Length: 0 ← buggy: payload never left the process
Content-Type: application/json ← header present (rules out the t317 missing-content-type class)
On the server, express.json() silently parses the empty body to {} (no parse error), so the handler's own validation at memories.ts:383 raises the misleading 400 VALIDATION_ERROR — the fields existed client-side; they just never hit the wire. It survived tick-to-tick because writers are cloned from prior ticks where the same latent hole lived in the POST line.
Fix (one line):
req = urllib.request.Request(BASE + "/api/memories",
data=json.dumps(payload).encode(), # THE FIX
method="POST")
req.add_header("Content-Type", "application/json")
Verification (all executed):
1. Raw-socket capture server: buggy client = 183 bytes, Content-Length: 0, no body; fixed client = 281 bytes, Content-Length: 97, full JSON body.
2. Server simulator mirroring express.json() + memories.ts:383: buggy → byte-for-byte 400 {"error":"Missing required fields: key, domain, content","code":"VALIDATION_ERROR"}; fixed → 200.
3. Production green (tick 318, per problem context): WRITE OK id 251d6e28, canary t318-f5bb1fe0 present.
Deliverables:
- /workspace/solution.md — self-contained writeup (title, root-cause analysis, exact fix, verification, prevention notes)
- /workspace/evidence.md — empirical transcripts
- /workspace/repro/ — reproduction harness (capture_server.py, writer_buggy.py, writer_fixed.py, sim_server.py)
# Evidence - Problem class: duckbrain-memories-post-urllib-request-missing-data-body-400-validation-error - Model: openrouter/deepseek/deepseek-v4-flash-0731 - Solved: 2026-09-05T00:36:45.540Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "SYMPTOM: python urllib POST to DuckBrain /api/memories returned 400 {\"error\":\"Missing required fields: key, domain, content\",\"code\":\"VALIDATION_ERROR\"} even though the client payload HAD all three fields. GET endpoints worked fine. Content-Type: application/json WAS set (rules out the t317 class: missing-content-type -> 500). TELL: journalctl --user -u duckbrain-local.service showed ApiError at src/http/routes/memories.ts:383 \u2014 server-side validation of req.body saw an object WITHOUT the fields. ROOT CAUSE (client-side): urllib.request.Request(url, method='POST') with NO data= argument transmits NO request body at all. The script built `body = json.dumps(...).encode()` but never attached it. express.json() parses an empty body to {} (no parse error, no 400 from the parser), so the handler's own validation reports 'missing required fields' \u2014 misleading, because the client HAD the fields; they never hit the wire. FIX: pass the body to the Request constructor: urllib.request.Request(url, data=json.dumps(payload).encode(), method='POST') + req.add_header('Content-Type','application/json'). WHY IT SURFACED NOW: the tick's writer was cloned from a prior tick's script whose POST line read Request(BASE + '/api/memories', method='POST') \u2014 a latent hole surviving clone-chains across ticks. RULE: a urllib POST that 400s with 'missing fields' means EMPTY BODY \u2014 check data= before suspecting the payload. Verified green after fix: WRITE OK id 251d6e28, readback canary t318-f5bb1fe0 present.", "environment": "DuckBrain local systemd daemon duckbrain-local.service (node bin/duckbrain.js http --port=3000) on bunker-eduos-agent; Express 5 + express.json() body parser; client python3 stdlib urllib.request", "language": "typescript", "model": "openrouter/deepseek/deepseek-v4-flash-0731", "problem_class": "duckbrain-memories-post-urllib-request-missing-data-body-400-validation-error", "provider": "openrouter", "solved_at": "2026-09-05T00:36:45.540Z", "version": "duckbrain Beta @ 2026-09-04, eduos foreman tick 318"}Done. Solution, evidence, and reproduction artifacts are in place.
Root cause (client-side, not the server): the tick's Python writer built body = json.dumps(payload).encode() and set Content-Type: application/json, but never passed data=body to urllib.request.Request(url, method="POST"). A data-less urllib POST transmits a zero-length body — verified on the wire:
POST /api/memories HTTP/1.1
Content-Length: 0 ← buggy: payload never left the process
Content-Type: application/json ← header present (rules out the t317 missing-content-type class)
On the server, express.json() silently parses the empty body to {} (no parse error), so the handler's own validation at memories.ts:383 raises the misleading 400 VALIDATION_ERROR — the fields existed client-side; they just never hit the wire. It survived tick-to-tick because writers are cloned from prior ticks where the same latent hole lived in the POST line.
Fix (one line):
req = urllib.request.Request(BASE + "/api/memories",
data=json.dumps(payload).encode(), # THE FIX
method="POST")
req.add_header("Content-Type", "application/json")
Verification (all executed):
1. Raw-socket capture server: buggy client = 183 bytes, Content-Length: 0, no body; fixed client = 281 bytes, Content-Length: 97, full JSON body.
2. Server simulator mirroring express.json() + memories.ts:383: buggy → byte-for-byte 400 {"error":"Missing required fields: key, domain, content","code":"VALIDATION_ERROR"}; fixed → 200.
3. Production green (tick 318, per problem context): WRITE OK id 251d6e28, canary t318-f5bb1fe0 present.
Deliverables:
- /workspace/solution.md — self-contained writeup (title, root-cause analysis, exact fix, verification, prevention notes)
- /workspace/evidence.md — empirical transcripts
- /workspace/repro/ — reproduction harness (capture_server.py, writer_buggy.py, writer_fixed.py, sim_server.py)
# Evidence - Problem class: duckbrain-memories-post-urllib-request-missing-data-body-400-validation-error - Model: openrouter/deepseek/deepseek-v4-flash-0731 - Solved: 2026-09-05T00:36:45.540Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "SYMPTOM: python urllib POST to DuckBrain /api/memories returned 400 {\"error\":\"Missing required fields: key, domain, content\",\"code\":\"VALIDATION_ERROR\"} even though the client payload HAD all three fields. GET endpoints worked fine. Content-Type: application/json WAS set (rules out the t317 class: missing-content-type -> 500). TELL: journalctl --user -u duckbrain-local.service showed ApiError at src/http/routes/memories.ts:383 \u2014 server-side validation of req.body saw an object WITHOUT the fields. ROOT CAUSE (client-side): urllib.request.Request(url, method='POST') with NO data= argument transmits NO request body at all. The script built `body = json.dumps(...).encode()` but never attached it. express.json() parses an empty body to {} (no parse error, no 400 from the parser), so the handler's own validation reports 'missing required fields' \u2014 misleading, because the client HAD the fields; they never hit the wire. FIX: pass the body to the Request constructor: urllib.request.Request(url, data=json.dumps(payload).encode(), method='POST') + req.add_header('Content-Type','application/json'). WHY IT SURFACED NOW: the tick's writer was cloned from a prior tick's script whose POST line read Request(BASE + '/api/memories', method='POST') \u2014 a latent hole surviving clone-chains across ticks. RULE: a urllib POST that 400s with 'missing fields' means EMPTY BODY \u2014 check data= before suspecting the payload. Verified green after fix: WRITE OK id 251d6e28, readback canary t318-f5bb1fe0 present.", "environment": "DuckBrain local systemd daemon duckbrain-local.service (node bin/duckbrain.js http --port=3000) on bunker-eduos-agent; Express 5 + express.json() body parser; client python3 stdlib urllib.request", "language": "typescript", "model": "openrouter/deepseek/deepseek-v4-flash-0731", "problem_class": "duckbrain-memories-post-urllib-request-missing-data-body-400-validation-error", "provider": "openrouter", "solved_at": "2026-09-05T00:36:45.540Z", "version": "duckbrain Beta @ 2026-09-04, eduos foreman tick 318"}