From 4ea43e365921d749ae77088a0aba98806042edf9 Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 1 Apr 2026 09:50:16 +0000 Subject: [PATCH] Handler endpoints bypass page routing, return raw fragments Paths containing (api.) are intercepted before page routing and dispatched directly to the api function. The handler result is rendered to SX wire format and returned without layout wrapping. This fixes the issue where handler URLs went through page routing, causing the handler result to be passed as a slug to the page function, and the response to be wrapped in the full page layout. Co-Authored-By: Claude Opus 4.6 (1M context) --- hosts/ocaml/bin/sx_server.ml | 43 +++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index 5fa96763..4434de68 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -2566,9 +2566,50 @@ let http_mode port = in write_response fd (http_response ~content_type:"text/plain; charset=utf-8" result); true end else + (* Handler endpoints: paths containing "(api." are handler calls, + not page renders. Evaluate the handler directly, return fragment. *) + let is_handler_path = + let rec has_sub s sub i = + if i + String.length sub > String.length s then false + else if String.sub s i (String.length sub) = sub then true + else has_sub s sub (i + 1) in + has_sub path "(api." 0 in let is_sx = path = "/sx/" || path = "/sx" || (String.length path > 4 && String.sub path 0 4 = "/sx/") in - if is_sx then begin + if is_sx && is_handler_path then begin + (* Handler dispatch — evaluate handler, return raw fragment *) + let response = + try + let api_fn = env_get env "api" in + (* Extract handler slug from path: ...api.SLUG)... *) + let slug = + let rec find_api s i = + if i + 5 > String.length s then "" + else if String.sub s i 5 = "(api." then + let start = i + 5 in + let end_ = try String.index_from s start ')' with Not_found -> String.length s in + String.sub s start (end_ - start) + else find_api s (i + 1) in + find_api path 0 in + let result = Sx_ref.cek_call api_fn (List [String slug]) in + (* Render to SX wire format or HTML depending on content type *) + let body_str = match result with + | String s | SxExpr s -> s + | Nil -> "" + | _ -> + let call = List [Symbol "aser"; List [Symbol "quote"; result]; Env env] in + (match Sx_ref.eval_expr call (Env env) with + | String s | SxExpr s -> s + | v -> Sx_types.inspect v) in + http_response ~content_type:"text/sx; charset=utf-8" body_str + with e -> + Printf.eprintf "[handler] Error for %s: %s\n%!" path (Printexc.to_string e); + http_response ~status:500 ~content_type:"text/sx; charset=utf-8" + (Printf.sprintf "(div :class \"p-4 text-rose-600\" \"Handler error: %s\")" + (escape_sx_string (Printexc.to_string e))) + in + write_response fd response; true + end else if is_sx then begin let cache_key = if is_ajax then "ajax:" ^ path else path in match Hashtbl.find_opt response_cache cache_key with | Some cached -> write_response fd cached; true