Closed bug report

web-control /command 413s any staged file over ~2 MB: real .step 3D models cannot be attached (write_file base64 body exceeds the cap)

Colby Knox · 2mo ago ·closed by John Lauer

web-control /command 413s any staged file over ~2 MB, so no real .step 3D model can be attached

ab 2.0.65, web-control on 127.0.0.1:24680. Distinct from #34 (footprint-open plugin, fixed) and #89 (/status LNA, resolved). The KiCad "Open in Footprint Editor" Quick Action now reaches the bridge and installs the footprint, but attaching the 3D body fails: staging a multi-MB .step through write_file exceeds the /command request-body cap and comes back 413 Payload Too Large.

Repro (a real user, machine k-zephyrus, real Firefox)

Part: adom/ads1220irvar. The footprint action stages two files onto the PC via write_file, then calls kicad_install_footprint:

staged file raw bytes base64 body (~+33%) result
ads1220irvar.kicad_mod 8,162 ~11 KB OK
ads1220irvar.step 1,930,666 ~2.57 MB 413 Payload Too Large

Browser console on the failing click:

XHR POST http://127.0.0.1:24680/command  [HTTP/1.1 413 Payload Too Large 7ms]

The symbol button on the same part works because a .kicad_sym is a few KB. That size split (KB symbol succeeds, ~2.5 MB model 413s) is the whole "symbol works, footprint fails" asymmetry we chased. It is not version, LNA, or CORS related; the bridge is 2.0.65 and reachable.

Root cause

The /command HTTP body limit is under ~2.5 MB, and write_file carries file bytes as base64 inside the JSON command body. A base64-in-JSON body is a poor transport for MB-scale binaries, and 3D models routinely run 1-10 MB, so effectively no genuine .step can be staged this way.

Two asks (either unblocks the feature; the second is the durable one)

  1. Raise the /command body cap to comfortably hold a real model (a base64 .step up to, say, 24-32 MB). Quick, but it still holds the whole body in memory.
  2. Add a chunked or streamed large-file transfer verb (offset-appended writes, or a dedicated upload path) so write_file of a big binary does not depend on a single oversized JSON body. This is the transport the feature wants.

Secondary: the 413 body is not JSON

ab's 413 (and other error pages) returns a plain-text body. A cross-origin client that does response.json() gets a parse error, so a clean 413 surfaced to the user as SyntaxError: JSON.parse ... / a "NetworkError", which is what made this look like a mystery instead of "file too large". A JSON error body would let clients react precisely, for example:

{ "success": false, "errorCode": "payload_too_large", "limit": 2097152 }

What is already handled on the wiki side (so this is contained, not urgent-broken)

The wiki client now degrades gracefully: if staging the model fails for any reason it drops the model and opens the Footprint Editor anyway, reporting "3D model skipped (file too large for Adom Bridge); opening the footprint without it". It also tolerates a non-JSON error body so the status reads as 413 rather than a JSON.parse error. So users get a working footprint today; the missing piece is attaching the 3D body, which is blocked here on the /command cap.

Happy to test any build live against adom/ads1220irvar (the 1.93 MB .step is a good fixture) once a raised cap or a chunked verb lands. If you tell me the exact current cap I will also tune the client's pre-flight so it never sends a body you will reject.

6 Replies

Colby Knox · 2mo ago

John confirms the shape: there is no web-control verb for large file downloads today, but ab already has large-file transfer machinery for containers and the desktop. So this is ask #2 above: expose that existing transfer path through web control (and let ab own the 413), rather than growing the base64-in-JSON /command body.

The wiki client degrades gracefully in the meantime: if the model does not stage, it drops it and opens the Footprint Editor without the 3D body, and now reads a 413 as "file too large" instead of a JSON.parse/NetworkError. So users are unblocked today; the 3D body is what is waiting on this.

Whenever you expose a transfer verb, tell me its name and argument shape and I will wire the client to it. adom/ads1220irvar (1.93 MB .step) is a ready fixture.

John Lauer · 2mo ago

Shipped in ab 2.0.72: write_file_chunk

Your ask #2, not #1 - the body cap is unchanged. Here is the name and argument shape to wire the client to.

write_file_chunk {
  path:       string    same expansion as write_file (~, %VARS%)
  dataB64:    string    THIS chunk's bytes, base64          (alias: content)
  offset:     number    byte position of this chunk         (REQUIRED)
  final:      bool?     true on the last chunk
  sha256:     string?   the SOURCE file's sha256, verified when final
  createDirs: bool?     default true
}
->
{
  path, offset, bytesWritten, size,
  nextOffset,          // where the next chunk goes - just follow this
  final,
  sha256?, verified?   // present on the final chunk
}

The loop is: send ~1 MB of raw bytes, set offset to the nextOffset you got back, repeat, and put final:true on the last one. Pass the source sha256 there and ab hashes the reassembly and verifies it for you, so a bad transfer is caught at the seam rather than inside KiCad.

Reachable from web control exactly like write_file - same /command endpoint, same consent gate, no new permission surface. It is not a web-only verb: it is on the one shared surface, so the CLI, Hydrogen and bridges can all call it. The ~2.5 MB cap is a property of HTTP, not of web control.

Three properties worth relying on

  • Stateless. No handle table, no session id, nothing to leak if your page navigates away mid-transfer. Each call opens, seeks, writes, closes. That also makes a chunk safely retryable: if you never saw the response, send the same offset again.
  • A gap is refused, never zero-filled. offset > size returns errorCode:"chunk_gap" with an expectedOffset to resume from. Zero-filling a hole would produce a file of exactly the right length that is silently corrupt, which for a 3D model means the failure surfaces days later in front of a user.
  • offset: 0 truncates, so a restarted transfer never appends onto the remains of a failed one.

Measured on your fixture class

Against the largest STEP on the wiki, adom/s10b-ph-sm4-tb (3,921,735 bytes - about double your ads1220irvar):

single-shot write_file  ->  HTTP 413 "length limit exceeded"
write_file_chunk        ->  4 chunks, 0.9s, 3,921,735 bytes, sha256 verified

The built-in web-control demo already uses it: anything over 1 MB goes chunked automatically, and its part picker now offers ten real wiki parts from a 40 KB resistor up to that 3.7 MB header, so the >2.5 MB path is exercised from a page rather than only from a report. ads1220irvar is one of the ten.

On your secondary point: the 413 body is not JSON

Still true, and still worth fixing on our side - that 413 comes from the HTTP body-limit layer before any ab handler runs, so it is plain text. You should not need it now that a large file never reaches the cap, but if you would like ab to return a structured JSON error there anyway (so a client can branch on errorCode instead of sniffing a string), say so and I will file it separately rather than bundle it here.

One thing I would like back

If you wire this and a transfer misbehaves, the most useful thing you can send is the offset and expectedOffset from the failing response - those two numbers localize it immediately. And if 1 MB chunks turn out to be the wrong size for the wiki client, tell me: the chunk size is entirely the caller's choice, ab imposes nothing beyond the body cap.

John Lauer · 2mo ago

Corroborating data point from the fusion bridge, since it may sharpen where the cap really is.

We hit the same wall from the container side and moved off the base64-in-JSON path entirely: send_files is what we stage .lbr and .step with now. The trigger was smaller than yours by an order of magnitude - a 245 KB STEP failed with Errno 7 when passed as base64 in argv (2026-08-14, it is a comment in our verb-test suite so nobody re-learns it). So the practical ceiling on that path can be far below the documented 2 MB body cap depending on the transport in between, which argues for exactly the direction you took: a transfer verb that owns chunking, rather than a bigger body limit.

write_file_chunk looks right to me, and the nextOffset-to-follow plus source-sha256-verified-on-final is the part I would keep loudest in the docs: it makes a bad transfer fail at the seam with a name, instead of producing a plausible file that breaks later inside the CAD app. A truncated .step does not error - it imports as a body with missing geometry, which is a much worse bug to chase.

No action needed from us: the fusion bridge stages via send_files and is unaffected. Flagging only in case the 245 KB figure is useful when you document the limits.

John Lauer · 2mo ago

That 245 KB figure was worth more than a footnote - it found a real hole in the verb I had just shipped. Thank you.

Your errno 7 is E2BIG, and it is a different ceiling from the body cap

errno 7 is E2BIG, "argument list too long" - so that failure was the argv limit, not an HTTP body limit. Two different walls, which is exactly why 245 KB could fail while the documented cap was 2 MB. Worth separating in anyone's notes:

  • body cap (~2.5 MB): applies to POST /command over HTTP.
  • ARG_MAX / command-line cap: applies when a payload rides in argv, and on Windows it is far smaller.

Which means write_file_chunk had a wrong hint, and I only found out because of your comment

I tested the verb over HTTP and it was fine - 3.9 MB in 4 chunks. So its hint told callers to use ~1 MB chunks. But the adom-bridge CLI passes verb args in argv, so a 1 MB chunk from the CLI never reaches ab at all:

1024 KB raw -> 1,398,325 argv chars : SPAWN FAILED (WinError 206)
  32 KB raw ->    43,911 argv chars : SPAWN FAILED
  24 KB raw ->    32,987 argv chars : SPAWN FAILED     <- Windows caps a command line at 32,767
  16 KB raw ->    22,067 argv chars : success=true, verified=true
   8 KB raw ->    11,142 argv chars : success=true, verified=true

The ceiling is sharp, and it fails before ab is reached, so ab cannot even return a useful error - the caller gets WinError 206 from the OS. A verb whose own guidance is impossible to follow on one of its two transports is worse than one with no guidance, so the hint now states both, measured:

  • HTTP (a web page, or the direct API): ~1 MB raw per chunk
  • the CLI on Windows: 16 KB or less raw per chunk
  • shipping a big file from a container: send_files, which is what you already moved to and remains the right answer there

Same correction is now in the web-control doc on the ab page, with your 245 KB datapoint credited as what prompted measuring it.

On the part you said to keep loudest

Agreed, and your reason is the better one - I will use it. "A truncated .step does not error, it imports as a body with missing geometry" is exactly why the gap check refuses rather than zero-fills: a zero-filled hole produces a file of precisely the right length that is silently wrong, and the failure then surfaces days later inside the CAD app with nothing pointing back at the transfer. sha256-verified-on-final exists to make that impossible to reach.

No action needed from you - flagging only because your note turned a shipped-and-tested verb into a shipped-and-tested-on-one-transport verb, which is a distinction I would rather have learned from you than from a user.

Colby Knox · 2mo ago

Wired and shipped to prod. The footprint recipe now stages the .step through write_file_chunk at ~1 MB raw chunks (HTTP transport), follows nextOffset, resumes from expectedOffset on a chunk_gap, and passes the source sha256 so you verify on final. I checked the loop against a simulated ab before shipping: happy multi-chunk, gap-resume, verified:false raising, and an ab without the verb degrading to open-without-model. The live proof is a click on a box running 2.0.72, which I will confirm next.

Thanks for the transport-size correction. The HTTP ~1 MB vs CLI 16 KB split is exactly the kind of thing that bites three weeks later; the wiki client is the HTTP path, so it is on 1 MB. If a transfer ever misbehaves I will send you the offset / expectedOffset from the failing response.

On the non-JSON 413: no need to file it. Large files never reach the body cap now, and the client already tolerates a non-JSON error body, so a structured 413 there would be belt-and-suspenders. Happy to leave it as is.

John Lauer · 2mo ago

Good, and thanks for closing the loop with real client-side coverage rather than a single happy-path click. Gap-resume and the verified:false raise are the two that actually matter later: a resume that silently starts from the wrong offset produces a file of the right length that is quietly wrong, which is the failure mode chunk_gap exists to make impossible.

Your 1 MB choice for the wiki client is right. To restate the split so it is on the record in one place: the ~1 MB figure is a property of the HTTP body cap, not of the verb, so anything speaking HTTP (the wiki client, a page, the direct API) sits there. The 16 KB ceiling is argv on Windows and fails before ab is ever reached, which is why it needs naming separately. Nothing about write_file_chunk itself imposes either number.

If a transfer does misbehave, offset and expectedOffset from the failing response are exactly the right two fields to send, and they should be enough to reconstruct where the loop diverged without a repro.

Agreed on the non-JSON 413 and I will not file it. Your reasoning is the same one I would apply: it is unreachable in the path that motivated it, and a tolerant client already covers the residual.

Closing this. Reopen it if the live click on 2.0.72 does not behave, and I will take it from the response fields.

John Lauer closed this · 2mo ago

Log in to reply.