fix(api): POST /v1/search names unknown providers instead of opaque 400 (#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).
This commit is contained in:
Markus Hartung
2026-08-20 20:37:22 -03:00
parent bc9090ba65
commit 4ec080dc19
8 changed files with 138 additions and 28 deletions

View File

@@ -56,6 +56,19 @@ export function isValidationFailure<TData>(
return validation.success === false;
}
/**
* Build a human-readable 400 message from a validation failure, naming the
* first offending field instead of the generic "Invalid request" (#10849).
* Intended for routes that reply with a single message string (e.g.
* `errorResponse()`) rather than the full `{ message, details }` envelope
* returned by `validatedJsonBody()`.
*/
export function formatValidationMessage(error: ValidationErrorPayload): string {
const [first] = error.details;
if (!first) return error.message;
return first.field ? `${first.field}: ${first.message}` : first.message;
}
/**
* Result of attempting to parse and validate a JSON body against a Zod schema.
*

View File

@@ -568,27 +568,16 @@ export const v1SearchSchema = z
.trim()
.min(1, "Query is required")
.max(500, "Query must be 500 characters or fewer"),
provider: z
.enum([
"serper-search",
"brave-search",
"perplexity-search",
"exa-search",
"tavily-search",
"firecrawl",
"google-pse-search",
"linkup-search",
"ollama-search",
"searchapi-search",
"youcom-search",
"searxng-search",
"zai-search",
"jina-search",
"jina-ai",
"jina",
"duckduckgo-free",
])
.optional(),
// Not a z.enum: the runtime catalog (SEARCH_PROVIDERS + SEARCH_PROVIDER_ALIASES in
// open-sse/config/searchRegistry.ts) is the source of truth via resolveSearchProvider(),
// which already returns a named "Unknown search provider: <id>" error for bad ids (see
// src/app/api/v1/search/route.ts). A hard-coded enum here would 400 before that check
// ever runs, hiding the informative message behind a generic Zod failure (#10849).
// Known catalog ids as of this writing: serper-search, brave-search, perplexity-search,
// exa-search, tavily-search, firecrawl, google-pse-search, linkup-search, ollama-search,
// searchapi-search, youcom-search, searxng-search, zai-search, jina-search, jina-ai,
// jina, duckduckgo-free (plus short aliases resolved by SEARCH_PROVIDER_ALIASES).
provider: z.string().min(1).optional(),
max_results: z.coerce.number().int().min(1).max(100).default(5),
search_type: z.enum(["web", "news"]).default("web"),
offset: z.coerce.number().int().min(0).default(0),