Chapter 9 / 51
Volume 2 · Configuration & Assembly

The Custom knob system: an open world for non-preset fields

Where do xs.json fields the host doesn't preset go? — Untouched into Custom, for the application to read. This "open world" also hosts the http driver's request limits.

Overview

Positioning: Custom is the other half of XServer's config system — preset fields say "how the host assembles", Custom says "what the application wants". Afterwards you can add your own knobs to the config and read them safely from scripts.

9.1 Intro

Every config system eventually hits this: the framework's preset fields aren't enough. The common fix is to keep adding fields — config bloat and semantic drift follow. XServer's answer is to draw a clear line: preset fields describe host assembly facts (ports, protocols, certificates); every other field is never popped, never validated, kept verbatim — at runtime they hang as xvalue trees on server->Custom, host->Custom and xsConfigRoot(), and the application decides their meaning and validation.

9.2 The Custom read model

Three read entry points
/* 1. Root-level Custom: xsConfigRoot returns a retained xvalue; Release it */
xvalue* pRoot = xsConfigRoot();
xvalue* pMy = xrtValueObjectGet(pRoot, "my_app");
xrtValueRelease(pRoot);

/* 2/3. server / host level: view fields hung directly on the info structs (borrowed) */
xvalue* pSrvCustom = pReq->server->Custom;
xvalue* pHostCustom = pReq->host->Custom;

What just happened: the root level needs a Release (you own the reference); server/host Custom are view fields — safe to read inside callbacks, stored only under chapter 3's ownership rules.

The strict parsing helper

The demo's stream module provides XS_CustomReadUInt: it reads a strictly non-negative integer from the Custom tree — a wrong type or a negative value fails instead of guessing a default. Read your knobs with it; avoid lenient conversions.

9.3 The six request-limit knobs

The most important "tenants" of the Custom open world are the request limits consumed by the http/ws drivers (written at the server level; applications may read them too):

KnobDefaultOn exceed
header_limit—431 Request Header Fields Too Large
path_limit2048414 URI Too Long
body_limit—413 Content Too Large
idle_timeout—silently closes the connection (http/ws/tcp alike)
ws_message_limit—close code 1009
ws_protocol—handshake 400 (required subprotocol mismatch)

Why are these Custom and not preset fields? Because they describe "this server's attitude toward requests" — deployment policy rather than assembly facts; and type validation happens at the consumer (e.g. ws_protocol is validated at startup), preserving "whoever consumes validates".

9.4 Contract

  • Ownership: xsConfigRoot is retained (pair with Release); server/host Custom are borrowed views.
  • Validation: the host never validates Custom fields — the application must parse strictly (the XS_CustomReadUInt pattern) and refuse lenient conversions.
  • Naming: use application-prefixed keys (my_app.*) to avoid colliding with the http driver's conventional knobs (header_limit etc.).

Common mistakes

Forgetting the xsConfigRoot Release
Symptom
Every script generation leaks one xvalue reference; the root object count climbs in memory reports.
Cause
The documented contract: the return is retained and must be xrtValueRelease'd — a textbook application of chapter 3's ownership rules.
Lenient parsing turns a typo'd knob into 0
Symptom
The config says "body_limt" (typo); the application gets the default 0 and misbehaves silently.
Cause
A lenient "missing means zero" read. Custom has no host validation — only strict application-side reads expose typos.
✗ Wrong
uint32 n = 0;
GetValue(pCustom, "body_limit", &n);
/* missing silently becomes 0 */
✓ Right
/* strict read: missing is a config error */

Exercises

BasicRead a knob of your own

Add "my_app": {"greeting": "hi"} at the xs.json root; read and print it in the script's ServiceInit.

Acceptance: the log prints hi; the xrtValueRelease pairing is correct.
AdvancedObserve the limit states

Set body_limit=100 on the demo, send 99- and 101-byte POSTs and record the status difference; then set path_limit=50 and send a long path.

Acceptance: the 413 and 414 trigger boundaries match the configured values exactly.
ChallengeCodify the knob pattern

Write modules/knobs.h: declaratively define application knobs (key / type / default / required), validate all of them strictly at Init and report every missing key at once.

Acceptance: with two keys deliberately missing, one error message lists both names.

Summary

PointConclusion
Custom definedNon-preset fields kept verbatim; the host never pops or validates them
Read entry pointsxsConfigRoot (retained) / server->Custom / host->Custom (borrowed)
Request limitsheader/path/body_limit + idle_timeout + two ws knobs; exceed → 431/414/413/close/1009
Reading disciplineStrict parsing (the XS_CustomReadUInt pattern); application-prefixed keys
xrt further reading