Chapter Orientation

Chapter 89 covered "how to parse one field"; this chapter covers "how to use one family of fields". HTTP headers include a batch of fields with their own structured syntax: Content-Type's semicolon parameters (charset=UTF-8; boundary="part;42" — values may contain quotes and semicolons), Connection/Upgrade's token lists (connection capability negotiation — the deciding input of Chapter 95's WebSocket handshake), Expect (client expectations — the 100 Continue flow), TE/Trailer (transfer coding and trailer capability, Chapter 91). Each family has its own parsing traps (quoted-string escapes, token case, repeatable fields), and XRT provides an independent small module per family — ParamNext parameter iteration, FieldTokenFind token lookup, xrtHttpTeParse negotiation aggregation. The daily toolbox of proxy, middleware, and protocol implementers.

92.1引入

Implementing a multipart form parser, the first step is extracting boundary from Content-Type: multipart/form-data; boundary="part;42" — the value carries quotes, and inside the quotes is a semicolon (boundaries may contain semicolons!). Anyone splitting with strchr(';') crashes here: part;42 is cut in half. Or take a WebSocket upgrade check: does Connection contain the upgrade token? Case, spaces, quotes — a hand-written check is another pile of edge cases.

The common thread of the two scenes: a field value's syntax is more complex than "looks like text" — RFC 9110 defines a different grammar per field family (token-list, semicolon parameters, quoted-string), each with canonical parsing rules and historical implementation holes. XRT's per-family layer implements each family's rules as small independent APIs: the parameter layer's quote decoding, the token layer's case-insensitive lookup, the TE layer's q-value negotiation (seen in Chapter 91). This chapter walks each family: what the syntax is, how to use the API, where the pits lie.

92.2概念

The parameter layer: the semicolon world and quoted-string

The "semicolon parameter" syntax of field values (Content-Type, Content-Disposition, and friends): 主值; key=value; key2="quoted string" (main value; key=value; key2="quoted string"). The API trio:

  • xrtHttpParamNext(值, &偏移, &参数) (value, &offset, &param): iterates yielding xhttpparam — a Name view, Flags (XHTTP_PARAM_HAS_VALUE — valueless parameters allowed), and the raw Value view.
  • xrtHttpParamValueWrite(&参数, 输出, 容量, &长度) (&param, output, capacity, &length): decodes the quoted-string then writes the value — quotes stripped, \" unescaped, capacity atomic. boundary="part;42" decodes to part;42 — the semicolon survives intact.
  • xrtHttpParamCount/ParamFind/ParamBuild: count, find by name, build a new parameter string.

Why "iteration yields raw views, decoding is a separate step": most consumers only compare parameter names or lengths — skipping the unquote costs nothing; only when the value content is truly needed is it decoded.

The token layer: connection capability

Connection/Upgrade/Transfer-Encoding are all token-lists (Chapter 89's TokenNext territory); the per-family layer adds semantic lookup: xrtHttpFieldTokenFind — whether a specified token exists in a field value (case-insensitive). "Does Connection contain upgrade" answered in one line; Chapter 95's WebSocket handshake check and a proxy's hop-by-hop field stripping (the field names Connection lists must be deleted before forwarding — xrtHttpConnectionNext iterates out that list) both stand on it.

Expect and 100 Continue

Before sending a large body the client asks "will the server take it": Expect: 100-continue. xrtHttpExpectFields/ExpectValid decide whether a request carries a legal 100-continue expectation — the server may answer 100 Continue first and then receive the body, or refuse with 417.Why a dedicated family: 100's timing interweaves with the body stream (the server may answer 100, receive the body, then answer the final status), and implementers easily miss the lenient path of "sending the body without waiting for 100" — the per-family layer standardizes the verdict; timing belongs to the connection layer.

The full lookup family over field arrays

The field arrays parsed in Chapter 90 come with a lookup family (implemented at the foundation layer, reused at the header layer): xrtHttpFieldFind (first by name), xrtHttpFieldNext (same-name multi-value iteration — the Set-Cookie scene), xrtHttpFieldGet/FieldGetUnique (take-value/unique-value semantics — GetUnique is for fields that "semantically must appear once"; duplicates are an error), xrtHttpFieldNameEqual (case-insensitive comparison), xrtHttpFieldTokenCursorInit+FieldTokenCount (token counting and cursor). This family is the complete vocabulary of "querying the head" — the foundation of middleware decisions by Route/Cache-Control.

The trimming surface of the family modules

One feature macro per family (HTTP_HOST/HTTP_PARAM/HTTP_TARGET/HTTP_TE/HTTP_CONNECTION/HTTP_EXPECT/HTTP_TRAILER/HTTP_UPGRADE...): an embedded client carries only Host+Param without upgrade and Expect; a gateway carries all. The per-family modules introduce no interdependencies — trimming is truly independent.

The layering of the field families

1Foundation (Chapter 89)FieldParse field views + TokenNext list iteration
2Parameter familyParamNext/ValueWrite - semicolon parameters and quoted-string decoding
3Semantic lookup familyFieldFind/FieldGetUnique/FieldTokenFind - case-insensitive retrieval
4Per-field familiesTE/Encoding/Upgrade/Expect/Trailer/Connection - independently trimmable negotiation mini-modules

92.3示例

First complete program: parameter iteration and quote decoding

The program below is from examples/http/param/main.c — the standard cure for the quoted-string trap:

examples/http/param/main.c源码 ↗
#include <stdio.h>

#include <xrt.h>






















 */


/* 逐项读取参数,并在需要时解码 quoted-string。 */
int main(void)
{
	xstrview Text = XRT_STR_LITERAL(
		"charset=UTF-8; boundary=\"part;42\""
	);
	xhttpparam Param;
	xhttpnext Next;
	char Value[64];
	size_t iOffset = 0;
	size_t iSize;

	while ( (Next = xrtHttpParamNext(
		Text, &iOffset, &Param
	)) == XHTTP_NEXT_ITEM ) {
		if ( (Param.Flags & XHTTP_PARAM_HAS_VALUE) == 0 ) {
			printf("%.*s\n", (int)Param.Name.Size, Param.Name.Data);
			continue;
		}
		if ( !xrtHttpParamValueWrite(
			&Param, Value, sizeof(Value), &iSize
		) ) {
			return 1;
		}
		printf("%.*s = %.*s\n",
			(int)Param.Name.Size, Param.Name.Data,
			(int)iSize, Value);
	}
	return (Next == XHTTP_NEXT_END) ? 0 : 2;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/http/param/main.c -lws2_32 -liphlpapicharset = UTF-8boundary = part;42

What just happened. (1) Input charset=UTF-8; boundary="part;42" — two parameters. (2) ParamNext yields them one by one: the first has a value (the HAS_VALUE flag), Name=charset, raw Value=UTF-8 (a bare token needing no decode, though going through ValueWrite uniformly is also correct); the second has Name=boundary and the raw value quoted "part;42". (3) ParamValueWrite decodes: quotes stripped, the inner ; preserved whole — output part;42.This is the flip side of the strchr-split crash scene: semicolons inside quotes don't split, quote pairing is validated, escape sequences restored — all handled by the decoder. Insufficient capacity writes zero — consistent with the library-wide atomicity.

Second complete program: the field lookup and write-back tour

The second program is from examples/http/field_tour/main.c — four self-checking lines over field blocks, lookup, tokens, and write-back:

examples/http/field_tour/main.c源码 ↗


















 */

#include <stdio.h>
#include <string.h>
#include <xrt/http.h>

#define SV(x) XRT_STR_LITERAL(x)

int main(void)
{
	static const char sBlock[] =
		"Connection: keep-alive\r\n"
		"Cache-Control: max-age=60\r\n"
		"Connection: Upgrade\r\n";
	static const xhttpfield Fields[3] = {
		{ XRT_STR_INIT("Connection"), XRT_STR_INIT("keep-alive, Upgrade") },
		{ XRT_STR_INIT("Cache-Control"), XRT_STR_INIT("max-age=60") },
		{ XRT_STR_INIT("Accept-Encoding"), XRT_STR_INIT("gzip, deflate") }
	};
	xhttpfield Field;
	size_t iOffset = 0;
	size_t iCount = 0;
	size_t iSize = 0;
	char Buffer[256];
	size_t iFound;

	/* FieldBlockCount:整块严格计数。 */
	if ( !xrtHttpFieldBlockCount(SV(sBlock), &iCount) || (iCount != 3u) ) {
		return 1;
	}
	printf("block-count=%zu", iCount);

	/* FieldNext:游标式逐字段(走完块为止)。 */
	{
		size_t iSeen = 0;

		while ( xrtHttpFieldNext(SV(sBlock), &iOffset, &Field) ==
			XHTTP_NEXT_ITEM ) {
			iSeen++;
		}
		printf(" next=%zu\n", iSeen);
	}

	/* Find / Get / Count / GetUnique / NameEqual / ValueValid。 */
	iFound = xrtHttpFieldFind(Fields, 3u, SV("Connection"), 0u);
	if ( iFound != 0u ) { return 1; }
	{
		const xhttpfield* pGet = xrtHttpFieldGet(Fields, 3u,
			SV("Connection"));

		printf("get=%.*s",
			pGet ? (int)pGet->Value.Size : 6,
			pGet ? pGet->Value.Data : "(null)");
	}
	printf(" value-valid=%d",
		xrtHttpFieldValueValid(Fields[0].Value) ? 1 : 0);
	(void)xrtHttpFieldCount(Fields, 3u, SV("nope"));
	printf(" count=%zu",
		xrtHttpFieldCount(Fields, 3u, SV("Connection")));
	{
		const xhttpfield* pUnique = NULL;

		printf(" unique=%s",
			xrtHttpFieldGetUnique(Fields, 3u, SV("Accept-Encoding"),
				&pUnique) == XHTTP_NEXT_ITEM ? "hit" : "miss");
	}
	printf(" find=%d find2=%zu\n",
		xrtHttpFieldNameEqual(SV("connection"), SV("Connection")) ? 1 : 0,
		xrtHttpFieldFind(Fields, 3u, SV("Accept-Encoding"), 0u));

	/* FieldToken 游标:聚合同名 Connection 字段的 token。 */
	{
		xhttpfieldtokencursor Cursor;
		xstrview Token;
		static const xhttpfield Dup[2] = {
			{ XRT_STR_INIT("Accept-Encoding"), XRT_STR_INIT("gzip") },
			{ XRT_STR_INIT("accept-encoding"), XRT_STR_INIT("deflate") }
		};

		xrtHttpFieldTokenCursorInit(&Cursor);
		printf("tokens:");
		while ( xrtHttpFieldTokenNext(Dup, 2u, SV("Accept-Encoding"),
			&Cursor, &Token) == XHTTP_NEXT_ITEM ) {
			printf(" %.*s", (int)Token.Size, Token.Data);
		}
		{
			size_t iTokens = 0;

			(void)xrtHttpFieldTokenCount(Dup, 2u,
				SV("Accept-Encoding"), &iTokens);
			printf(" count=%zu", iTokens);
		}
		printf(" find=%d\n",
			xrtHttpFieldTokenFind(Dup, 2u, SV("Accept-Encoding"),
				SV("deflate")) == XHTTP_NEXT_ITEM ? 1 : 0);
	}

	/* FieldWrite / FieldBlockWrite。 */
	{
		xstrview Block = SV(sBlock);

		(void)xrtHttpFieldWrite(&Fields[0], Buffer, sizeof(Buffer), &iSize);
		printf("write=%c", Buffer[0]);
		(void)xrtHttpFieldBlockWrite(Fields, 2u, Buffer, sizeof(Buffer),
			&iSize);
		printf(" blockwrite=%s\n",
			iSize > 0u ? "ok" : "fail");
		(void)Block;
	}
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/http/field_tour/main.c -lws2_32 -liphlpapiblock-count=3 next=3get=keep-alive, Upgrade value-valid=1 count=1 unique=hit find=1 find2=2tokens: gzip deflate count=2 find=1write=C blockwrite=ok

What just happened. (1) Line 1: field-block counting and iteration — the batch-processing surface of multi-field arrays. (2) Line 2 runs the lookup family in one pass: value by name, view validity, counting, GetUnique unique-value semantics (the positive use of this chapter's Pitfall 3), and two Find calls verifying same-name multi-value returns in order (find2=2 — the second same-named field). (3) Line 3, the token family: Accept-Encoding's list iteration, counting, and named-token lookup — a runnable footnote to this chapter's "token semantic lookup". (4) Line 4, the write-back round trip (write=C is one field, blockwrite the whole block) — re-verifying Chapter 89's atomicity conventions. The companion examples/http/small_fields covers the joint tour of the five per-field families TE/Encoding/Upgrade/Expect/Trailer, and examples/http/host covers the authority split — every API in this chapter has a runnable sample.

92.4契约

  • Parameter iteration: raw views first (Name/quoted Value), the HAS_VALUE flag distinguishing valueless parameters; decoding (ValueWrite) on demand — quote stripping and escape restoration, capacity atomic.
  • Token semantic lookup: FieldTokenFind case-insensitive; the field names listed by Connection iterate via ConnectionNext — the basis of hop-by-hop stripping.
  • Unique-value semantics: FieldGetUnique for single-value fields — duplicates error rather than "take the first" (silent selection = accepting ambiguity).
  • Expect: ExpectFields/ExpectValid decide the expectation's presence and legality; 100 timing belongs to the connection layer.
  • Trailer declaration: the trailer list announced by the Trailer field is queryable — reconciled against Chapter 91's read-side array.
  • q negotiation: thousandths integers (TE/Encoding share the model); unmentioned q=0.
  • Independent family trimming: the per-family macros are mutually independent; ParamValueCursorInit/ValueNext provide a value-iteration shape without decoding.
  • Zero allocation: the family APIs are all borrowed views + caller buffers.

Common Mistakes

Pitfall 1: splitting parameters with strchr — the semicolon inside quotes crashes:Pitfall 1: splitting parameters with strchr — the semicolon inside quotes crashes

Symptom: a multipart boundary comes out wrong — boundary="part;42" cut into boundary="part and 42", and form parsing goes entirely wrong.

Cause: in the semicolon-parameter syntax, a semicolon inside a quoted-string is not a separator. Hand-written splitting knows nothing of quote context.

错误写法
char* p = strtok(Value, ";");   /* part;42's semicolon treated as a separator */
正确写法
xhttpparam Param; size_t iOff = 0;
while ( (N = xrtHttpParamNext(Value, &iOff, &Param)) ==
		XHTTP_NEXT_ITEM ) {
	char Buf[64]; size_t iSize;
	if ( (Param.Flags & XHTTP_PARAM_HAS_VALUE) &&
			xrtHttpParamValueWrite(&Param, Buf,
				sizeof(Buf), &iSize) ) {
		use(Param.Name, Buf, iSize);   /* decoded value: part;42 whole */
	}
}
Pitfall 2: case-sensitive token lookup:Pitfall 2: case-sensitive token lookup

Symptom: checking Connection: keep-alive, Upgrade for upgrade with strstr(value, "upgrade") — returns NULL, and the WebSocket handshake is refused.

Cause: HTTP token semantics are case-insensitive, and on-the-wire casing is all over the map (Upgrade/upgrade/UPGRADE are all legal).

错误写法
if ( strstr(Field.Value, "upgrade") != NULL ) {
	/* "Upgrade"'s casing never gets in: a legal handshake refused */
}
正确写法
if ( xrtHttpFieldTokenFind(Fields, iCount,
		XRT_STR_LITERAL("upgrade")) ) {
	/* case-insensitive + quote context handled by the family API */
}
Pitfall 3: handling duplicates of a single-value field by "take the first":Pitfall 3: handling duplicates of a single-value field by "take the first"

Symptom: Content-Length: 11 and Content-Length: 12 both appear; the code handles the first and ignores the second — front and back ends understand differently: a smuggling variant.

Cause: a semantically unique field appearing twice is erroneous input. "Take the first" silently dissolves the ambiguity — choosing an understanding on the attacker's behalf.

错误写法
xstrview V = first_field("Content-Length", ...);
parse_length(V);   /* the second CL goes unquestioned */
正确写法
xstrview V;
if ( !xrtHttpFieldGetUnique(Fields, iCount,
		XRT_STR_LITERAL("Content-Length"), &V) ) {
	reject("conflicting content-length");  /* duplicate means reject */
}

Programming Exercises

Basic: run the five-family tour

Run both samples, small_fields and param, annotating each output line with its family and API. Change param's input to boundary="a\"b" (escaped quote) and verify decoding. Acceptance criteria: escape restoration correct (a"b); the valueless-parameter path takes the HAS_VALUE-false branch.

Advanced: a Content-Type consumer

Implement content_type(字段值, &主类型, &子类型, 参数数组, 容量) (field value, &main type, &sub type, param array, capacity): split out the two segments of multipart/form-data and all parameters (quote-decoded). Verify against text/plain (no parameters) and multipart/mixed; boundary="x;y"; charset=UTF-8 (multi-parameter with a quoted semicolon). Acceptance criteria: both outputs match hand-derivation; main/sub types compared after case normalization.

Challenge: a hop-by-hop field stripper

Implement a proxy's hop-by-hop handling: iterate Connection for the hop-by-hop field-name list + the standard hop-by-hop set (Connection/Keep-Alive/TE/Trailer/Upgrade), delete hits from the field array (compacting it), then FieldBlockWrite the block back. Acceptance criteria: after stripping a request with Connection: keep-alive, X-Push, X-Push is gone while Connection itself remains but without the deleted item; end-to-end fields (Accept etc.) untouched; zero heap allocation throughout.

Summary

TopicQuick reference
Parameter layerParamNext iteration (raw view + HAS_VALUE); ValueWrite decodes (quote strip/escape/atomicity)
quoted-stringsemicolons inside quotes don't split, \" restored - hand-slicing with strchr is an accident source
Token lookupFieldTokenFind case-insensitive; ConnectionNext iterates hop-by-hop field names
Lookup familyFieldFind/FieldNext (multi-value)/FieldGet (value)/FieldGetUnique (duplicate = error)
ExpectExpectParse decides the 100-continue expectation; timing belongs to the connection layer
Trailer declarationthe Trailer field's announced list is queryable; reconciled against the read-side array
q negotiationthousandths integers; TE/Encoding share the model; unmentioned q=0
Upgrade familyUpgrade iterate/count/build - Chapter 95's handshake parts
Trimmingone macro per family (HOST/PARAM/TE/CONNECTION/EXPECT/TRAILER/UPGRADE...)
Zero allocationthe whole family borrows views + caller buffers

📖 API references in this chapter:http (72 entries)http_te (16 entries)http_connection (7 entries)http_expect (13 entries)http_trailer (6 entries)

Click for full signatures, parameter constraints, return values and error notes