From f89e33e2412ff01a6fd01164a30752ca7408bf25 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 18 Jul 2026 01:33:57 +0000 Subject: [PATCH] Frame brokered HTTP responses once --- docs/wasm-runtime-architecture.md | 9 +++++++++ site/tests/cli_runner.uce | 5 +++++ site/tests/services.uce | 2 +- src/fastcgi/src/fcgicc.cc | 2 ++ src/linux_fastcgi.cpp | 8 +++++++- src/wasm/core.cpp | 2 ++ 6 files changed, 26 insertions(+), 2 deletions(-) diff --git a/docs/wasm-runtime-architecture.md b/docs/wasm-runtime-architecture.md index b40bb50..c926772 100644 --- a/docs/wasm-runtime-architecture.md +++ b/docs/wasm-runtime-architecture.md @@ -62,6 +62,15 @@ forced by Wasmtime: an `Engine`/`Store` cannot be safely re-created across runtime. So the brokers hold the long-lived connection and units respond to events the same way they respond to a page request — through a clean worker. +The broker-to-worker hop explicitly sets `GATEWAY_INTERFACE=CGI/1.1`. After +the Wasm core decodes request parameters it derives the guest's default status +syntax from that marker (`Status:` for FastCGI, `HTTP/1.1` for direct HTTP). +The FastCGI transport clears completed stdout/stderr as soon as their records +are queued and emits no stream records after `FCGI_END_REQUEST`. Together these +boundaries prevent a forwarded dynamic response from being parsed as body and +wrapped in a second HTTP response. The TCP and custom-server tests reject an +inner status line and require the exact dynamic body. + --- ## 2. The membrane and the DValue ABI diff --git a/site/tests/cli_runner.uce b/site/tests/cli_runner.uce index c266c09..e443368 100644 --- a/site/tests/cli_runner.uce +++ b/site/tests/cli_runner.uce @@ -421,6 +421,11 @@ void cli_run_tcp_smoke() cli_test_case("uce_tcp_smoke:http websocket port 8080", fd8080 != 0, "TCP connect " + String(fd8080 != 0 ? "succeeded" : "failed") + " on port 8080"); if(fd8080 != 0) socket_close(fd8080); + CliHttpResponse direct = cli_direct_http("/tests/pool-isolation.uce?token=raw-http-framing"); + StringList direct_parts = split(trim(direct.body), "|"); + bool one_response = direct.status == 200 && direct_parts.size() == 3 && direct_parts[0] == "fresh" && direct_parts[1] == "raw-http-framing" && direct.body.find("HTTP/1.1 ") == String::npos; + cli_test_case("uce_tcp_smoke:raw HTTP dynamic response is framed once", one_response, + one_response ? "single status/header block and exact dynamic body" : "status=" + std::to_string(direct.status) + " body=" + cli_truncate(direct.body)); } void cli_run_wasm_kill(bool include_kill) diff --git a/site/tests/services.uce b/site/tests/services.uce index 6af1104..0fbe4d5 100644 --- a/site/tests/services.uce +++ b/site/tests/services.uce @@ -66,7 +66,7 @@ RENDER(Request& context) socket_close(stopped_sockfd); mark( "server_start_http() / SERVE_HTTP:named / server_stop()", - (custom_http_pid != 0 && write_ok && response.find("custom-http-named") != String::npos && response.find("query=alpha=1") != String::npos && stopped && listener_closed) ? "pass" : "fail", + (custom_http_pid != 0 && write_ok && response.find("custom-http-named") != String::npos && response.find("query=alpha=1") != String::npos && response.find("\r\nHTTP/1.1 ") == String::npos && stopped && listener_closed) ? "pass" : "fail", response.substr(0, response.length() > 260 ? 260 : response.length()) + " stopped=" + (stopped ? "true" : "false") + " listener_closed=" + (listener_closed ? "true" : "false") ); } diff --git a/src/fastcgi/src/fcgicc.cc b/src/fastcgi/src/fcgicc.cc index dba0483..045fcec 100644 --- a/src/fastcgi/src/fcgicc.cc +++ b/src/fastcgi/src/fcgicc.cc @@ -1388,6 +1388,8 @@ FastCGIServer::request_write_fgci(Connection& connection, RequestID id, //switch_to_system_alloc(); write_data_fcgi(connection.output_buffer, id, request.out, FCGI_STDOUT); write_data_fcgi(connection.output_buffer, id, request.err, FCGI_STDERR); + request.out.clear(); + request.err.clear(); FCGI_EndRequestRecord complete; bzero(&complete, sizeof(complete)); diff --git a/src/linux_fastcgi.cpp b/src/linux_fastcgi.cpp index 0f3136f..deaf5af 100644 --- a/src/linux_fastcgi.cpp +++ b/src/linux_fastcgi.cpp @@ -855,7 +855,13 @@ static String custom_server_dispatcher_key = ""; int forward_request_to_worker(FastCGIRequest& request, u32 timeout_seconds = 30) { String fcgi_socket = first(server_state.config["FCGI_SOCKET_PATH"], "/run/uce.sock"); - FcgiForwardResult fwd = fcgi_forward_request(fcgi_socket, request.params, request.in, timeout_seconds); + StringMap forward_params = request.params; + // The broker accepted raw HTTP, but this hop is FastCGI. Without the CGI + // marker Request::set_status() emits an HTTP status line that the FastCGI + // reply parser correctly treats as body; the broker would then wrap that + // complete response in a second HTTP response. + forward_params["GATEWAY_INTERFACE"] = "CGI/1.1"; + FcgiForwardResult fwd = fcgi_forward_request(fcgi_socket, forward_params, request.in, timeout_seconds); request.ob_start(); if(!fwd.ok) { diff --git a/src/wasm/core.cpp b/src/wasm/core.cpp index 0f43bda..8277391 100644 --- a/src/wasm/core.cpp +++ b/src/wasm/core.cpp @@ -830,6 +830,8 @@ int uce_wasm_apply_context(const char* buf, size_t len) if(applied.key("server_config")) apply_map(applied.key("server_config"), wasm_server.config); apply_map(applied.key("params"), wasm_request.params); + wasm_request.response_code = wasm_request.params["GATEWAY_INTERFACE"] != "" ? + "Status: 200 OK" : "HTTP/1.1 200 OK"; apply_map(applied.key("get"), wasm_request.get); apply_map(applied.key("post"), wasm_request.post); apply_map(applied.key("cookies"), wasm_request.cookies);