A route may have its own listener: every request there is that route, paths unprefixed

Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
2026-09-25 19:59:21 -07:00
parent 35b07ece81
commit fa1c398fc4
10 changed files with 392 additions and 12 deletions
+39
View File
@@ -2,8 +2,10 @@ package config
import (
"fmt"
"net"
"regexp"
"sort"
"strconv"
"strings"
)
@@ -22,6 +24,7 @@ type Route struct {
Peers []string `toml:"peers"`
Affinity string `toml:"affinity"` // "" or "conversation" (the default), or "route"
Queue *bool `toml:"queue"` // nil means true
Listen string `toml:"listen"` // "" = none; else host:port of the route's own listener
}
// PerRoute reports affinity = "route": every request on the route (chat or control) shares one
@@ -80,6 +83,9 @@ func (c *Config) checkRoutes(peersDefined map[string]bool, identityDefined bool)
names = append(names, name)
}
sort.Strings(names)
// A route's own listener address, keyed for the uniqueness check: the value is the
// route that first claimed it, so the second route in sorted order reports the miss.
seenListen := make(map[string]string, len(c.Routes))
for _, name := range names {
r := c.Routes[name]
@@ -124,6 +130,39 @@ func (c *Config) checkRoutes(peersDefined map[string]bool, identityDefined bool)
if e := checkPeers(name, r.Peers, peersDefined[name], identityDefined, c.Identity); e != nil {
return e
}
if e := checkListen(name, r.Listen, c.Listen, seenListen); e != nil {
return e
}
}
return nil
}
// checkListen validates one route's own listener. The rules, in order, each naming the key
// routes.<name>.listen: the value must split into host and a numeric port 1-65535, it must not be on
// a template route, it must not be the top-level listen, and it must be unique across routes (the
// second route in sorted name order reports the clash and the other route's name).
func checkListen(name, listen, mainListen string, seenListen map[string]string) *Error {
if listen == "" {
return nil
}
_, port, err := net.SplitHostPort(listen)
if err != nil {
return &Error{Field: fmt.Sprintf("routes.%s.listen", name), Msg: "must be host:port"}
}
n, err := strconv.Atoi(port)
if err != nil || n < 1 || n > 65535 {
return &Error{Field: fmt.Sprintf("routes.%s.listen", name), Msg: "port must be 1-65535"}
}
if templateName.MatchString(name) {
return &Error{Field: fmt.Sprintf("routes.%s.listen", name), Msg: "a template route cannot have its own listener"}
}
if listen == mainListen {
return &Error{Field: fmt.Sprintf("routes.%s.listen", name), Msg: "cannot be the main listen address"}
}
if other, dup := seenListen[listen]; dup {
return &Error{Field: fmt.Sprintf("routes.%s.listen", name), Msg: fmt.Sprintf("already used by route %s", other)}
}
seenListen[listen] = name
return nil
}