App, Group, Operation. A group is a prefix and what holds under it, an operation
is one typed route declared there, and the verb methods are generic methods —
which Go 1.27 allows on a concrete type and still refuses on an interface. So In
and Out come from the handler and the caller writes no type arguments:
users := app.Group("/v1/users")
users.Post("/", api.Create)
mint := users.Group("/mint").With(middleware.Mint(gate))
mint.Post("/deposit", api.Deposit)
A Group is a CLOSED HANDLE: two unexported fields, and no method that hands back
the app, the parent or the fiber underneath. A subsystem given the group at its
own prefix cannot reach above it, so confinement is a property of the type
instead of a rule something has to police — which is what a host needs in order
to hand out a bounded surface and know what comes back.
That is bought by giving up the router interface, deliberately. Router and
OpTarget are gone, and with them the ability to implement a route table outside
this package: a decorator is no longer a router, a gate is middleware on a group,
and a bound is a path. The five package-level verb functions go the same way —
a registration is a method on the thing it registers into.
Raw carries what is not a typed operation: a stream, an upgrade, a probe, a
foreign subtree. It names its method, and it is the longer spelling because
opting out of the typed model is the exception and should read like one.
Co-authored-by: Hanzo Dev <[email protected]>
The HTTP transport builds a bare fasthttp.Server, so Config travels to the wire
only through applyConfig. It carried ReadBufferSize, WriteBufferSize and
Concurrency and left BodyLimit behind: fiberConfig sets fiber's own BodyLimit,
which fiber pushes onto MaxRequestBodySize only when fiber owns the listener,
and here the transport does. The socket kept fasthttp's 4 MiB default whatever
the App declared.
Measured on a deployment configured for 100 MiB: 4,194,304 bytes answered,
4,194,305 answered 400 "Error when parsing request" — fasthttp refusing before
any handler runs, in a voice that reads as a malformed payload rather than a
size cap. That capped every request body on the edge, so a site publish over
4 MiB and a full-context prompt (~4.3 MB of JSON for 1M tokens) failed alike.
The regression test drives a real socket in both directions, because the
assertion that passed throughout was a config-level one: the value was set
correctly and never left the struct.
zap-proto/http is package http now (v0.3.5) — the last element of its own module
path, which is the name Go resolves an unaliased import to. My v1.27.4 called it
zap, and that was wrong twice: it did not match the module path, and this package
imports zap, the wire format, so one word meant two things in the same file.
No file here imports net/http, so the transport needs no alias at all.
Co-authored-by: Hanzo Dev <[email protected]>
zap-proto/http renamed its package (v0.3.3): the module path already says http,
so zaphttp said it twice and built a compound word into every file that touched
the wire. This module imports it under its own name now.
Co-authored-by: Hanzo Dev <[email protected]>
A fleet view needs one thing this package did not expose: the ability to ask a
live process what it loaded, at which version, and whether it is still up.
Deployment config answers what was INTENDED; only the process knows what is
TRUE — which is exactly the difference that matters mid-rollout, when the two
disagree by design.
for _, p := range app.Plugins() {
// Name, Prefix, Source, Version, Addr, PID, Running, Since, Reloads
}
Version is the artifact's SHA-256 for a URL-installed plugin — the only version
identifier that cannot drift from the bits running, because it IS the bits.
Since resets on Reload, so it reports the age of what is running rather than of
the mount, and Reloads climbing on one host and not its peers is the signal that
a rollout is uneven. An Unloaded plugin stays listed with Running=false, because
"deployed but down" and "not deployed" are different states and its routes are
still registered answering 503.
Remote (Addr) mounts are recorded too. They took an early return and were
invisible before; a fleet view that silently omits part of the surface is worse
than none.
Two fixes found by having the tests exercise this:
Shutdown now waits for instances a Reload or Unload is still draining. Those
retire asynchronously so the caller is not blocked for the drain window, which
let a child outlive the host that spawned it — and since children inherit the
host's stdout, the process appeared to hang after main returned. The suite
showed it as "Test I/O incomplete 1m0s after exiting"; it now finishes in 22s
instead of 74s.
Tests no longer hardcode ports. A fixed 19653 meant an unrelated process on the
box made TestListen_ZAP fail with 404 rather than a connection error, because
the port answers — it is just answering for someone else. That reads as a
routing bug and is not one.
Co-authored-by: Hanzo Dev <[email protected]>
A colocated plugin wants a socket path, not a port: nothing to allocate,
nothing to collide, and filesystem permissions are the ACL. That needed no new
scheme. The scheme names the PROTOCOL (zap); the address already says where it
is spoken, so a leading "/" (or "./", or "@" for a Linux abstract socket) is a
unix socket and anything else is host:port.
app.Listen("/run/hanzo/billing.sock") // in the plugin
app.Mount("/v1/billing", "/run/hanzo/billing.sock") // in the host
A "zapuds" scheme was the obvious move and the wrong one: it is a compound name
for the same wire, and it would have split one protocol across two registry
entries that then drift. Reading the network off the address keeps it at one.
Both directions gained unix from a single registry change, which is the point
of Serve and Dial living in one Transport value.
Requires zap-proto/http v0.2.2 (network as a value).
Co-authored-by: Hanzo Dev <[email protected]>
The HTTP transport built a bare fasthttp.Server and applyConfig never set its
Name, so fasthttp stamped its default 'Server: fasthttp' on the responses it
writes BEFORE the fiber handler runs — 431 (header overflow), 400 (parse error),
timeouts. Those bytes are unreachable by ProductionHeaders (or any middleware),
so the framework name leaked on exactly the malformed-request path an attacker
probes, invisible to the in-memory (Fiber().Test) suite.
applyConfig now propagates ServerHeader onto the transport server (Name, or
NoDefaultServerHeader when '-'), so pre-routing responses carry the same value
as handled ones. Proven on a REAL socket: a 9 KiB-header 431 now reads
Server: <brand>, never fasthttp/zip; '-' emits none. Also corrects the Ctx.Host()
doc to state X-Forwarded-Host is ignored (zip has no TrustProxy), so the Server
brand cannot be spoofed from a request header.
zip's built-in HTTP transport constructed a bare fasthttp.Server{Handler: h}
and never propagated the App's Config — so zip.Config.ReadBufferSize was
silently dropped at the wire. fasthttp then capped request headers at its
4 KiB default and returned 431 (Request Header Fields Too Large) once a
browser's multi-domain SSO cookies crossed ~4 KiB. Only fiber's own listener
(app.Fiber().Listener) honored the config; app.Listen("http://...") did not.
Push the App's per-conn tuning (ReadBufferSize / WriteBufferSize / Concurrency)
onto the transport's fasthttp.Server after construction via a tunableServer
seam. Only non-zero knobs are applied, so unset fields keep fasthttp defaults
(no behavior change for callers that don't set them) and internal high-
concurrency services keep the 4 KiB budget.
Test TestHTTPTransport_ReadBufferSize_Raises431Ceiling: default 431s a 9 KiB
header; ReadBufferSize:32768 serves it 200.
Decomplected the transport layer and added a free MCP tool surface — both from
the principle 'values, not places; one and only one way'.
TRANSPORT — one verb, transport is a VALUE (the address scheme), not a method:
app.Listen(":9653") // ZAP (bare addr = the primary)
app.Listen(":9653", "http://:8080") // ZAP + HTTP in one call
app.Listen("quic://:443") // any RegisterTransport'd protocol
Mirrors net.Listen(network, addr). Removed ListenHTTP/ListenZAP/Serve — one Listen.
An open transport registry (RegisterTransport) means any future termination/
serialization proto slots in with ZERO API change. ZAP is DefaultScheme.
FREE MCP — every typed handler (Get/Post[In,Out]) auto-projects to a Model
Context Protocol tool at /mcp (JSON-RPC 2.0: initialize/tools/list/tools/call).
The SAME op registry feeds REST routes, the OpenAPI doc, AND the MCP tools — one
value (the op), three projections; tools/call runs the exact same fn as the REST
route (op.invoke, the transport-agnostic handler core). Because /mcp is an
ordinary route, it's served over every transport → ZAP-native MCP for free. On by
default; Config.MCP.Disabled to suppress.
Supersedes the same-session v1.0.0 transitional ListenZAP/HTTP/Serve API — forward
only, no compat shims (per repo policy: break within v1, never lazily bump major).
Verified: build/vet/gofmt clean; TestListen_ZAP + TestListen_DualTransport serve a
real request over ZAP (and both transports from one Listen); TestMCP_FreeToolSurface
drives initialize/tools/list/tools/call end-to-end (schemaOf projection + same-fn
call + validation→isError).