From a600efbe3f3d2c29e3aa6e7877937a29429b9a1e Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 23 Jul 2026 15:13:17 +0200 Subject: web: default lectio-web to port 1099 with free-port fallback chooseListener(port) factors the listen logic out of Run: port==0 now tries defaultWebPort (1099) first and only falls back to an OS-picked free port if 1099 is taken; a non-zero port is still bound exactly, surfacing its bind error as before. config.Default() keeps WebPort: 0 unchanged (0 still means "auto"). Adds TestChooseListener, robust to sandboxes that can't bind 1099 or :0 (t.Skip instead of failing). Also folds the ?ref= whitespace trim into renderLookup itself so GET / and GET /lookup treat a whitespace-only ref identically (previously only lookupHandler trimmed it). --- internal/web/server.go | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) (limited to 'internal/web/server.go') diff --git a/internal/web/server.go b/internal/web/server.go index d484b69..5886f0f 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -270,9 +270,12 @@ type lookupColumn struct { // applies to a section's own citation via render.GatherVersion, not a // free-typed lookup) against each requested version. Shared by lookupHandler // (the HTMX partial) and indexHandler (so a bookmarked/shared "/?ref=..." -// link shows the same result instead of an empty pane). An empty ref -// renders nothing, matching lookupHandler's previous no-op behavior. +// link shows the same result instead of an empty pane). ref is trimmed of +// surrounding whitespace here so both routes treat a whitespace-only ref +// identically; an empty (or now-empty) ref renders nothing, matching +// lookupHandler's previous no-op behavior. func renderLookup(ref string, versions []string) template.HTML { + ref = strings.TrimSpace(ref) if ref == "" { return "" } @@ -293,7 +296,7 @@ func renderLookup(ref string, versions []string) template.HTML { // its doc comment for the lookup semantics). func lookupHandler(cfg config.Config) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ref := strings.TrimSpace(r.URL.Query().Get("ref")) + ref := r.URL.Query().Get("ref") // renderLookup trims whitespace w.Header().Set("Content-Type", "text/html; charset=utf-8") io.WriteString(w, string(renderLookup(ref, requestVersions(cfg, r)))) } @@ -322,11 +325,27 @@ func themeCSSHandler(cfg config.Config) http.HandlerFunc { } } -// Run starts lectio-web: listens on cfg.WebPort (0 picks a free OS port), -// prints the URL, best-effort opens it in a browser, and serves until the -// listener errors. +// defaultWebPort is the port chooseListener prefers when cfg.WebPort is 0. +const defaultWebPort = 1099 + +// chooseListener binds the port to serve on. port==0 means "prefer +// defaultWebPort (1099), else let the OS pick a free port"; a non-zero port +// is bound exactly (and its bind error surfaced if the port is in use). +func chooseListener(port int) (net.Listener, error) { + if port != 0 { + return net.Listen("tcp", fmt.Sprintf(":%d", port)) + } + if ln, err := net.Listen("tcp", fmt.Sprintf(":%d", defaultWebPort)); err == nil { + return ln, nil + } + return net.Listen("tcp", ":0") // 1099 taken -> any free port +} + +// Run starts lectio-web: listens on cfg.WebPort via chooseListener (0 +// prefers 1099, falling back to a free OS port), prints the URL, best-effort +// opens it in a browser, and serves until the listener errors. func Run(cfg config.Config) error { - ln, err := net.Listen("tcp", fmt.Sprintf(":%d", cfg.WebPort)) + ln, err := chooseListener(cfg.WebPort) if err != nil { return err } -- cgit v1.3