How the exec and conditions design maps onto existing cbang code. Internal — not user documentation.
Originally handlers were synchronous first-true-wins: bool operator()(ctx)
returned false to pass (the HandlerGroup loop advanced) or true to
stop. An async handler must return true to yield to the event loop, after
which the loop has returned and the downstream handlers are unreachable — so
async breaks "pass". The old ArgFilterHandler only worked around this by
hard-wiring one child.
The return value is replaced with an explicit continuation:
using Cont = std::function<void (const CtxPtr &)>;
virtual void operator()(const CtxPtr &ctx, const Cont &next) = 0;- a terminal handler replies and ignores
next - a pass / matcher / wrapper calls
next(ctx) - an async handler captures
nextandctx, returns to the loop, and on completion either replies or callsnext(ctx). It stays alive across the wait —Queryvia aselfSmartPointer, anExecProcessvia the proc pool, acmdcondition via itsCmdConditionProc.
HandlerGroup folds its children right-to-left into one Cont (each child's
continuation runs the rest; the last child's is the group's own next), then
invokes it. Nested groups pass the outer next through, so "pass" crosses
group boundaries. The top-level driver supplies a terminal next that replies
404. A YAML-list sequence folds the same way.
A method body, a then/else body, or a sequence item is a statement.
API::createStatementHandler(cfg) dispatches on shape:
- a list → a
HandlerGroupfolded into the chain (a sequence) - a dict with
if→ aConditionalHandler - otherwise a single statement:
addSteps(headers + theexecpre-step) then the endpoint handler
Method-level validation (args/allow/deny) wraps the whole statement once,
applied by createMethodsHandler via Config::addValidation — not re-applied
per sequence item.
ExecHandler/ExecProcess(api/handler/) generalize the oldArgFilterHandler/ArgFilterProcess.exec()writes the resolvedinputenvelope to stdin.done()parses the result and applies it: mergeargs, setheaders, then eithernext(ctx)(continue) orctx->reply(...)(short-circuit).- Added as a pre-step in
addSteps, before the endpoint handler.
Merged args must be visible to later {args.*} resolution. ArgsHandler
sets the resolver args namespace from ctx->getArgs(); after an exec
merges, that binding has to be refreshed (or share the args object) so the
next handler's SQL and templates see the new values.
Conditions live in api/condition/, one class per file: a Condition base
with a parse factory, the sync exists/compare/not/and/or evaluators,
and the async sql/cmd evaluators. Evaluation is continuation-based
(BoolCallback) so and/or short-circuit correctly even when an operand is
an async sql/cmd. Those async conditions reuse the same plumbing as a query
or an exec — which is why a DB- or process-driven condition needs no separate
"run a query, store a flag, then test it" step.
Typed value substitution (a lone '{ref}' resolving to its native JSON type)
is what lets numeric </<= compares work and exec input embed
non-strings — see Typed values.
The Event::SubprocessPool is maxActive = CPU count, no timeout.
Long-running resizes will queue; consider a per-step timeout and/or a separate
pool. exec and cmd are blocking-async only (the request waits for the
process); there is no fire-and-forget mode.
User docs: binary.md. Decisions: side-channel refs for input, prepared-statement binding for blobs, raw response body for output. Maps onto cbang as follows; all of it is net-new work.
Ordered so the build stays green at each step. Purely additive — no config is
removed — so minVer stays 1.2.0. [x] done, [~] partial, [ ] to do.
-
[x]Multipart parser (cbang HTTP; independent, do first). Parsemultipart/form-data: split by boundary, read each part'sContent-Disposition(name, optionalfilename) andContent-Typeinto (name, filename, type, bytes). A standalone util, unit-tested offline against fixture bodies — no API wiring yet. See Multipart parsing. Done:cbang/http/MultipartParser.{h,cpp}(getBoundary+parse, binary-safe, quoted-string-aware); 11 offline cases incbang/tests/multipartTests. Hardened via adversarial review (line-anchored first delimiter; quoted-value;/escape spoofing fixed). -
[x]Context binary store + resolver roots (see Side channel on Context). Add the body/file-parts store toContext; teachResolverthe{body}/{files.*}roots — metadata fields resolve as strings/numbers, bytes refs are flagged binary and error in a string context. Populate it fromRequest::parseArgs/ an API pre-step: plain multipart fields →args, file parts → store, raw body →{body}. Done:cbang/api/Blob— aJSON::Dictsubclass holding the bytes in a member withsize/type/filenameas entries, so metadata resolves via the normal dotted-path select (noResolverchanges) andwrite()throws on any string/JSON use of the bytes.Context::parseBody()(called fromAPI::operator()) sets thebody/filesresolver roots and folds plain multipart fields into args. Tests:apiTestsBody/Upload cases.* -
[x]Prepared-statement binding (the big one; see Prepared-statement parameter binding). Addmysql_stmt_*prepare/bind/exec toEventDB(blob + existing scalar types), results streamed as now. Change the query build from "resolve to a string" to "resolve to a string + ordered bind list": a binary ref in SQL mode emits?and appends its bytes; non-binary refs interpolate as before. Thread the list throughQueryDef/Query. Binary-only to start. Done:DB/EventDBfully converted to themysql_stmt_*path, regression-tested DB-free viacbang/tests/dbTests. Bind list:Resolver::resolveSQLemits?per binary ref and collects the bytes; threadedQueryDef→Query→EventDB::query→QueryCallback→DB::queryNB, bound asMYSQL_TYPE_BLOBinDB::startExecute.QueryDefchecks placeholder vs param count quote-aware, so a binary ref inside a SQL string literal fails with a clear error;startExecutere-checks viamysql_stmt_param_countas a backstop. Tested: dbTests Blob/Upload/BlobEmbed (exact bytes verified by captured binds). -
[x]Binary response (see Binary response). Add a rawContextreply (bytes +Content-Type) overRequest::reply(Event::Buffer); implementreturn: binary(row 1 / col 1 binary-safe,Content-Typefrom thecontent-typekey → 2nd column →application/octet-stream, no row →404). Done:Query::returnBinarypackages the bytes as aBlobthrough the existing JSON reply callback;Context::replydetects a Blob and replies raw (Request::reply(Event::Buffer)+Content-Type). Thecontent-typekey is read byQueryDefand resolved per request. Tested: dbTests Binary/BinaryTypeCol/BinaryCTKey/BinaryNotFound. -
[x]Validation + config + spec.body:/files:declaration blocks (required/max-size/type→400/413/415) and thecontent-typekey; add the request-body andmultipart/form-dataentries to the generated OpenAPI spec. Done:api/handler/BodyHandlervalidates declaredbody:/files:(size suffixes K/M/G,typestring/list/*glob matched against the media type sans parameters); collected byConfigand wrapped viaaddValidationlikeargs. Spec:requestBodywith binary schema (application/octet-streamor the declared type) or amultipart/form-dataobject schema with per-file properties. Tests: apiTests BodyRequired/BodyTooBig/BodyType/UploadBadType + SpecTest. The offline drivers now dispatch throughHTTP::RequestErrorHandleras the server does, so thrown validation errors reply with real status codes. -
[x]Tests. Offline C++ drivers (no server / no curl): multipart-parser units; resolver{body}/{files.*}(metadata resolves, bytes-in-string errors). The store +return: binaryround-trip needs a DB-backed harness — the currentapiTestsfixture is DB-free, so that path needs either a test DB or a fakedEventDB. Done: multipart-parser units (multipartTests); resolver{body}/{files.*}units (apiTests: metadata + typed compare + bytes-in-string error + multipart fold); binding units (dbTests: raw-body and multipart blob → bound?param byte-exact, embedded-ref error);return: binaryunits (dbTests: bytes + Content-Type chain + 404); validation units (apiTests: 400/413/415 + spec). Both halves of the store/fetch round-trip are covered against the fakedEventDB. -
[ ]Acceptance. Upload via multipart → store the blob → fetch viareturn: binarywith the rightContent-Type, end to end.
JSON::Value has no byte type, so binary cannot ride in the args dict. The
Context gains a binary store — the raw body and the parsed multipart file
parts (name → {bytes, filename, type, size}). The Resolver learns two roots:
{body}/{body.type}/{body.size}{files.<name>}(+.filename/.type/.size)
The metadata fields (.type, .size, .filename) are ordinary strings and
numbers and resolve normally. The bytes refs ({body}, {files.<name>}) are valid
only as a whole value bound into SQL or written as the response; using one in a
string context is an error.
cbang HTTP has no multipart parser (only raw-body access via
Request::getInputBuffer). Add one: split multipart/form-data by boundary,
read each part's Content-Disposition (name, optional filename) and
Content-Type. Plain fields (no filename) merge into the args dict before
validation; file parts go to the Context binary store. Likely a parser util
under http/, driven from Request::parseArgs or an API pre-step.
The biggest change. Today QueryDef::query resolves the SQL to one finished
string (Resolver::resolve(sql, true)) and EventDB runs it as text — there
is no parameter binding. To bind blobs:
EventDBgains prepared-statement support (mysql_stmt_*): prepare, bind an ordered list of params (at least blob + the existing scalar types), execute, and stream results as now.- The query build changes from "resolve to a string" to "resolve to a string
plus an ordered list of bound values." When the resolver hits a binary
ref in SQL mode it emits a
?placeholder and appends the bytes to the bind list; non-binary refs interpolate as before.Query/QueryDefthread the bind list through to the prepared statement. - Scope question: bind only binary params (mixed text-interpolation +
?placeholders in one statement) or move all params to binding over time. The docs commit only to binary being bound; start there.
Context is JSON-only; add a raw reply (bytes + Content-Type) that drops to
Request::reply(Event::Buffer) — the path FileHandler/ResourceHandler
already use. return: binary reads row 1 / column 1 binary-safe
(DB::getString keeps length, no JSON escaping) and replies with it; the
Content-Type comes from the content-type key, else a second column, else
application/octet-stream; no row → 404.
- Large uploads are buffered in memory (no streaming to disk or to the DB).
- The binary path is verified against the faked
EventDBonly; the opt-in real-MariaDB acceptance test (plan step 7) has not been run.
User docs: exec.md (temp files, files result),
sql.md (into:, return: pass),
handlers.md (reply:). Enables e.g. image storage
with resize-on-request — no DB queue, no external worker.
Decisions:
- No carried status. The statement that replies chooses the code;
reply:takes a siblingcode:key.statusstays terminal. - Temp files via
TMPDIR. A private directory per exec step, exported to the child asTMPDIR, removed when the step completes (after output files are read). Binary input refs are written there; the envelope carries path strings. - Returned files merge into the
filesroot, shadowing same-name uploads, so binding/validation/reply work identically. - Resolved-result capture (
into:) lives on the query statement; exec feeds forward viaargsandfilesas before.
[x]steps:keyword.createStatementHandler: a dict withstepsfolds the list (after itsheaders/execpre-steps); a bare list body is shorthand for{steps: [...]}. A handler-implying key alongsidestepsis a config error. Lets a method keepargs/helpbeside its pipeline. Tested: apiTests Steps/StepsZero; spec picks uphelp+ args besidesteps.[x]Query capture.into: <name>on a query statement: the formatted result (incl. a binary Blob) is set on the request resolver and the chain continues; errors still reply.return: passruns and discards.QueryDefreads both keys (combining them is a config error);QueryHandlerswitches reply → capture on success. Tested: dbTests Pipe (pass + dict capture + shaped reply) and BlobPipe (binary capture → re-bound?param → raw reply with carried Content-Type).[x]reply:statement.ReplyHandlerdeep-copies its template, resolves it inside a wrapper list (so a lone'{ref}'keeps its native type, binary included), and replies —Context::replyhandles Blob → raw body. Siblingcode:sets the status. Tested: apiTests Reply/ReplyCode. Spec generation now tolerates bare-list method bodies.[x]Exec temp-file input.ExecHandlerwalks the resolvedinputenvelope; each Blob is written to a per-stepTemporaryDirectory(named by its envelope key) and replaced by its path string. The directory is owned byExecProcess(auto-removed afterdone) and exported asTMPDIR.[x]Exec file output.ExecProcess::donereads the resultfilesdict (path or {path,type,filename}), loads each into a Blob, and merges into the resolver'sfilesroot before continuing.[x]Tests. apiTests: Steps/StepsZero, Reply/ReplyCode, Upcase (body → temp file → transformed file → binary reply, temp dir verified removed); dbTests: Pipe, BlobPipe.[x]Acceptance. dbTests Resize: PUT body with NUL bytes →ImageSet(?)bound byte-exact → fetch viainto→ exec transform via temp files →ImageSetSize(?)re-bound transformed bytes → rawimage/pngreply. The full exec.md pipeline against the fakes.
User docs: sql.md. Triggered by the {~ref}-eaten-at-
load-time bug: API::load resolved the whole config once and a tilde ref
could never be "missing", so every {~x} became literal null before the
first request.
Decisions:
- One resolve mode parameter, no instance state.
resolve(s, partial); the load-time pass is partial (missing refs, tilde included, stay literal for request time).~handling moved out ofselect()(pure lookup) into the resolve layer (selectRef). - No SQL text mode.
resolveSQLbinds every ref as a prepared- statement parameter (?); quoting/escaping code deleted, injection structurally impossible. Params are JSON values end-to-end (DB::queryNB): null → SQLNULL, bool → 1/0, else string value. - Strict refs. At request time a missing
{ref}is an error naming the fix (use {~ref});{~ref}resolves null/NULL. Optional args must be referenced as{~args.x}. - A ref inside a SQL string literal is caught by the quote-aware
placeholder/param count check → "use CONCAT()".
:S(force SQL quote) is gone; a:fmtspec on a SQL ref formats the value, then binds the string — uniform with non-SQL templates. - SQL fragments can only come from
{options.*}, resolved at load.
Config migration: {args.x} where x is optional → {~args.x};
LIKE '%{q:s}%' → LIKE CONCAT('%', {q}, '%').
Tested: resolverTests (bind/partial/strict/typed), dbTests Opt*/Strict*/
AssetUpload (multipart + exec renditions + {~files.*} NULL binds), all
goldens regenerated and reviewed.