diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index 3cae0201..f14f49f6 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -817,14 +817,28 @@ let setup_evaluator_bridge env = (* Run the handler through the IO-aware CEK runner (not bare sx_call) so request handlers can perform per-request IO — durable store reads/writes resolve via cek_run_with_io's - suspension loop instead of returning an unresolved suspension. *) + suspension loop instead of returning an unresolved suspension. + On ANY handler exception, synthesise a 500 response rather than + letting it escape: an escaped exception drops the connection + with no bytes written, which a reverse proxy (Caddy/Cloudflare) + surfaces as a 502 error page. A real 500 keeps the origin + responsive and debuggable. Note: a native exception (e.g. the + parser's Parse_error) cannot be caught by an SX (guard ...), so + this boundary is the only place it can be trapped. *) (try let st = Sx_ref.continue_with_call handler (List [Dict req]) (Env (Sx_types.make_env ())) (List [Dict req]) (List []) in - cek_run_with_io st - with e -> Mutex.unlock mtx; raise e) in - Mutex.unlock mtx; + let r = cek_run_with_io st in + Mutex.unlock mtx; r + with e -> + Mutex.unlock mtx; + Printf.eprintf "[http-listen] handler error: %s\n%!" + (Printexc.to_string e); + let d = Sx_types.make_dict () in + Hashtbl.replace d "status" (Integer 500); + Hashtbl.replace d "body" (String "Internal Server Error"); + Dict d) in let getk k = match resp with | Dict h -> Hashtbl.find_opt h k | _ -> None in let status = match getk "status" with @@ -1250,6 +1264,20 @@ let setup_type_constructors env = (* Already a value — return as-is *) v | _ -> raise (Eval_error "parse: expected string")); + (* Like parse, but returns nil instead of raising on malformed input. The + parser raises a native Parse_error that an SX-level (guard ...) cannot catch + (guard only traps SX conditions, not host exceptions), so code that handles + untrusted text — e.g. a stored post body — needs a value-returning parse to + degrade gracefully rather than crash the request. *) + bind "parse-safe" (fun args -> + match args with + | [String s] | [SxExpr s] -> + (try + let exprs = Sx_parser.parse_all s in + (match exprs with [e] -> e | _ -> List exprs) + with _ -> Nil) + | [v] -> v + | _ -> Nil); (* Native bytecode compiler — bootstrapped from lib/compiler.sx *) bind "compile" (fun args -> match args with [expr] -> Sx_compiler.compile expr | _ -> Nil); diff --git a/lib/host/blog.sx b/lib/host/blog.sx index 1322a8c1..872eeec3 100644 --- a/lib/host/blog.sx +++ b/lib/host/blog.sx @@ -76,7 +76,7 @@ (fn (record) (let ((sx (get record :sx-content))) (if (and sx (not (= sx ""))) - (let ((tree (guard (e (true nil)) (parse sx)))) + (let ((tree (parse-safe sx))) (cond ((nil? tree) "

(unparseable content)

") ((and (= (type-of tree) "list") (> (len tree) 0)