Chapter 17 / 51
Volume 3 · Protocol Development

Routing: static table + dynamic patterns + method slots

The host ships no router — by design, not omission. This chapter hands you the routing framework proven in demo-single and stress-tested by routebench at ten thousand routes: two tables, one compile, three returns.

Overview

Positioning: routing is the hub of HTTP business. The finished product here is the demo's modules/protocol.h — take it away as-is. The real 405+Allow behavior was measured in chapter 2; this chapter explains where it comes from.

17.1 Intro

"Should routing live in the host?" is a real question: built in, flexible path needs (regex, prefixes, wildcards) start squeezing the config system; left out, every app writes its own. XServer's choice is to provide parts, not the whole machine: xrtMap (a hash table) for exact matches, xrtPattern (a batch pre-compiled pattern matcher) for parameterized paths — twenty lines of assembly buy a production-grade router, and special needs (regex routing, version prefixes) can grow their own anytime.

17.2 The two-table pattern

route.h — the registration manifest (real source)verified on a real machinesource: demo-single/route.h
static void RouteHTTP_Init(void)
{
    G_StaticRouteTableHTTP = xrtMapCreate(sizeof(RouteInfoHTTP));

    /* static table: exact URIs (xrtMap hash) */
    AddStaticRouteHTTP("/api/v1/items", XHTTP_METHOD_GET,  Handle_API_List);
    AddStaticRouteHTTP("/api/v1/items", XHTTP_METHOD_POST, Handle_API_Add);

    /* dynamic table: {id} segment capture (compiled after registration) */
    AddDynamicRouteHTTP("/api/v1/items/{id}",
        XHTTP_METHOD_GET,                    Handle_API_Item);
    AddDynamicRouteHTTP("/api/v1/items/{id}",
        XHTTP_METHOD_PUT | XHTTP_METHOD_PATCH, Handle_API_Item_Update);
    AddDynamicRouteHTTP("/api/v1/items/{id}",
        XHTTP_METHOD_DELETE,                 Handle_API_Item_Delete);

    /* in-segment capture works too: /api/v1/file/{name}.txt */
    AddDynamicRouteHTTP("/api/v1/file/{name}.txt",
        XHTTP_METHOD_GET, Handle_API_File);

    /* dynamic routes must be compiled once after all registration */
    (void)RouteHTTP_Compile();
}

What just happened: the static table hits in O(1); the dynamic table is batch-compiled inside RouteHTTP_Compile via xrtPatternCompileMany into an immutable matcher — afterwards matching cost depends only on path length, never on route count. The handler signature is the demo convention: (XS_HttpReq*, const RouteParamHTTP* arrParam, uint32 iParamCount), with arrParam carrying the captured {id} views.

17.3 Method slots & 405

  • Each (URI, method) pair is a slot — one URI with several methods registers separately; a duplicate URI+method overwrites the old callback with a warning
  • Method masks combine freely: PUT | PATCH share a slot; XHTTP_METHOD_CRUD (five methods) and XHTTP_METHOD_ANY (incl. extensions) are aggregate macros (the enums reference)
  • URI hit but method miss → 405 + Allow header: Allow aggregates every method registered on that URI — the origin of chapter 2's HEAD /json → 405, Allow: GET
Want the GET slot to take HEAD too: register with GET | HEAD — your ReplyRaw already handles HEAD not sending a body uniformly (chapter 15), so most handlers support HEAD correctly with zero changes.

17.4 Match order & performance

request → ① static table (xrtMap exact hit → method slot) miss → ② dynamic patterns (match → method slot → captured params) miss → XS_FALLBACK → static layer (wwwroot) / 404

Performance numbers (routebench, 3 rounds × 10,000 requests averaged):

Route countXServer latencyComparison
1042.0 µsGo 42.2
10,00059.1 µs (+41%)PHP +492% / FastAPI +935%

The secret of flatness: the static table is an O(1) hash; the dynamic table, after batch compilation, matches in O(path length) — ten thousand routes cost only 41% more than ten, while the comparison stacks degrade by an order of magnitude.

17.5 Contract

  • Compile: dynamic routes must Compile once after registration; the table is immutable afterwards (rebuilding on swap is hot reload's job).
  • Order: static → dynamic → FALLBACK; for the same URI, static beats dynamic.
  • Params: arrParam is a per-request view — use it up inside the callback.

Common mistakes

Forgetting RouteHTTP_Compile
Symptom
All dynamic routes 404 / fall into the static layer; static routes work fine.
Cause
The dynamic table becomes a matchable immutable pattern only after Compile — registration merely collects. The demo puts Compile at the end of RouteHTTP_Init with a "must" comment.
Dynamic routes growing ever wider
Symptom
Catch-alls like "/api/{a}/{b}/{c}" steal each other's traffic and hit the wrong handler.
Cause
Patterns match exact segments (not prefixes/regex); but stuffing many resources into one pattern drifts the semantics. Different resources get different patterns.

Exercises

BasicThe REST seven slots

Implement full REST for /api/v1/users and /api/v1/users/{id} (GET list / POST create / GET item / PUT+PATCH update / DELETE remove + HEAD list).

Acceptance: curl -I on the list returns 200 (HEAD works); DELETE on the list returns 405 with Allow listing five methods.
AdvancedAuditing 405 semantics

Watch several "URI hit, method miss" requests with -v; verify the Allow header matches your registrations exactly.

Acceptance: the Allow set equals the union of that URI's registered methods — nothing extra, nothing missing.
ChallengeA thousand-route generator

Script-generate 1000 static + 1000 dynamic routes (see tools/routebench's generator); measure compile time at startup and request latency.

Acceptance: compile < 100ms; latency growth vs 20 routes < 50%.

Summary

PointConclusion
Two tablesxrtMap static O(1) + xrtPattern dynamic, flat after compilation
Method slotsone slot per (URI × method); combinable masks; CRUD/ANY aggregates
405URI hit, method miss → 405 + Allow (union of registered methods)
Orderstatic → dynamic → FALLBACK → static layer / 404
Measured10k routes 59.1µs (+41%); comparison stacks degrade an order of magnitude
API references in this chapter
xrt further reading