Skip to content

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

@manjughosa

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

  1. Activate any search provider (e.g. AnySearch) in Settings → Providers.
  2. 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)

  1. open-sse/executors/index.ts (getExecutor()), SEARCH_PROVIDERS guard (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;
}
  1. src/app/api/v1/chat/completions/route.ts — the early assertRuntimeModelProviderAvailable catch does honor e.status, but handleChat calls getExecutor(provider) much deeper; when the guard fires there, the route's outer catch only does lease.release(); throw e;.

  2. Next.js App Route handler fallback (compiled app-route template chunk, module 404857): catch(t) { … await sendResponse(res, new Response(null, { status: 500 })); } → 500 + Content-Length: 0. err.status and 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:

  1. Minimal: the route's top-level catch honors err.status (like the assertRuntimeModelProviderAvailable catch 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"}}
  1. 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 → synthesize chat.completion, incl. stream:true SSE frames); it works for anysearch-search/web and parallel-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/completions with model: "anysearch-search/web" asserts status === 400 (or 200 + synthesized completion under option 2) and a non-empty JSON body containing "/v1/search".
  • Regression: the existing assertRuntimeModelProviderAvailable 400 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

Activity

  1. diegosouzapw commented on Oct 22, 2026

    @diegosouzapw
    Collaborator

    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.

  2. diegosouzapw commented on Oct 22, 2026

    @diegosouzapw
    Collaborator

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    apibugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions