Chapter Orientation
URL query strings and HTTP forms are two similar-but-different key-value grammars, and xhttp gives two tool layers.The zero-allocation layer (the query module): xrtQueryNext traverses directly over the raw string — xquerypair's XQUERY_HAS_VALUE precisely distinguishes a (no value), a= (empty value), =v (empty key); xrtQueryFind/Count/Validate complete the family; enabling query_codec adds percent encoding/decoding.The owning layer (the query_params module): the xqueryparams container parses, rewrites (Set position-preserving dedup / Append append / Sort stable sort / Compact compaction), and rebuilds — the standard path of "change one parameter and stitch it back". The form side has two shapes: application/x-www-form-urlencoded (the same grammar + the +-space semantics) and multipart/form-data (Chapter 92's quoted-string in live combat). The raw layer (borrowing views) and owning layer (containers), are fully isomorphic with Chapters 89/92's layering — grammar layer first, containers after.
103.1引入
High-frequency API-client operations: pagination (page=2 becomes page=3), adding filters (append tag=network), copying a link with changed parameters. Hand string-splicing stumbles on duplicate keys: in ?tag=c&tag=xlang&debug tag has two values — append a third? Replace all? Keep which position? xqueryparams' semantics are explicit: Set keeps the first same-name position and drops the rest, Append adds at the tail, Find iterates all same-named in order — three intents, three verbs. The zero-allocation layer's value is the read-side hot path: a proxy routes by the debug flag and must glance at the query of every request — view traversal costs nothing.
The a vs a= distinction runs through both layers: XQUERY_HAS_VALUE unset = no value (no = written in the URL), set and empty = explicit empty value — whether to re-add the = on round-trip writing is decided by it. The same philosophy as Chapter 102's presence bits: grammatical structure is never guessed from view length.
103.2概念
The zero-allocation layer: iteration, finding, and validation
xrtQueryNext(串, &偏移, &对) (string, &offset, &pair) iterates in three states (XQUERY_NEXT_ITEM/END/ERROR); duplicate keys keep original order; the leading ? may be omitted; empty segments from consecutive/leading/trailing & are skipped. xrtQueryFind iterates same-named items by name from an offset (the standard read for duplicate keys); xrtQueryCount counts; xrtQueryValidate decides legality.This layer does not decode — %2F is just three characters without interpretation; decoding values goes through query_codec (leniency switches like XQUERY_PARAMS_LENIENT_PERCENT live there).
The owning layer: the container's four verbs
- Parse semantics:
application/x-www-form-urlencodedrules — strict by default, illegal percents rejected (ErrorOffsetgives the percent position); lenient mode (XQUERY_PARAMS_LENIENT_PERCENT) keeps invalid percent signs as ordinary bytes — browser-URLSearchParams-compatible scenarios. - Modification semantics: append/set copy the name and value (the input may borrow from the container itself — in-place modification is safe);
Getreads the first same-named,Finditerates all; container modification invalidates existing borrowed views (Chapter 18's general rule). - Caps:
xqueryparamsconfigcaps pairs/name length/value length/total decoded bytes — the physical string region compacts before touchingMaxBytes(the cap logic excludes growth headroom). - Atomicity:
ParseAppendparses on a working copy — quota/syntax/memory failures never expose a partial append.
The encoding layer and writing
query_codec depends on query and the percent codec: build an RFC 3986 query directly from "unencoded fields" — the xrtQueryBuild/QueryAppendWrite family bakes in the "which characters to encode" rules (&/= structure preserved, space form chosen). The raw layer (parse and forward without rewriting) and the codec layer (constructing new strings) trim independently — a pure proxy needs no encoder; trimming is per-face.
The dual form shapes
- urlencoded: the body is just query grammar (
a=1&b=2) — all of this chapter's vocabulary applies directly;Content-Type: application/x-www-form-urlencoded. - multipart/form-data: a boundary-delimited part structure (each part with its own headers and content) — the carrier for file uploads; the boundary comes from the Content-Type parameter (that frequent consumer of quoted-string decoding, Chapter 92); the
xrtFormDataConfigInit/FormDataAppendBodyfamily provides building and parsing.examples/http/form_data/multipart/multipart_stream/multipart_writecover building, streaming, and writing.
The division with Chapter 92's parameter layer
Chapter 92 covered semicolon parameters inside field values (Content-Type; key=value — HTTP header grammar); this chapter covers URL query and forms (&-separated — URI/form grammar). The two iterators share a shape (offset + three states) but differ in grammar domain — picking the wrong domain is a parse error. The + semantics hold only in the form domain (urlencoded + = space; in a pure URI query + is an ordinary character — the divide from Chapter 89).
103.3示例
First complete program: zero-allocation traversal and semantic distinctions
The program below is from examples/url/query/main.c — four key-value shapes traversed in one pass:
#include <stdio.h> #include <xhttp.h> /* 演示无分配遍历重复查询键,并区分缺失值和空值。 */ int main(void) { xstrview Query = XRT_STR_LITERAL("?tag=c&tag=xlang&debug&empty="); xquerypair Pair; size_t iOffset = 0; for ( ;; ) { xquerynext Next = xrtQueryNext(Query, &iOffset, &Pair); if ( Next == XQUERY_NEXT_END ) { break; } if ( Next == XQUERY_NEXT_ERROR ) { return 1; } printf("%.*s", (int)Pair.Key.Size, Pair.Key.Data); if ( (Pair.Flags & XQUERY_HAS_VALUE) != 0 ) { printf(" = %.*s", (int)Pair.Value.Size, Pair.Value.Data); } else { printf(" = <missing>"); } printf("\n"); } return 0; }
What just happened. (1) Input ?tag=c&tag=xlang&debug&empty= — four items: the duplicate key tag yields both values in original order, debug has no value (flag unset — printed <missing>), empty is an explicit empty value (flag set, length 0 — printed as empty). (2) Four shapes, one loop: one XQUERY_HAS_VALUE bit distinguishes the two pairs of semantics (no value vs empty value). (3) Zero allocation throughout — Key/Value borrow the original string; this is the cost shape of a proxy/gateway reading queries. (4) This output shape doubles as a verification checklist: any query-handling code should branch explicitly on these four inputs just like this.
Second complete program: container rewriting and rebuilding
The second program is from examples/url/query_params/main.c — the parse, Set/Append, Build loop:
#include <xhttp.h> #include <stdio.h> /* 解析、修改并重新构建一组拥有型 URL 查询参数。 */ int main(void) { xqueryparams* pParams; str sQuery; size_t iError; size_t iSize; pParams = xrtQueryParamsParse( XRT_STR_LITERAL("page=1&tag=c&tag=network"), NULL, &iError ); if ( pParams == NULL ) { return 1; } if ( !xrtQueryParamsSet( pParams, XRT_STR_LITERAL("page"), XRT_STR_LITERAL("2") ) || !xrtQueryParamsAppend( pParams, XRT_STR_LITERAL("tag"), XRT_STR_LITERAL("xlang") ) ) { xrtQueryParamsDestroy(pParams); return 1; } sQuery = xrtQueryParamsBuild(pParams, &iSize); if ( sQuery == NULL ) { xrtQueryParamsDestroy(pParams); return 1; } printf("%.*s\n", (int)iSize, sQuery); xrtFree(sQuery); xrtQueryParamsDestroy(pParams); return 0; }
What just happened. (1) ParamsParse("page=1&tag=c&tag=network") parses three items under form rules (+ and percent decoding happen here; ErrorOffset reports the percent position on failure). (2) Set("page","2") — page changes value in place; Append("tag","xlang") — a third tag appended at the tail: the intent difference of Set vs Append toward duplicate keys shows itself (Set would clear the other same-named; tag uses Append here, so three values coexist). (3) Build rebuilds page=2&tag=c&tag=network&tag=xlang — a canonical-form zero-terminated string, freed with xrtFree. Pagination, filter-adding, copy-and-change — the three-line template is these two Set/Append plus one Build.
103.4契约
- Zero-allocation layer: iterate/find/count/validate all views; three-state return; duplicate keys in order; empty segments skipped; leading ? optional; no decoding.
- Semantic distinction:
XQUERY_HAS_VALUEdistinguishes no-value/empty-value/empty-key, four shapes; round-trip writing preserved by the flag. - Container parsing: form rules (+→space, percent decoding); strict by default (+ErrorOffset position); lenient mode keeps invalid percents as bytes.
- Four rewrite verbs: Set (position-preserving dedup) / Append (append) / Pair forms (preserve no-equals items) / Sort (stable by name bytes) / Compact (clear dead storage).
- Ownership: the container copies append/set inputs (may borrow itself); modification invalidates borrowed views; Build returns an xrtFree string.
- Caps: four lines — pairs/name length/value length/total decoded bytes; compact before judging the cap; ParseAppend atomic (failures expose no partial result).
- Encoding layer:
query_codecindependent (raw and construction trim separately); structural-character rules built in. - Forms: urlencoded = query grammar directly; multipart = part structure (FormData family); boundary from the Content-Type parameter.
- Domain boundary: this chapter's
&domain; Chapter 92's;domain — iterators same-shaped, different grammar.
Common Mistakes
Symptom: ?debug rewritten into ?debug= — semantics shift from "flag present" to "value empty"; or the reverse, dropping the =.
Cause: no value and empty value are two grammatical structures; judging by Pair.Value.Size == 0 conflates them — judgment reads XQUERY_HAS_VALUE, writing uses AppendPair/SetPair to preserve the shape.
if ( Pair.Value.Size == 0 ) { treat_as_flag(Pair.Key); /* debug and empty= treated alike */ }
if ( (Pair.Flags & XQUERY_HAS_VALUE) == 0 ) { treat_as_flag(Pair.Key); /* truly no value */ } else { treat_as_value(Pair.Key, Pair.Value); /* has a value (possibly empty string) */ }
Symptom: using ParamsParse on a non-form URI query — a+b decodes into a b, but under pure URI semantics + is just a plus character.
Cause: +→space is a urlencoded form rule; in RFC 3986 query + is an ordinary character. The two layers differ: reading URI queries uses the zero-allocation layer (no decoding) + percent decoding as needed; only form bodies use ParamsParse.
/* handling a search URL's query: + wrongly taken as space */ pParams = xrtQueryParamsParse(Url.Query, NULL, &iErr);
/* URI query: zero-allocation traversal + explicit percent decoding */ while ( xrtQueryNext(Url.Query, &iOff, &Pair) == XQUERY_NEXT_ITEM ) { percent_decode_value(Pair.Value, Buf, sizeof(Buf)); /* decode %xx only */ }
Symptom: the pagination link carries only page — tag, sort, and other parameters all vanish.
Cause: building a new query from scratch (writing only page) instead of modifying the existing container. Correct path: Parse existing → Set("page", N) → Build — change one parameter, touch nothing else.
snprintf(Next, "%s?page=%u", Path, Page + 1); /* other parameters evaporate */
pParams = xrtQueryParamsParse(Url.Query, NULL, &iErr); xrtQueryParamsSet(pParams, XRT_STR_LITERAL("page"), PageText); sNext = xrtQueryParamsBuild(pParams, &iSize); /* page changed in place; tag/sort and friends fully preserved */
Programming Exercises
Construct a query with the four shapes (no value/empty value/empty key/normal); traverse and print with the zero-allocation layer; then ParamsParse → Build and compare round-trip differences (which shapes ParamsParse normalizes). Acceptance criteria: you can point out the boundary between semantic preservation and normalization in the round trip.
Implement next_page(当前URL文本, &下一页文本) (current URL text, &next-page text): UrlParse → ParamsParse → Set("page") → Build → stitch the URL back. Verify against URLs with duplicate tag keys and a debug no-value flag. Acceptance criteria: pagination touches only page; duplicate keys and the flag preserved verbatim; the produced zero-terminated string freed correctly.
Implement urlencoded_to_multipart(字段容器, 输出, 容量) (field container, output, capacity): render the Params container's fields as multipart parts (boundary self-generated, each part with Content-Disposition: form-data; name="..." — names passing quoted-string rules). Implement the reverse, parsing multipart back into a container. Cross-check with the form_data sample. Acceptance criteria: bidirectional conversion preserves fields (including values with quotes/semicolons); the boundary satisfies the delimiter-uniqueness requirement (never appearing in the body).
Summary
| Topic | Quick reference |
|---|---|
| Two-layer structure | zero allocation (Next/Find/Count/Validate, no decoding) + owning (Params container) |
| Four shapes | a = no value / a= = empty value / =v = empty key / normal - one XQUERY_HAS_VALUE bit sorts all |
| Container parsing | form rules (+→space/percent decoding); strict default + ErrorOffset; lenient mode available |
| Four verbs | Set position-preserving dedup / Append append / Sort stable / Compact clears dead bytes |
| Atomicity | ParseAppend working copy - failures expose no partial append |
| Caps | four lines - pairs/name/value/total bytes; compact before judging the cap |
| Encoding layer | query_codec independent (constructs new strings); raw and construction trim separately |
| The + domain | form = space; URI query = ordinary character - never mix the two layers |
| Dual form shapes | urlencoded = query grammar; multipart = parts + boundary (FormData family) |
| Boundary with Chapter 92 | this chapter's & domain (URI/forms); Chapter 92's ; domain (header field values) |
📖 API references in this chapter:xhttp-query (8 entries)xhttp-query_params (24 entries)xhttp-form (8 entries)xhttp-form_data (24 entries)
Click for full signatures, parameter constraints, return values and error notes