feat: add configurable UCE error pages

This commit is contained in:
udo
2026-06-12 08:59:09 +00:00
parent b957a2373b
commit 941f5aea08
12 changed files with 352 additions and 1 deletions
+107 -1
View File
@@ -18,7 +18,7 @@ const char* UCE_CLI_SYMBOL = "__uce_cli";
const char* UCE_SERVE_HTTP_SYMBOL = "__uce_serve_http";
const char* UCE_ONCE_SYMBOL = "__uce_once";
const char* UCE_INIT_SYMBOL = "__uce_init";
const u64 UCE_UNIT_ABI_VERSION = 4;
const u64 UCE_UNIT_ABI_VERSION = 5;
struct SharedUnitFilesystemState
{
@@ -995,6 +995,15 @@ SharedUnit* compiler_load_shared_unit(Request* context, String file_name, String
printf("%s\n", display_messages.c_str());
if(compiler_can_write_response(context) && context->stats.invoke_count == 1)
{
DTree error_info;
error_info["type"] = "compiler_error";
error_info["status"] = su->compile_status;
error_info["source"] = su->file_name;
error_info["compiler_output"] = su->compiler_messages;
error_info["generated_cpp"] = compiler_generated_cpp_path(su);
error_info["details"] = display_messages;
if(compiler_render_error_page(context, "page_compiler_error", 500, "UCE Unit Compile Error", error_info))
return(0);
context->set_status(500, "UCE Unit Compile Error");
context->header["Content-Type"] = "text/plain; charset=utf-8";
}
@@ -1739,9 +1748,106 @@ bool compiler_invoke_component(Request* context, String file_name, String render
));
}
String compiler_error_page_unit(Request* context, String config_key)
{
if(!context || !context->server)
return("");
String configured = trim(first(
context->server->config[config_key],
context->server->config[to_upper(config_key)]
));
if(configured == "")
return("");
// Resolve relative to where the server was started, not the volatile
// per-unit working directory (which is also stale right after a fault).
String resolved = compiler_resolve_unit_path(context, configured, process_start_directory());
if(resolved == "" || !file_exists(resolved))
return("");
return(resolved);
}
bool compiler_render_error_page(Request* context, String config_key, s32 status_code, String status_reason, DTree error_info)
{
if(!compiler_can_write_response(context))
return(false);
if(!context->server || context->resources.error_page_active)
return(false);
String unit_file = compiler_error_page_unit(context, config_key);
if(unit_file == "")
return(false);
String previous_response_code = context->response_code;
s32 previous_status = context->flags.status;
String previous_content_type = context->header["Content-Type"];
context->resources.error_page_active = true;
context->call["error"] = error_info;
context->set_status(status_code, status_reason);
context->header["Content-Type"] = first(context->server->config["CONTENT_TYPE"], "text/html; charset=utf-8");
ob_start();
String error_message = "";
bool rendered = compiler_invoke_render(context, unit_file, "render", &error_message);
String html = ob_get_close();
context->resources.error_page_active = false;
if(!rendered)
{
printf("(!) configured %s page %s failed to render: %s\n", config_key.c_str(), unit_file.c_str(), trim(error_message).c_str());
context->response_code = previous_response_code;
context->flags.status = previous_status;
context->header["Content-Type"] = previous_content_type;
context->call.remove("error");
return(false);
}
print(html);
return(true);
}
bool compiler_unit_compile_pending(Request* context, String file_name)
{
String normalized = compiler_normalize_unit_path(context, file_name);
if(normalized == "" || !file_exists(normalized))
return(false);
if(compiler_reusable_cached_unit(context, normalized, false, false))
return(false);
SharedUnit probe;
setup_unit_paths(context, &probe, normalized);
auto state = inspect_shared_unit_filesystem(context, &probe);
if(!shared_unit_compile_check(state).needs_compile)
return(false);
// A persisted, still-current failure is a compiler error, not a build in
// progress: let the load path surface it.
if(compiler_failure_retry_deferred(context, &probe, state))
return(false);
return(true);
}
void compiler_invoke(Request* context, String file_name)
{
printf("(i) compiler_invoke file %s\n", file_name.c_str());
// Entry-unit hook for the configured "compiling" page: when the unit needs
// a (re)build that the proactive compiler can deliver, answer immediately
// instead of blocking the worker on a synchronous compile.
if(context && context->stats.invoke_count == 0 &&
!context->resources.is_cli && !context->resources.error_page_active &&
config_bool("PROACTIVE_COMPILE_ENABLED", true) &&
compiler_error_page_unit(context, "page_compiling") != "")
{
String resolved = compiler_resolve_unit_path(context, file_name, "");
if(resolved != "" && compiler_unit_compile_pending(context, resolved))
{
compiler_track_known_unit(context, resolved);
DTree error_info;
error_info["type"] = "compiling";
error_info["source"] = resolved;
if(compiler_render_error_page(context, "page_compiling", 503, "UCE Unit Compiling", error_info))
return;
}
}
String error_message = "";
if(!compiler_invoke_render(context, file_name, "render", &error_message) && error_message != "")
{
+3
View File
@@ -21,6 +21,9 @@ void compiler_invoke_websocket(Request* context, String file_name);
void compiler_invoke_cli(Request* context, String file_name);
void compiler_invoke_serve_http(Request* context, String file_name, String handler_name = "");
SharedUnit* compiler_load_shared_unit(Request* context, String file_name, String current_path = "", bool opt_so_optional = false);
String compiler_error_page_unit(Request* context, String config_key);
bool compiler_render_error_page(Request* context, String config_key, s32 status_code, String status_reason, DTree error_info);
bool compiler_unit_compile_pending(Request* context, String file_name);
String compiler_site_directory(Request* context);
StringList compiler_scan_site_units(Request* context);
StringList compiler_list_known_units(Request* context);
+9
View File
@@ -348,6 +348,15 @@ String cwd_get()
return(std::filesystem::current_path());
}
String process_start_directory()
{
// Primed in main() before any unit handler can chdir; fault recovery and
// config-relative path resolution anchor to this instead of the volatile
// per-unit working directory.
static String start_directory = cwd_get();
return(start_directory);
}
void cwd_set(String path)
{
chdir(path.c_str());
+1
View File
@@ -30,6 +30,7 @@ bool file_append(String file_name, Ts... args)
}
String cwd_get();
void cwd_set(String path);
String process_start_directory();
time_t file_mtime(String file_name);
void file_unlink(String file_name);
String expand_path(String path, String relative_to_path = "");
+4
View File
@@ -224,6 +224,10 @@ struct Request {
bool websocket_is_text = false;
String current_unit_file = "";
String params_buffer;
// True while a configured error page (page_compiling /
// page_compiler_error / page_runtime_error) is rendering, so a failing
// error page can never recurse into another error page.
bool error_page_active = false;
} resources;
void ob_start();
+28
View File
@@ -71,6 +71,30 @@ void render_request_failure(Request& request, String title, String details, Stri
Request* previous_context = set_active_request(request);
clear_request_output(request);
if(!request.resources.is_cli)
{
DTree error_info;
error_info["type"] = "runtime_error";
error_info["title"] = title;
error_info["details"] = details;
error_info["source"] = request.params["SCRIPT_FILENAME"];
error_info["request_uri"] = first(request.params["REQUEST_URI"], request.params["SCRIPT_FILENAME"]);
if(request.server && request.params["SCRIPT_FILENAME"] != "")
error_info["generated_cpp"] = compiler_generated_cpp_path(&request, request.params["SCRIPT_FILENAME"]);
if(request_fault_signal != 0)
{
error_info["signal"] = (f64)request_fault_signal;
error_info["signal_name"] = signal_name((int)request_fault_signal);
}
error_info["trace"] = trace;
if(compiler_render_error_page(&request, "page_runtime_error", status_code, "Internal Server Error", error_info))
{
request.err = "UCE runtime error: " + title + (details != "" ? " (" + details + ")" : "");
restore_active_request(previous_context);
return;
}
}
String body;
body += "UCE runtime error\n";
body += "Request: " + first(request.params["REQUEST_URI"], request.params["SCRIPT_FILENAME"]) + "\n";
@@ -902,6 +926,9 @@ int handle_complete(FastCGIRequest& request) {
failure_title = "fatal signal during request";
failure_details = "worker recovered before closing the upstream connection";
failure_trace = backtrace_frames_string(request_fault_frames, request_fault_frame_count, 1);
// The siglongjmp skipped UnitInvocationScope's destructor, so the
// worker may still sit in the crashed unit's source directory.
cwd_set(process_start_directory());
}
else
{
@@ -1508,6 +1535,7 @@ int main(int argc, char** argv)
// Warm up libgcc's backtrace state so the first in-handler backtrace()
// after a fault does not allocate.
backtrace(request_fault_frames, 4);
process_start_directory();
init_base_process();
ensure_proactive_compiler();