From 3cbe6f50d43e42098cca17defa36337e89d97944 Mon Sep 17 00:00:00 2001 From: udo Date: Sat, 18 Jul 2026 16:55:42 +0000 Subject: [PATCH] Fail unresolved component renders with HTTP 500 --- site/doc/pages/component.txt | 2 ++ site/doc/pages/component_render.txt | 2 ++ site/tests/cli_runner.uce | 15 +++++++++++++++ site/tests/component_failure.uce | 9 +++++++++ src/wasm/core.cpp | 1 + 5 files changed, 29 insertions(+) create mode 100644 site/tests/component_failure.uce diff --git a/site/doc/pages/component.txt b/site/doc/pages/component.txt index f8e7ff5..4bc4e79 100644 --- a/site/doc/pages/component.txt +++ b/site/doc/pages/component.txt @@ -14,6 +14,8 @@ Renders another `.uce` file as a component and returns the captured output as a Component props are passed in `context.props`. +Calling a missing component or named component handler is a server error and sets HTTP status 500. Probe intentionally optional components with `component_exists()` before calling `component()`. + Because `` HTML-escapes its value, embed component markup with ``, `print(component(...))`, or use `component_render(...)` for direct output. ## Named Components diff --git a/site/doc/pages/component_render.txt b/site/doc/pages/component_render.txt index 81c8c55..1ffe782 100644 --- a/site/doc/pages/component_render.txt +++ b/site/doc/pages/component_render.txt @@ -17,6 +17,8 @@ Component props are passed through `context.props`, and `name:COMPONENTFUNC` may When `name` starts with `:`, the runtime resolves that named handler against the current `.uce` file. +Calling a missing component or named component handler is a server error and sets HTTP status 500. Probe intentionally optional components with `component_exists()` before calling `component_render()`. + If the target file defines `ONCE(Request& context)`, that hook runs once per request before the file's first component or render entrypoint. Use `component_render()` when you want to write component output directly from C++ code instead of capturing it as a `String`. diff --git a/site/tests/cli_runner.uce b/site/tests/cli_runner.uce index e443368..c18f6eb 100644 --- a/site/tests/cli_runner.uce +++ b/site/tests/cli_runner.uce @@ -329,6 +329,21 @@ void cli_run_site_suite(bool skip_local_service_pages = false) void cli_run_security_smoke() { + CliHttpResponse missing_component = cli_frontend("/tests/component_failure.uce"); + cli_test_case("uce_component_failure:component_render missing target is HTTP 500", + missing_component.status == 500 && cli_contains(missing_component.body, "component not found: components/does-not-exist"), + "status=" + std::to_string(missing_component.status) + " body=" + cli_truncate(missing_component.body)); + + CliHttpResponse missing_captured_component = cli_frontend("/tests/component_failure.uce?mode=capture"); + cli_test_case("uce_component_failure:component missing target is HTTP 500", + missing_captured_component.status == 500 && cli_contains(missing_captured_component.body, "component not found: components/does-not-exist"), + "status=" + std::to_string(missing_captured_component.status) + " body=" + cli_truncate(missing_captured_component.body)); + + CliHttpResponse optional_component = cli_frontend("/tests/component_failure.uce?mode=optional"); + cli_test_case("uce_component_failure:component_exists keeps optional lookup non-fatal", + optional_component.status == 200 && optional_component.body == "optional component absent", + "status=" + std::to_string(optional_component.status) + " body=" + cli_truncate(optional_component.body)); + CliHttpResponse dotdot = cli_direct_http("/../site/demo/hello.uce"); cli_test_case("uce_security_smoke:direct HTTP rejects dot-dot script traversal", dotdot.status == 404 && !cli_contains(dotdot.body, "hello world"), "direct HTTP dot-dot traversal returned HTTP " + std::to_string(dotdot.status)); diff --git a/site/tests/component_failure.uce b/site/tests/component_failure.uce new file mode 100644 index 0000000..f30d53a --- /dev/null +++ b/site/tests/component_failure.uce @@ -0,0 +1,9 @@ +RENDER(Request& context) +{ + if(context.get["mode"] == "optional") + print(component_exists("components/does-not-exist") ? "unexpected" : "optional component absent"); + else if(context.get["mode"] == "capture") + print(component("components/does-not-exist", context)); + else + component_render("components/does-not-exist", context); +} diff --git a/src/wasm/core.cpp b/src/wasm/core.cpp index a6e33b5..e3f009a 100644 --- a/src/wasm/core.cpp +++ b/src/wasm/core.cpp @@ -686,6 +686,7 @@ static void component_render_with_props(String name, DValue& props, Request& req s32 slot = wasm_resolve_target(file_name, handler, &resolved); if(!slot) { + request.set_status(500, "Internal Server Error"); print(component_error_banner("component not found: " + trim(name))); return; }