Server: render htmx (HX-Request) responses as HTML, not SX wire format
htmx sends HX-Request header on AJAX calls. The server now detects this and renders the SX response to HTML via sx_render_to_html before sending. SX-Request (from SX client navigation) still gets SX wire format. Also skip response cache for htmx requests (they need fresh HTML renders). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3740,18 +3740,23 @@ let http_mode port =
|
||||
(try Unix.close client with _ -> ())
|
||||
in
|
||||
|
||||
(* Check if request has SX-Request or HX-Request header (AJAX navigation) *)
|
||||
(* Check if request has SX-Request header (SX AJAX navigation — return SX wire format)
|
||||
or HX-Request header (htmx AJAX — return rendered HTML). *)
|
||||
let has_substring s sub =
|
||||
let slen = String.length s and sublen = String.length sub in
|
||||
if sublen > slen then false
|
||||
else let rec check i = if i > slen - sublen then false
|
||||
else if String.sub s i sublen = sub then true else check (i + 1)
|
||||
in check 0
|
||||
in
|
||||
let is_sx_request data =
|
||||
let lower = String.lowercase_ascii data in
|
||||
let has_substring s sub =
|
||||
let slen = String.length s and sublen = String.length sub in
|
||||
if sublen > slen then false
|
||||
else let rec check i = if i > slen - sublen then false
|
||||
else if String.sub s i sublen = sub then true else check (i + 1)
|
||||
in check 0
|
||||
in
|
||||
has_substring lower "sx-request" || has_substring lower "hx-request"
|
||||
in
|
||||
let _is_hx_request data =
|
||||
let lower = String.lowercase_ascii data in
|
||||
has_substring lower "hx-request" && not (has_substring lower "sx-request")
|
||||
in
|
||||
|
||||
(* Non-blocking event loop with render worker pool.
|
||||
- Main loop: Unix.select on listen socket + all connected clients
|
||||
@@ -3785,14 +3790,23 @@ let http_mode port =
|
||||
match work with
|
||||
| Some (fd, path, headers) ->
|
||||
let is_ajax = headers <> [] in
|
||||
let cache_key = if is_ajax then "ajax:" ^ path else path in
|
||||
let is_htmx = List.exists (fun (k,_) -> String.lowercase_ascii k = "hx-request") headers in
|
||||
let cache_key = if is_ajax then (if is_htmx then "htmx:" else "ajax:") ^ path else path in
|
||||
let response =
|
||||
try
|
||||
match http_render_page env path headers with
|
||||
| Some body ->
|
||||
let ct = if is_ajax then "text/sx; charset=utf-8"
|
||||
(* htmx requests get HTML; SX requests get SX wire format *)
|
||||
let final_body = if is_htmx then
|
||||
(try
|
||||
let exprs = Sx_parser.parse_all body in
|
||||
let expr = match exprs with [e] -> e | [] -> Nil | _ -> List (Symbol "<>" :: exprs) in
|
||||
Sx_render.sx_render_to_html env expr env
|
||||
with _ -> body)
|
||||
else body in
|
||||
let ct = if is_ajax && not is_htmx then "text/sx; charset=utf-8"
|
||||
else "text/html; charset=utf-8" in
|
||||
let resp = http_response ~content_type:ct body in
|
||||
let resp = http_response ~content_type:ct final_body in
|
||||
Hashtbl.replace response_cache cache_key resp;
|
||||
resp
|
||||
| None -> http_response ~status:404 "<h1>Not Found</h1>"
|
||||
@@ -4120,20 +4134,35 @@ let http_mode port =
|
||||
end
|
||||
end else
|
||||
let has_state_cookie = Hashtbl.mem _request_cookies "sx-home-stepper" in
|
||||
let cache_key = if is_ajax then "ajax:" ^ path else path in
|
||||
match (if has_state_cookie then None
|
||||
let is_htmx_req = is_ajax && has_substring (String.lowercase_ascii data) "hx-request" in
|
||||
let cache_key = if is_htmx_req then "htmx:" ^ path
|
||||
else if is_ajax then "ajax:" ^ path else path in
|
||||
match (if has_state_cookie || is_htmx_req then None
|
||||
else Hashtbl.find_opt response_cache cache_key) with
|
||||
| Some cached -> write_response fd cached; true
|
||||
| None ->
|
||||
if is_ajax then begin
|
||||
(* AJAX: render on main thread — aser only, fast, no SSR.
|
||||
Avoids queueing behind slow full-page renders. *)
|
||||
Avoids queueing behind slow full-page renders.
|
||||
HX-Request (htmx) gets HTML; SX-Request gets SX wire format. *)
|
||||
let headers = parse_http_headers data in
|
||||
let is_htmx = List.exists (fun (k,_) ->
|
||||
String.lowercase_ascii k = "hx-request") headers in
|
||||
let response =
|
||||
try match http_render_page env path headers with
|
||||
| Some body ->
|
||||
let resp = http_response ~content_type:"text/sx; charset=utf-8" body in
|
||||
Hashtbl.replace response_cache cache_key resp; resp
|
||||
let final_body = if is_htmx then
|
||||
(try
|
||||
let exprs = Sx_parser.parse_all body in
|
||||
let expr = match exprs with [e] -> e | [] -> Nil | _ -> List (Symbol "<>" :: exprs) in
|
||||
Sx_render.sx_render_to_html env expr env
|
||||
with _ -> body)
|
||||
else body in
|
||||
let ct = if is_htmx then "text/html; charset=utf-8"
|
||||
else "text/sx; charset=utf-8" in
|
||||
let resp = http_response ~content_type:ct final_body in
|
||||
if not is_htmx then Hashtbl.replace response_cache cache_key resp;
|
||||
resp
|
||||
| None -> http_response ~status:404
|
||||
"(div :class \"p-8\" (h2 :class \"text-rose-600 font-semibold\" \"Page not found\") (p :class \"text-stone-500\" \"No route matched this path\"))"
|
||||
with e ->
|
||||
|
||||
Reference in New Issue
Block a user