From fd41bf65a2e0013b1ecafa832f8ce6f18b1753e8 Mon Sep 17 00:00:00 2001 From: Pascal Date: Tue, 21 Jul 2026 17:47:54 +0200 Subject: [PATCH] server: return 400 instead of 500 on validation error with X-Conversation-Id (#25760) * server: return 400 instead of 500 on validation error with X-Conversation-Id set_req() attaches the spipe as soon as the header is present, before the request body is parsed. When params validation throws, set_next() never runs and next_orig stays empty, so on_complete() called it and crashed with std::bad_function_call, turning the prepared 400 JSON into a generic 500. on_complete() now treats an empty next_orig as "streaming never started" and evicts the session installed by set_req(), so a failed request leaves nothing behind for discovery or replay. This also covers valid requests that carry the header but do not stream, which previously left an empty finalized session in the map until the GC TTL. * ui: do not send the backend_sampling placeholder On a fresh profile the syncable settings hold the empty string placeholder meaning "let the server decide". Every neighbor field goes through the hasValue() guard that filters it, except backend_sampling, which sent the placeholder verbatim and made every default settings completion fail validation. Guard the field with hasValue() like its neighbors. hasValue(false) is true, so an explicit false still reaches the server and the intent of #18781 (send both true and false) is preserved. Only the placeholder is filtered. --- tools/server/server-stream.cpp | 7 +++++++ tools/ui/src/lib/stores/chat.svelte.ts | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/tools/server/server-stream.cpp b/tools/server/server-stream.cpp index 19db04988d..f0a35b18e5 100644 --- a/tools/server/server-stream.cpp +++ b/tools/server/server-stream.cpp @@ -632,6 +632,13 @@ void server_res_spipe::on_complete() { if (!spipe || next_finished) { return; } + // an empty next_orig means set_next() never ran: the request failed before streaming + // started, typically a params validation throw. evict the session installed by set_req() + // so the failed request leaves nothing behind for discovery or replay + if (!next_orig) { + g_stream_sessions.evict(server_stream_conv_id_from_headers(req->headers)); + return; + } std::string chunk; while (!spipe->is_cancelled()) { chunk.clear(); diff --git a/tools/ui/src/lib/stores/chat.svelte.ts b/tools/ui/src/lib/stores/chat.svelte.ts index 658eb5978c..ab418ed9cd 100644 --- a/tools/ui/src/lib/stores/chat.svelte.ts +++ b/tools/ui/src/lib/stores/chat.svelte.ts @@ -2428,7 +2428,8 @@ class ChatStore { if (currentConfig.samplers) apiOptions.samplers = currentConfig.samplers; - apiOptions.backend_sampling = currentConfig.backend_sampling; + if (hasValue(currentConfig.backend_sampling)) + apiOptions.backend_sampling = currentConfig.backend_sampling; if (currentConfig.customJson) apiOptions.custom = currentConfig.customJson;