security: hide router dispatch errors

Jaime Fournier <jaimef@linbsd.org>

6f6fb649bd28bc1003dfb70629bb2bc1d54697ae

diff --git a/docs/kimi3-security-recommmendations.md b/docs/kimi3-security-recommmendations.md
index 0ffee14..0ab10a4 100644
--- a/docs/kimi3-security-recommmendations.md
+++ b/docs/kimi3-security-recommmendations.md
@@ -791,8 +791,9 @@ they are build-time. Close the runtime loop.
   unexpected tool-handler exceptions and framed/unframed JSON parse failures
   while logging internal detail server-side. `jlsp` now returns opaque
   references for request-handler failures and logs detailed read/handler
-  failures server-side under the same reference. Remaining work: router
-  integrations need opaque client error refs.
+  failures server-side under the same reference. `thread-httpd` router dispatch
+  now converts direct route-handler exceptions to opaque 500 responses with
+  references, covering the remaining router integration surface.
 
 ### K3-P1-11 — Parameterized-only SQL in the safe surface
 **Serves:** G1. **Effort:** 2–3 days.
diff --git a/lib/std/net/thread-httpd.ss b/lib/std/net/thread-httpd.ss
index ea1b25b..a7592e1 100644
--- a/lib/std/net/thread-httpd.ss
+++ b/lib/std/net/thread-httpd.ss
@@ -360,8 +360,11 @@
                  [params (and (string=? (car route) method)
                               (match-path-pattern (cadr route) path))])
             (if params
-              (parameterize ([current-route-params params])
-                ((caddr route) req))
+              (try
+                (parameterize ([current-route-params params])
+                  ((caddr route) req))
+                (catch (exn)
+                  (safe-http-error-response 'internal-error exn)))
               (loop (cdr routes))))))))
 
   (def (route-get r path handler)    (router-add! r "GET" path handler))
diff --git a/tests/test-thread-httpd-errors.ss b/tests/test-thread-httpd-errors.ss
index b0cb14d..0c16e13 100644
--- a/tests/test-thread-httpd-errors.ss
+++ b/tests/test-thread-httpd-errors.ss
@@ -63,6 +63,25 @@
           #f)))
     (lambda ()
       (thread-httpd-stop! server))))
+(let* ([secret "router-secret-token-789"]
+       [router (make-router)])
+  (router-add! router "GET" "/boom"
+    (lambda (req)
+      (error 'test-thread-httpd-router secret)))
+  (let* ([resp (router-dispatch
+                 router
+                 (make-request "GET" "/boom" "HTTP/1.1" '() ""))]
+         [body (response-body resp)])
+    (test "router-dispatch status" (response-status resp) 500)
+    (test "router-dispatch body has safe message"
+      (contains-substring? body "Internal server error")
+      #t)
+    (test "router-dispatch body has reference"
+      (contains-substring? body "Reference: ")
+      #t)
+    (test "router-dispatch hides handler secret"
+      (contains-substring? body secret)
+      #f)))
 
 (printf "~%thread-httpd error tests: ~a passed, ~a failed~%" pass fail)
 (when (> fail 0)