A route says the largest body its proxy may carry, and both proxies honour it

The registry's public name is served by the predecessor with a twenty-gigabyte buffering
middleware, because a registry takes image layers in single requests of gigabytes and a
proxy's default turns every push into a 413 the registry never sees. A route contribution
had no way to say so, so the mesh could not take the name over without losing what made it
usable.

The contribution now carries `max-request-body`, a whole positive number of bytes, and the
catalogue holds every route to an agreed vocabulary — label or name, port, and the limit —
refusing a key no proxy reads (a field that parses cleanly and does nothing is a promise
nobody keeps) and a route with no port (unreachable by the proxy it just asked for, found at
parse time rather than in a proxy's log). The mesh's own proxy reads the limit as written,
refuses a body past it as 413 rather than the 502 the transport would have reported, and
skips a route whose limit it cannot read rather than carrying what the module said not to.

The registry's hand-over itself is read from the catalogue beside this checkout: the store
still resolves with no proxy, the gate beside it pulls the store in, contributes the
predecessor's name on the port the node gave it, and locks only the door that faces the
world.

hq ADR 0082/0104, the registry hand-over.
This commit is contained in:
2026-09-23 23:19:12 +02:00
parent 8fb32d7ee0
commit 01d57b629f
6 changed files with 695 additions and 28 deletions
+99 -11
View File
@@ -1,7 +1,9 @@
package main
import (
"bytes"
"context"
"io"
"net/http"
"net/http/httptest"
"os"
@@ -30,7 +32,7 @@ func TestARouteGoesToWhereTheMeshSaysTheConsumerIs(t *testing.T) {
}
// Lower-cased, because a Host header is not case-sensitive and a route that only answers the
// spelling in the manifest answers half the requests made to it.
if routes["app.example"] != "http://laptop.internal:8080" {
if routes["app.example"].Target != "http://laptop.internal:8080" {
t.Fatalf("the route does not point at the consumer: %v", routes)
}
}
@@ -44,7 +46,7 @@ func TestAConsumerOnTheProxysOwnMachineIsReachedOverLoopback(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if routes["app.example"] != "http://127.0.0.1:9000" {
if routes["app.example"].Target != "http://127.0.0.1:9000" {
t.Fatalf("a workload on this machine was not reachable: %v", routes)
}
}
@@ -59,7 +61,7 @@ func TestAContributionMissingWhatARouteNeedsIsSkipped(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if len(routes) != 1 || routes["fine.example"] == "" {
if len(routes) != 1 || routes["fine.example"].Target == "" {
t.Fatalf("an unusable contribution was served: %v", routes)
}
}
@@ -75,7 +77,7 @@ func TestTheProxyReachesTheWorkloadAndNamesWhatItServes(t *testing.T) {
host, port, _ := strings.Cut(target, ":")
held := newTable()
held.set(map[string]string{"app.example": "http://" + host + ":" + port})
held.set(map[string]route{"app.example": {Target: "http://" + host + ":" + port}})
proxy := httptest.NewServer(handler(held))
defer proxy.Close()
@@ -120,11 +122,11 @@ func TestTheProxyReachesTheWorkloadAndNamesWhatItServes(t *testing.T) {
// nothing fails more visibly than a stale grant, which is exactly why it must not survive.
func TestWithdrawingARouteStopsServingIt(t *testing.T) {
held := newTable()
held.set(map[string]string{
"going.example": "http://a.internal:80",
"staying.example": "http://b.internal:80",
held.set(map[string]route{
"going.example": {Target: "http://a.internal:80"},
"staying.example": {Target: "http://b.internal:80"},
})
held.set(map[string]string{"staying.example": "http://b.internal:80"})
held.set(map[string]route{"staying.example": {Target: "http://b.internal:80"}})
if _, still := held.find("going.example"); still {
t.Fatal("a route whose module was unassigned is still served")
@@ -137,7 +139,7 @@ func TestWithdrawingARouteStopsServingIt(t *testing.T) {
// A Host header carries a port and the name does not.
func TestARequestNamingAPortStillFindsItsRoute(t *testing.T) {
held := newTable()
held.set(map[string]string{"app.example": "http://a.internal:8080"})
held.set(map[string]route{"app.example": {Target: "http://a.internal:8080"}})
if _, found := held.find("app.example:8080"); !found {
t.Fatal("a request to app.example:8080 did not find the route for app.example")
}
@@ -168,7 +170,7 @@ func TestTheIssuerIsStagingUnlessNamed(t *testing.T) {
// rate limit — and the proxy would look healthy throughout.
func TestNoCertificateIsAskedForOnAnUnroutedName(t *testing.T) {
held := newTable()
held.set(map[string]string{"photos.example": "http://127.0.0.1:8080"})
held.set(map[string]route{"photos.example": {Target: "http://127.0.0.1:8080"}})
policy := onlyWhatTheMeshSaid(held)
if err := policy(context.Background(), "photos.example"); err != nil {
@@ -184,7 +186,7 @@ func TestNoCertificateIsAskedForOnAnUnroutedName(t *testing.T) {
// A route withdrawn stops being certifiable, without the proxy restarting.
func TestWithdrawingARouteWithdrawsItsCertificate(t *testing.T) {
held := newTable()
held.set(map[string]string{"photos.example": "http://127.0.0.1:8080"})
held.set(map[string]route{"photos.example": {Target: "http://127.0.0.1:8080"}})
policy := onlyWhatTheMeshSaid(held)
if err := policy(context.Background(), "photos.example"); err != nil {
t.Fatal(err)
@@ -196,3 +198,89 @@ func TestWithdrawingARouteWithdrawsItsCertificate(t *testing.T) {
"once rather than what is served now")
}
}
// The registry's hand-over (novox/hq ADR 0082, ADR 0104): a registry takes image layers in single
// requests of gigabytes, and its public name was served by the predecessor with a twenty-gigabyte
// body limit. The contribution now says so, and this proxy reads it as written — and a limit it
// cannot read is a route it does not serve, like a port that is not one.
func TestABodyLimitIsReadFromTheContributionOrTheRouteIsSkipped(t *testing.T) {
routes, err := routesFrom(write(t, `{"given":[
{"from":"gate","node":"anchor","values":{"name":"registry-api.example","port":5001,"max-request-body":21474836480}},
{"from":"app","node":"anchor","values":{"name":"app.example","port":8080}},
{"from":"odd","node":"anchor","values":{"name":"odd.example","port":8081,"max-request-body":"20g"}},
{"from":"none","node":"anchor","values":{"name":"none.example","port":8082,"max-request-body":0}}
]}`))
if err != nil {
t.Fatal(err)
}
if got := routes["registry-api.example"].MaxRequestBody; got != 21474836480 {
t.Errorf("the limit did not arrive as written: %d", got)
}
if got := routes["app.example"].MaxRequestBody; got != 0 {
t.Errorf("a route that asked for no limit was given one: %d", got)
}
for _, skipped := range []string{"odd.example", "none.example"} {
if _, served := routes[skipped]; served {
t.Errorf("%s asked for a limit that is not a number of bytes and was served anyway", skipped)
}
}
}
// A body past the route's limit is refused as too large — whether its length is declared up front
// or only discovered while it is read — and a body within it reaches the workload whole. Refused
// as 413, not 502: a push that is too large must be told so, not told the registry is down.
func TestABodyPastTheRoutesLimitIsRefusedAsTooLarge(t *testing.T) {
var received int64
workload := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n, _ := io.Copy(io.Discard, r.Body)
received = n
w.WriteHeader(http.StatusCreated)
}))
defer workload.Close()
held := newTable()
held.set(map[string]route{
"limited.example": {Target: workload.URL, MaxRequestBody: 1024},
"unlimited.example": {Target: workload.URL},
})
proxy := httptest.NewServer(handler(held))
defer proxy.Close()
push := func(host string, body []byte, declared bool) int {
t.Helper()
var reader io.Reader = bytes.NewReader(body)
if !declared {
// A reader that is not a bytes.Reader carries no length: the request goes out chunked
// and the proxy learns the size only by reading it.
reader = io.MultiReader(bytes.NewReader(body))
}
asked, err := http.NewRequest(http.MethodPut, proxy.URL+"/v2/blob", reader)
if err != nil {
t.Fatal(err)
}
asked.Host = host
answer, err := http.DefaultClient.Do(asked)
if err != nil {
t.Fatal(err)
}
defer answer.Body.Close()
_, _ = io.Copy(io.Discard, answer.Body)
return answer.StatusCode
}
small, large := bytes.Repeat([]byte("x"), 1000), bytes.Repeat([]byte("y"), 4096)
if got := push("limited.example", small, true); got != http.StatusCreated || received != 1000 {
t.Fatalf("a body within the limit got %d and %d bytes arrived", got, received)
}
if got := push("limited.example", large, true); got != http.StatusRequestEntityTooLarge {
t.Fatalf("a declared body past the limit got %d, not 413", got)
}
if got := push("limited.example", large, false); got != http.StatusRequestEntityTooLarge {
t.Fatalf("an undeclared body past the limit got %d, not 413", got)
}
// And a route that asked for no limit carries whatever it is given.
if got := push("unlimited.example", large, true); got != http.StatusCreated || received != 4096 {
t.Fatalf("a route with no limit refused or truncated a body: %d, %d bytes", got, received)
}
}