feat: add configurable UCE error pages
This commit is contained in:
parent
b957a2373b
commit
941f5aea08
@ -699,3 +699,24 @@ order-independent check (n unique canonical index keys with max n-1), index
|
||||
keys require canonical form (`"1"`, not `"01"`), and `each()` iterates lists
|
||||
in numeric index order, matching the encoders. Regression test with a
|
||||
12-entry list in core.uce covers `each`, `dtree_values`, and `dtree_map`.
|
||||
|
||||
### 7.10 NEW: fault recovery left the worker in the crashed unit's working directory
|
||||
|
||||
**File:** `src/lib/compiler.cpp` (`UnitInvocationScope`), `src/linux_fastcgi.cpp`, found while building configurable error pages (2026-06-12)
|
||||
|
||||
`UnitInvocationScope` chdirs into the unit's source directory for every handler
|
||||
invocation and restores it in its destructor — but `siglongjmp` skips
|
||||
destructors, so after any recovered fault the worker process stayed in the
|
||||
crashed unit's directory **permanently** (the next scope captures the wrong
|
||||
directory as its "previous" value and faithfully restores it). Every relative
|
||||
filesystem operation in every subsequent request on that worker resolved
|
||||
against the wrong base until restart.
|
||||
|
||||
**Fixed:** `process_start_directory()` (primed in `main()` before any chdir)
|
||||
anchors the start directory; the fault-recovery branch in `handle_complete`
|
||||
restores it explicitly. Config-relative paths (e.g. the new `page_*` error
|
||||
page keys) resolve against the same anchor instead of the volatile cwd.
|
||||
|
||||
**Status:** fixed and verified — after a SIGSEGV-recovered request, the next
|
||||
request on the same worker renders correctly (previously its relative paths
|
||||
were silently broken).
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
Runtime
|
||||
error_pages
|
||||
unit_info
|
||||
units_list
|
||||
unit_compile
|
||||
|
||||
61
site/doc/pages/error_pages.txt
Normal file
61
site/doc/pages/error_pages.txt
Normal file
@ -0,0 +1,61 @@
|
||||
:title
|
||||
Configurable Error Pages
|
||||
|
||||
:sig
|
||||
page_compiling=site/errors/compiling.uce
|
||||
page_compiler_error=site/errors/compiler-error.uce
|
||||
page_runtime_error=site/errors/runtime-error.uce
|
||||
|
||||
:params
|
||||
page_compiling : UCE page rendered (status 503) while the requested page is being built
|
||||
page_compiler_error : UCE page rendered (status 500) when the requested page fails to build
|
||||
page_runtime_error : UCE page rendered (status 500) after a recovered fault or uncaught exception
|
||||
|
||||
:see
|
||||
>runtime
|
||||
0_Request
|
||||
0_DTree
|
||||
set_status
|
||||
|
||||
:content
|
||||
Three optional keys in `/etc/uce/settings.cfg` route error conditions to developer-defined UCE pages. Each key names a `.uce` file, either absolute or relative to the server's working directory. When a key is unset, or the named file does not exist, or the error page itself fails, the runtime's built-in plain-text behavior stands.
|
||||
|
||||
All three pages receive the error context in `context.call["error"]`:
|
||||
|
||||
- `type` — `compiling`, `compiler_error`, or `runtime_error`
|
||||
- `source` — the requested `.uce` file
|
||||
- `compiler_error` adds: `compiler_output` (raw clang output), `generated_cpp`, `status`, `details`
|
||||
- `runtime_error` adds: `title`, `details`, `request_uri`, `signal`, `signal_name`, `trace`, `generated_cpp`
|
||||
|
||||
A sensible status code is set before the page renders (`503` for compiling, `500` for errors); the page may override it with `set_status()` and set its own headers.
|
||||
|
||||
## page_compiling
|
||||
|
||||
Without this key, a request that hits an uncompiled or changed page blocks until the synchronous build finishes. With it, the runtime responds immediately with your page, hands the build to the proactive compiler, and the page can poll until the build lands:
|
||||
|
||||
```cpp
|
||||
RENDER(Request& context)
|
||||
{
|
||||
context.header["Refresh"] = "2";
|
||||
<><!doctype html><html><head><meta http-equiv="refresh" content="2"></head>
|
||||
<body>Building… this page reloads automatically.</body></html></>
|
||||
}
|
||||
```
|
||||
|
||||
The compiling page is only used while `PROACTIVE_COMPILE_ENABLED` is on (the default) — otherwise nothing would finish the build, and the runtime falls back to the blocking compile.
|
||||
|
||||
## page_compiler_error
|
||||
|
||||
Triggered when the entry page fails to build (components that fail mid-page keep the inline behavior, since the page is already half-rendered). `context.call["error"]["compiler_output"]` carries the full raw compiler output — show it on development hosts, hide it in production.
|
||||
|
||||
## page_runtime_error
|
||||
|
||||
Triggered after the worker recovers from a fatal signal or an uncaught exception. The worker has just recovered from a fault, so keep this page simple: static markup and the error fields, no database connections or heavy components.
|
||||
|
||||
## Recursion safety
|
||||
|
||||
While an error page renders, error-page handling is disabled: a compiling, broken, or crashing error page falls back to the built-in behavior instead of looping.
|
||||
|
||||
## Ready-made examples
|
||||
|
||||
`site/errors/compiling.uce`, `site/errors/compiler-error.uce`, and `site/errors/runtime-error.uce` in this repository are working examples — point the config keys at them or copy them into your site.
|
||||
36
site/errors/compiler-error.uce
Normal file
36
site/errors/compiler-error.uce
Normal file
@ -0,0 +1,36 @@
|
||||
// Example page_compiler_error handler. Enable in /etc/uce/settings.cfg:
|
||||
// page_compiler_error=site/errors/compiler-error.uce
|
||||
// Served with status 500 when the requested unit fails to build. The full
|
||||
// compiler output stays available in context.call["error"], so this page can
|
||||
// show as much or as little of it as the deployment wants.
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree error = context.call["error"];
|
||||
|
||||
<>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Build failed</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; background: #1c1017; color: #fde8e8; margin: 0; min-height: 100vh; }
|
||||
main { max-width: 60rem; margin: 0 auto; padding: 3rem 2rem; }
|
||||
h1 { color: #f87171; }
|
||||
code { color: #fca5a5; }
|
||||
pre { background: #0f0a0d; color: #e2e8f0; padding: 1rem; border-radius: 0.5rem; overflow-x: auto; font-size: 0.85rem; line-height: 1.5; }
|
||||
.meta { color: #9f8a93; font-size: 0.9rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>This page failed to build</h1>
|
||||
<p>Source: <code><?= error["source"].to_string() ?></code></p>
|
||||
<p class="meta">Generated C++: <code><?= error["generated_cpp"].to_string() ?></code></p>
|
||||
<pre><?= error["compiler_output"].to_string() ?></pre>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
</>
|
||||
}
|
||||
36
site/errors/compiling.uce
Normal file
36
site/errors/compiling.uce
Normal file
@ -0,0 +1,36 @@
|
||||
// Example page_compiling handler. Enable in /etc/uce/settings.cfg:
|
||||
// page_compiling=site/errors/compiling.uce
|
||||
// Served with status 503 while the requested unit is being (re)built; the
|
||||
// page refreshes itself until the build finishes and the real page renders.
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
context.header["Refresh"] = "2";
|
||||
String source = context.call["error"]["source"].to_string();
|
||||
|
||||
<>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="refresh" content="2">
|
||||
<title>Building page…</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
|
||||
main { text-align: center; max-width: 40rem; padding: 2rem; }
|
||||
.pulse { width: 3rem; height: 3rem; margin: 0 auto 1.5rem; border-radius: 50%; background: #38bdf8; animation: pulse 1.2s ease-in-out infinite; }
|
||||
@keyframes pulse { 0%, 100% { transform: scale(0.8); opacity: 0.5; } 50% { transform: scale(1); opacity: 1; } }
|
||||
code { color: #94a3b8; font-size: 0.85rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="pulse"></div>
|
||||
<h1>Building this page…</h1>
|
||||
<p>The page is being compiled and will load automatically in a moment.</p>
|
||||
<p><code><?= source ?></code></p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
</>
|
||||
}
|
||||
45
site/errors/runtime-error.uce
Normal file
45
site/errors/runtime-error.uce
Normal file
@ -0,0 +1,45 @@
|
||||
// Example page_runtime_error handler. Enable in /etc/uce/settings.cfg:
|
||||
// page_runtime_error=site/errors/runtime-error.uce
|
||||
// Served with status 500 after a recovered fault or uncaught exception. Keep
|
||||
// runtime error pages simple: they run in a worker that may have just
|
||||
// recovered from a fatal signal, so avoid database work or heavy components.
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
DTree error = context.call["error"];
|
||||
String signal_name = error["signal_name"].to_string();
|
||||
|
||||
<>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Something went wrong</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; background: #1c1017; color: #fde8e8; margin: 0; min-height: 100vh; }
|
||||
main { max-width: 60rem; margin: 0 auto; padding: 3rem 2rem; }
|
||||
h1 { color: #f87171; }
|
||||
code { color: #fca5a5; }
|
||||
pre { background: #0f0a0d; color: #e2e8f0; padding: 1rem; border-radius: 0.5rem; overflow-x: auto; font-size: 0.85rem; line-height: 1.5; }
|
||||
.meta { color: #9f8a93; font-size: 0.9rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Something went wrong</h1>
|
||||
<p><?= error["title"].to_string() ?></p>
|
||||
<p class="meta">Request: <code><?= error["request_uri"].to_string() ?></code></p>
|
||||
<? if(error["details"].to_string() != "") { ?>
|
||||
<p class="meta">Details: <?= error["details"].to_string() ?></p>
|
||||
<? } ?>
|
||||
<? if(signal_name != "") { ?>
|
||||
<p class="meta">Signal: <?= signal_name ?></p>
|
||||
<? } ?>
|
||||
<? if(error["trace"].to_string() != "") { ?>
|
||||
<pre><?= error["trace"].to_string() ?></pre>
|
||||
<? } ?>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
</>
|
||||
}
|
||||
@ -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 != "")
|
||||
{
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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 = "");
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user