Repository navigation
fix(api): search-provider guard's 400 is swallowed by the chat route — clients get 500 with an empty body and never see the "use /v1/search" hint (#10274 follow-up) #17858
Description
OmniRoute Version
3.8.52 (Desktop, Windows)
Summary
Follow-up gap to #10274 (search providers rejected as chat/combo targets). The guard added for #10274 throws an Error with status = 400 and a helpful message, but nothing in the chat route ever reads err.status. The error is re-thrown from the /v1/chat/completions route's top-level catch, and Next.js's App-Route fallback turns it into HTTP 500 with a completely empty body. The actionable hint (use the /v1/search endpoint instead) never reaches the client.
Steps to Reproduce
- Activate any search provider (e.g. AnySearch) in Settings → Providers.
- Send it as a chat model:
curl -sS -i http://localhost:20128/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"anysearch-search/web","messages":[{"role":"user","content":"test"}]}'Actual: HTTP/1.1 500 Internal Server Error, Content-Length: 0 — empty body, no error JSON, no hint.
Root Cause (source walk, v3.8.52)
open-sse/executors/index.ts(getExecutor()),SEARCH_PROVIDERSguard (added for fix(providers): reject search providers as chat/combo targets or route via search API #10274):
if (SEARCH_PROVIDERS.has(provider)) {
const t = Error(`Provider "${provider}" is a search provider and does not support chat completions; use the /v1/search endpoint instead.`);
throw t.status = 400, t;
}-
src/app/api/v1/chat/completions/route.ts— the earlyassertRuntimeModelProviderAvailablecatch does honore.status, buthandleChatcallsgetExecutor(provider)much deeper; when the guard fires there, the route's outercatchonly doeslease.release(); throw e;. -
Next.js App Route handler fallback (compiled
app-routetemplate chunk, module404857):catch(t) { … await sendResponse(res, new Response(null, { status: 500 })); }→ 500 +Content-Length: 0.err.statusand the message are silently discarded.
So the #10274 guard works (the key is no longer leaked to api.openai.com — that part is fixed), but its diagnostic value is destroyed by the last hop.
Expected Behavior
One of:
- Minimal: the route's top-level catch honors
err.status(like theassertRuntimeModelProviderAvailablecatch a few lines above already does) and returns a JSON error body:
{"error":{"message":"Provider \"anysearch-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.","type":"invalid_request_error","code":"search_provider_not_chat"}}- Nicer (what fix(providers): reject search providers as chat/combo targets or route via search API #10274's title already suggested): route via search API — detect the search-provider error in the chat route and transparently dispatch to
/api/v1/search, returning the results as a normal chat completion. I prototyped this locally as a hot patch on the compiled route chunk (extract last user text → POST internal/api/v1/search→ synthesizechat.completion, incl.stream:trueSSE frames); it works foranysearch-search/webandparallel-search/web, stream and non-stream.
Actual Behavior
500 with an empty body for every *-search/* chat request. From the client's perspective the provider just "randomly errors"; the connection test (which hits /v1/search) stays green, which makes it even more confusing. This exact symptom cost me a full debugging session before I decompiled the chunk to find the swallowed 400.
Note: same observable symptom as #17132 (500 empty body) but different root cause (guard rethrow vs quota-share crash), so filed separately.
Test Impact
- Unit test:
POST /v1/chat/completionswithmodel: "anysearch-search/web"assertsstatus === 400(or 200 + synthesized completion under option 2) and a non-empty JSON body containing"/v1/search". - Regression: the existing
assertRuntimeModelProviderAvailable400 path keeps working.
Environment
- Affected: every provider in
SEARCH_PROVIDERS(anysearch-search, parallel-search, tavily-search, exa-search, context7, …) called via/v1/chat/completions
Related
- fix(providers): reject search providers as chat/combo targets or route via search API #10274 — original fix (guard added); this issue is the leftover "400 becomes 500 empty body" hop
- fix(sse): a quota-share block crashes /v1/chat/completions (500, empty body) and makes a combo skip the connection's other steps (502 "Cannot read properties of undefined (reading 'headers')") #17132 — same 500-empty-body symptom, different cause
- fix(api): provider mini-playground sends x-connection-id but /api/v1/search ignores it — always tests the default provider #13245 — related:
/api/v1/searchignoringx-connection-id
Activity
diegosouzapw commentedon Oct 22, 2026
We compared #17859, #17860, #17861 and #17862 with this report. They describe the same search-provider guard reaching the chat route as an empty HTTP 500, so this issue will remain the canonical tracker.
The acceptance target is a sanitized, non-empty JSON error with HTTP 400 and the /v1/search hint, while preserving cleanup and existing error paths. Transparently converting chat requests into searches is a separate optional behaviour, not required to fix the lost error response. No fix is claimed by this consolidation.
diegosouzapw commentedon Oct 22, 2026
Confirmed on release/v3.8.53 with a route-level test: POST /v1/chat/completions for anysearch-search/web (stream and non-stream) makes the route handler reject with the #10274 guard error (status=400) instead of returning a Response, so Next serves 500 with an empty body. The throw comes from getExecutor() via chatCore's unguarded resolveExecutorWithProxy(); the route's outer catch only releases the admission lease and rethrows.
We also found a second loss: even after the 400 is converted to an error body, sanitizeErrorMessage rewrites "/v1/search endpoint instead." to "use the ", so the hint needs rewording (for example "use the search API (POST /v1/search) instead.", which survives sanitization).
Planned fix: return a sanitized 4xx error result from chatCore when executor resolution throws an error with a 4xx status (also covers the 410 retirement errors), reword the guard message, and add the regression test. Transparent chat-to-search routing is out of scope here. #17859-#17862 are duplicates of this issue.
fix(api): reject search-only providers once in chatCore for every cha…
refactor(sse): move the search-provider chat rejection into a chatCor…