From 6a32c29a746a2e44de463de647f9f6661eb5086b Mon Sep 17 00:00:00 2001 From: Xuan-Son Nguyen Date: Thu, 6 Aug 2026 15:07:22 +0200 Subject: [PATCH] server: fix empty response for /cors-proxy (#26656) --- tools/server/server-models.cpp | 22 +++++++++++++++----- tools/server/tests/unit/test_proxy.py | 30 +++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/tools/server/server-models.cpp b/tools/server/server-models.cpp index 188a72a374..2fd9519c10 100644 --- a/tools/server/server-models.cpp +++ b/tools/server/server-models.cpp @@ -2079,9 +2079,8 @@ server_http_proxy::server_http_proxy( return has_next; // false if EOF or pipe broken }; - // wire up the HTTP client - // note: do NOT capture `this` pointer, as it may be destroyed before the thread ends - httplib::ResponseHandler response_handler = [pipe, cli](const httplib::Response & response) { + // build the header message forwarded to the reader thread, stripping internal proxy headers + auto make_header_msg = [](const httplib::Response & response) { msg_t msg; msg.status = response.status; for (const auto & [key, value] : response.headers) { @@ -2095,7 +2094,17 @@ server_http_proxy::server_http_proxy( } msg.headers[key] = value; } - return pipe->write(std::move(msg)); // send headers first + return msg; + }; + + // true once response_handler has already forwarded the headers + auto headers_sent = std::make_shared>(false); + + // wire up the HTTP client + // note: do NOT capture `this` pointer, as it may be destroyed before the thread ends + httplib::ResponseHandler response_handler = [pipe, headers_sent, make_header_msg](const httplib::Response & response) { + headers_sent->store(true); + return pipe->write(make_header_msg(response)); // send headers first }; httplib::ContentReceiverWithProgress content_receiver = [pipe](const char * data, size_t data_length, size_t, size_t) { // send data chunks @@ -2169,13 +2178,16 @@ server_http_proxy::server_http_proxy( // start the proxy thread SRV_DBG("start proxy thread %s %s\n", req.method.c_str(), req.path.c_str()); - this->thread = std::thread([cli, pipe, req]() { + this->thread = std::thread([cli, pipe, req, headers_sent, make_header_msg]() { auto result = cli->send(std::move(req)); if (result.error() != httplib::Error::Success) { auto err_str = httplib::to_string(result.error()); SRV_ERR("http client error: %s\n", err_str.c_str()); pipe->write({{}, 500, "", ""}); // header pipe->write({{}, 0, "proxy error: " + err_str, ""}); // body + } else if (!headers_sent->load()) { + // httplib skips response_handler for bodyless statuses like 204, send headers here instead + pipe->write(make_header_msg(*result)); } pipe->close_write(); // signal EOF to reader SRV_DBG("%s", "client request thread ended\n"); diff --git a/tools/server/tests/unit/test_proxy.py b/tools/server/tests/unit/test_proxy.py index 0fed536e59..cb439b7a45 100644 --- a/tools/server/tests/unit/test_proxy.py +++ b/tools/server/tests/unit/test_proxy.py @@ -1,5 +1,7 @@ import pytest from utils import * +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer server = ServerPreset.tinyllama2() @@ -39,3 +41,31 @@ def test_mcp_proxy_custom_port(): res = server.make_request("GET", f"/cors-proxy?url=http://{server.server_host}:{server.server_port}/models") assert res.status_code == 200 assert "data" in res.body + + +def test_mcp_proxy_no_content(): + # note: see issue #26598 + class NoContentHandler(BaseHTTPRequestHandler): + def do_POST(self): + self.send_response(204) + self.end_headers() + + def log_message(self, format, *args): + pass + + target = ThreadingHTTPServer(("127.0.0.1", 0), NoContentHandler) + target_thread = threading.Thread(target=target.serve_forever, daemon=True) + target_thread.start() + + try: + global server + server.ui_mcp_proxy = True + server.start() + + res = server.make_request("POST", f"/cors-proxy?url=http://127.0.0.1:{target.server_port}/", data={}) + assert res.status_code == 204 + assert res.body in (None, b"", "") + finally: + target.shutdown() + target.server_close() +