A handler is told the address it was asked at

The address an op answers at is a property of the CALL, not of the declaration:
a group's prefix composes at build, and a definition composed under two hosts
has two addresses and one handler. So a handler could not learn its own address,
and a service that needed one composed it in a wrapper and threaded it in —
which made every one of those registrations a call this pass cannot see, and
took a whole service's operations out of the document, the tool list and the
SDKs.

OpOf reads it from the context instead, so the declaration stays the bound
method the projections resolve. The seam that knows the occurrence states it:
the REST route that matched, and the by-name seams, which looked the op up in
the composed registry. Under that, the one contract every projection funnels
through states the declaration, so a handler always has an operation to read and
a direct call says there is none rather than guessing.

Costs two allocations on a typed REST request, measured: 61 to 63 on
Benchmark_TypedRoute, with bytes unchanged.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-09-17 08:57:42 -07:00
co-authored by Hanzo Dev
parent cb7c5fb64c
commit d1ac3e430e
8 changed files with 218 additions and 6 deletions
+1 -1
View File
@@ -155,7 +155,7 @@ func (a *App) installCallPlane() {
// through a text format on the way. JSON is the BOUNDARY encoding — it
// belongs on the REST routes a browser reaches and in the MCP envelope an
// agent reads, and has no place inside the binary protocol.
out, err := op.invoke(callerContext(fc), zapenc.Unmarshal, fc.Body(), nil, nil, func(k string) string { return fc.Get(k) })
out, err := op.invoke(withOp(callerContext(fc), servedOp(op)), zapenc.Unmarshal, fc.Body(), nil, nil, func(k string) string { return fc.Get(k) })
if err != nil {
return sendCallError(fc, err)
}
+1 -1
View File
@@ -434,7 +434,7 @@ func LocalInvoke(ctx context.Context, c Command, path map[string]string, body []
// request to read headers from. A declared header field is simply supplied
// as an argument here — which is why the honest answer on a transport with
// no request is "nothing", not a panic.
return c.op.invoke(ctx, jsonenc.Unmarshal, body, nil, path, nil)
return c.op.invoke(withOp(ctx, servedOp(c.op)), jsonenc.Unmarshal, body, nil, path, nil)
}
// Remote executes a command against a running zip service, and reads that
+10 -2
View File
@@ -315,12 +315,12 @@ func TestExtract_BothTablesLiftTheSameDocs(t *testing.T) {
// serve.
func TestExtract_NestedGroupComposes(t *testing.T) {
p := load(t, "nested")
if len(p.Ops) != 3 {
if len(p.Ops) != 4 {
var keys []string
for _, o := range p.Ops {
keys = append(keys, o.Key())
}
t.Fatalf("ops = %d, want 3; got %v", len(p.Ops), keys)
t.Fatalf("ops = %d, want 4; got %v", len(p.Ops), keys)
}
chained := opByKey(t, p, "POST /v1/accounts/open")
if !strings.Contains(chained.Description, "account for the caller's org") {
@@ -331,4 +331,12 @@ func TestExtract_NestedGroupComposes(t *testing.T) {
if !strings.Contains(nested.Description, "what the account holds") {
t.Errorf("nested description = %q", nested.Description)
}
// A handler that reads its own address through zip.OpOf is registered as a
// bound method, so it is documented. Handing it an address from a wrapper
// instead is what hid one service's whole surface from this pass.
watch := opByKey(t, p, "GET /v1/accounts/:id/watch")
if !strings.Contains(watch.Description, "activity as it happens") {
t.Errorf("watch description = %q; reading the address does not hide the declaration", watch.Description)
}
}
+17
View File
@@ -50,6 +50,18 @@ func (a *API) Balance(ctx context.Context, in *struct{}) (*Ledger, error) {
return &Ledger{}, nil
}
// Watch streams the account's activity as it happens.
//
// It reads the address it was asked at rather than being handed one by a
// wrapper, which is what keeps the declaration below a bound method this pass
// can see.
func (a *API) Watch(ctx context.Context, in *struct{}) (*Ledger, error) {
if op, ok := zip.OpOf(ctx); ok {
_ = op.Path
}
return &Ledger{}, nil
}
// Register declares the accounts surface.
func Register(app *zip.App, api *API) {
accounts := app.Group("/v1/accounts").Tag("accounts")
@@ -61,4 +73,9 @@ func Register(app *zip.App, api *API) {
// A nested group composes its prefix.
accounts.Group("/:id").Get("/ledger", api.Balance)
// A handler that reads its own address is still a bound method here, so it
// is documented like any other. Composing the address in a helper instead is
// what made a whole service's operations invisible.
accounts.Get("/:id/watch", api.Watch)
}
+1 -1
View File
@@ -729,7 +729,7 @@ func (a *App) tool(ctx context.Context, f *zapmcp.Frame) *zapmcp.Frame {
// object, so the body IS the whole input — neither query nor path binds.
// The header reader comes off the ctx, so it answers honestly on a transport
// that has a request behind it and nothing on one that does not.
out, err := op.invoke(ctx, jsonenc.Unmarshal, params.Arguments, nil, nil, headerOf(ctx))
out, err := op.invoke(withOp(ctx, servedOp(op)), jsonenc.Unmarshal, params.Arguments, nil, nil, headerOf(ctx))
return a.answer(f, out, err)
}
+63
View File
@@ -0,0 +1,63 @@
package zip
import "context"
// opKey carries the operation being served. Its own type, so nothing else can
// collide with it or read it by guessing a string.
type opKey struct{}
// OpOf is the operation whose handler is running, and whether there is one.
//
// It answers the question a handler cannot answer for itself: WHERE it is being
// asked from. The path is the address the op is served at — a group's prefix
// composed with the leaf, resolved for the occurrence answering this call, which
// is not knowable at registration. A definition composed under two hosts has two
// addresses and one handler, so the address is a property of the CALL.
//
// That is why this is a context value rather than something a group could hand
// out at declaration: a prefix read at registration is the local join, and a
// consumer that built an address from it published one that nothing served the
// moment its definition was composed twice.
//
// There is no operation exactly when a handler is called directly rather than
// through the declaration that registered it — in a unit test, say — which is
// worth naming rather than papering over with a guessed address.
//
// func (a *API) Livetail(ctx context.Context, in *Tail) (*Stream, error) {
// op, ok := zip.OpOf(ctx)
// if !ok {
// return nil, fmt.Errorf("livetail: called outside its registration")
// }
// return a.relay(ctx, op.Path, in)
// }
func OpOf(ctx context.Context) (Op, bool) {
op, ok := ctx.Value(opKey{}).(Op)
return op, ok
}
// withOp states the operation being served. The seam that KNOWS the occurrence
// calls it — the REST route, which matched, and the by-name seams, which looked
// the op up in the composed registry — so the address in the context is the
// resolved one.
func withOp(ctx context.Context, op Op) context.Context {
return context.WithValue(ctx, opKey{}, op)
}
// declaredOp states the operation as DECLARED, and only if no seam has stated a
// resolved one. It is the floor under [OpOf]: every projection funnels through
// the one contract, so a handler always has an operation to read, and a seam
// that can do better than the declaration overrides this by arriving first.
func declaredOp(ctx context.Context, op Op) context.Context {
if _, ok := ctx.Value(opKey{}).(Op); ok {
return ctx
}
return withOp(ctx, op)
}
// servedOp is the identity of an op taken from the COMPOSED registry, where the
// path is already the address it answers at. The by-name seams — an MCP
// tools/call, the call plane, the in-process CLI — resolve an op by looking it
// up there, so what they hold is the occurrence, and this states it.
func servedOp(op *registeredOp) Op {
return Op{Method: op.Method, Path: op.Path, OperationID: opName(op)}
}
+114
View File
@@ -0,0 +1,114 @@
package zip_test
import (
"context"
"encoding/json"
"net/http/httptest"
"sort"
"testing"
"github.com/zap-proto/zip"
)
type tailIn struct {
Follow bool `json:"follow"`
}
// where is the address the handler was asked at.
type where struct {
Address string `json:"address"`
ID string `json:"id"`
}
// tail is the shape a service takes when it forwards what it was asked for: a
// BOUND METHOD registered directly, reading its own address from the context.
type tail struct{}
// Livetail streams matching records as they arrive.
func (tail) Livetail(ctx context.Context, _ *tailIn) (*where, error) {
op, ok := zip.OpOf(ctx)
if !ok {
return &where{Address: "", ID: "no operation"}, nil
}
return &where{Address: op.Path, ID: op.OperationID}, nil
}
func addressAt(t *testing.T, app *zip.App, path string) where {
t.Helper()
resp, err := app.Test(httptest.NewRequest("GET", path, nil))
if err != nil {
t.Fatalf("%s: %v", path, err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != 200 {
t.Fatalf("%s: status %d", path, resp.StatusCode)
}
var w where
if err := json.NewDecoder(resp.Body).Decode(&w); err != nil {
t.Fatalf("%s: %v", path, err)
}
return w
}
// A HANDLER IS TOLD THE ADDRESS IT WAS ASKED AT, and the registration stays the
// bound method — no closure, no factory, nothing between the source and the
// projections that read it.
//
// This is what a registration-time wrapper was for: the address was composed by
// hand and threaded in, which made every such op invisible to cmd/zipdoc,
// because a registration made inside a helper is not a registration this pass
// can see. Reading it from the context leaves the declaration direct.
func TestOpOf_IsTheAddressTheHandlerWasAskedAt(t *testing.T) {
app := zip.New(zip.Config{AppName: "o11y", DisableStartupMessage: true})
app.Group("/v1/o11y").Get("/logs/livetail", tail{}.Livetail)
if got := addressAt(t, app, "/v1/o11y/logs/livetail").Address; got != "/v1/o11y/logs/livetail" {
t.Errorf("address = %q, want the composed path", got)
}
// And the op is in the registry under that address, so what the handler reads
// and what the document publishes are the same string.
var addrs []string
for _, op := range app.Manifest().Ops {
addrs = append(addrs, op.Method+" "+op.Path)
}
sort.Strings(addrs)
if len(addrs) != 1 || addrs[0] != "GET /v1/o11y/logs/livetail" {
t.Errorf("registry = %v, want the one op at the address the handler reads", addrs)
}
}
// ONE HANDLER, TWO ADDRESSES. A definition composed under two hosts answers at
// both, and each call is told where IT was asked — which is the whole reason
// this is a property of the call and not something a group could hand out when
// it was declared.
func TestOpOf_ADefinitionComposedTwiceAnswersAtBoth(t *testing.T) {
child := zip.New(zip.Config{AppName: "tail", DisableStartupMessage: true})
child.Get("/livetail", tail{}.Livetail)
host := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
host.Group("/east").Use(child)
host.Group("/west").Use(child)
if err := host.Build(); err != nil {
t.Fatalf("Build: %v", err)
}
if got := addressAt(t, host, "/east/livetail").Address; got != "/east/livetail" {
t.Errorf("east address = %q", got)
}
if got := addressAt(t, host, "/west/livetail").Address; got != "/west/livetail" {
t.Errorf("west address = %q — one handler, and each call is told its own", got)
}
}
// Called directly rather than through the declaration that registered it, there
// is no operation, and saying so beats guessing an address.
func TestOpOf_SaysWhenThereIsNoOperation(t *testing.T) {
w, err := tail{}.Livetail(context.Background(), &tailIn{})
if err != nil {
t.Fatal(err)
}
if w.Address != "" {
t.Errorf("address = %q, want empty outside a registration", w.Address)
}
}
+11 -1
View File
@@ -646,6 +646,11 @@ func registerTyped[In, Out any](depth int, on *App, method, path string, fn Type
// here, so no way of reaching this op can skip a check another way makes. A
// nil *Out becomes a nil `any`.
run := func(ctx context.Context, in *In) (out any, err error) {
// The operation, readable by the handler through [OpOf]. Stated here
// because this is the one contract every projection funnels through, and
// only as the DECLARATION: a seam that matched an occurrence has already
// stated the resolved address and keeps it.
ctx = declaredOp(ctx, meta)
// TOLD ONCE, ON EVERY WAY OUT. This is the one contract every projection
// funnels through — REST, an MCP tools/call, the call plane, the graph and
// an in-process invoke — so a hook here is told about all of them, and a
@@ -772,7 +777,12 @@ func registerTyped[In, Out any](depth int, on *App, method, path string, fn Type
if op.readsHeaders {
header = func(k string) string { return c.Get(k) }
}
out, err := op.invoke(callerContext(c), jsonenc.Unmarshal, body, c.Queries(), path, header)
// THE MATCHED ROUTE IS THE ADDRESS. materialise mounts each occurrence at
// its absolute path, so the pattern fiber matched is where this op is
// served — including the prefix of whichever host composed it, which the
// registration-time identity cannot name.
served := Op{Method: meta.Method, Path: c.Route().Path, OperationID: meta.OperationID}
out, err := op.invoke(withOp(callerContext(c), served), jsonenc.Unmarshal, body, c.Queries(), path, header)
if err != nil {
return err
}