fix(api): POST /v1/search names unknown providers instead of opaque 400 (#10849) - #10919
Merged
Merged
Conversation
…00 (#10849) v1SearchSchema.provider was a hard-coded z.enum that rejected any id outside its list before the route's own resolveSearchProvider() check ever ran, so unknown/short-alias provider ids (grok, brave, serper, ...) always surfaced a generic "Invalid request" instead of the informative "Unknown search provider: <id>" message. Relax the schema to a free-form string and let resolveSearchProvider() own runtime validation (as it already did for ids that passed the enum). Also extend SEARCH_PROVIDER_ALIASES with short-form aliases mirroring the existing jina/jina-ai pattern (brave, serper, perplexity, exa, tavily, google-pse, linkup, ollama, searchapi, youcom, searxng, zai, duckduckgo), and surface the first Zod validation issue's field name instead of the generic message for other still-invalid fields (e.g. search_type).
muhamadgalihsaputra
pushed a commit
to niyatna/NiyatnaRoute
that referenced
this pull request
Sep 27, 2026
…rch-provider-400 fix(api): POST /v1/search names unknown providers instead of opaque 400 (diegosouzapw#10849)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #10849
Root cause
v1SearchSchema.provider(src/shared/validation/schemas/apiV1.ts) was a hard-codedz.enum([...17 catalog ids]). Any provider id outside that literal list (e.g.grok, or short aliases likebrave/serper) failed Zod validation insidevalidateBody()before the route's own, better-designed check (resolveSearchProvider()inopen-sse/config/searchRegistry.ts) ever ran.validateBody()also always hard-codesmessage: "Invalid request"on failure, with the real Zod issue only indetails[]— butroute.tswas building the 400 response fromvalidation.error.messagealone, droppingdetailsand hiding which field/value actually failed.Downstream,
resolveSearchProvider()already returnsnullfor unknown ids and the route already repliesUnknown search provider: <id>— but that branch was unreachable for anything outside the enum. Additionally,SEARCH_PROVIDER_ALIASESonly mappedjina-ai/jina→jina-search; no short aliases existed for the other providers (brave→brave-search, etc.), so even after relaxing the enum those ids would still fail with an unnamed error.Fix
src/shared/validation/schemas/apiV1.ts:v1SearchSchema.provideris nowz.string().min(1).optional()(documented with a comment listing the known catalog ids) instead of a hard-coded enum, soresolveSearchProvider()is the single runtime source of truth for provider validity.open-sse/config/searchRegistry.ts: extendedSEARCH_PROVIDER_ALIASESwith short-form aliases mirroring the existingjina/jina-aipattern —brave,serper,perplexity,exa,tavily,google-pse,linkup,ollama,searchapi,youcom,searxng,zai,duckduckgo.src/shared/validation/helpers.ts: addedformatValidationMessage(), which builds a"<field>: <message>"string from the first Zod issue instead of the generic"Invalid request", for routes (like/v1/search) that reply with a single message string viaerrorResponse().src/app/api/v1/search/route.ts: usesformatValidationMessage()when schema validation fails, so a genuinely bad field (e.g.search_type: "bogus") also gets a named message instead of an opaque one.tests/unit/firecrawl-search.test.ts,tests/unit/search-registry.test.ts) that encoded the old (buggy) schema-level-rejection contract — they now assert the corrected contract: the schema accepts any non-empty string, andresolveSearchProvider()is the runtime gate for unknown/legacy ids.Regression test (TDD)
tests/unit/search-provider-opaque-400-10849.test.ts— reproduces the bug against the unfixed code (confirmed RED: bothprovider: "grok"andprovider: "brave"returned the opaque"Invalid request"message), then confirms GREEN after the fix:Unknown search provider: grokbrave→ resolves like the existingjinaaliases (no opaque 400)search_type: "bogus") → field-named message, not opaqueRun:
node --import tsx/esm --test tests/unit/search-provider-opaque-400-10849.test.ts→ 3/3 passing.Validation
npm run typecheck:core— cleannode scripts/check/check-complexity.mjs/check-cognitive-complexity.mjs/check-file-size.mjs— all OK (no regression vs baseline)node scripts/check/check-changelog-integrity.mjs— OKnpx eslint --suppressions-location config/quality/eslint-suppressions.json <changed files>— cleantests/unit/search-route.test.ts,tests/unit/search-registry.test.ts,tests/unit/firecrawl-search.test.ts,tests/unit/jina-complete-provider.test.ts,tests/unit/9201-search-proxy-bypass.test.ts— all passing after the fix (2 pre-existing schema-level assertions updated to the corrected contract, see above)tests/unit/hard-session-lease-bypass-inventory.test.tsfails on this branch, but the failure is unrelated pre-existing drift:git diff origin/release/v3.8.50 -- tests/unit/hard-session-lease-bypass-inventory.test.tsis empty (the test file is untouched by this PR) and thesrc/app/api/v1/search/route.tscall-site count in the failing assertion is identical (2) betweenactualandexpected— the mismatch is entirely in unrelated routes (classify,images/edits,segment,videos/generations,geminiWeb).