fix(web): reject credentialed search redirects

This commit is contained in:
Tianyi Cui
2026-07-19 21:32:23 +08:00
parent f84092045c
commit 5e0e4b2401
10 changed files with 135 additions and 6 deletions

View File

@@ -23,7 +23,7 @@ This is an **implementation** package: it registers a provider into `ctx.web`, i
## Mapping
`content` ← `choices[0].message.content` (the generated answer). `sources[]` prefers the structured `search_results[]` (`url`, `title`, `snippet`, `publishedAt` ← `date`), falling back to the URL-only `citations[]` array only when `search_results` is absent — those sources carry just a `url`, which is why `title`/`snippet`/`publishedAt` are optional on the seam. Provider failures surface as `WebError` `WEB_PROVIDER_ERROR`; an aborted request surfaces as `WEB_ABORTED`. Perplexity has no result-count control, so `maxResults` is enforced by the seam (truncating `sources[]` and setting `truncated`).
`content` ← `choices[0].message.content` (the generated answer). `sources[]` prefers the structured `search_results[]` (`url`, `title`, `snippet`, `publishedAt` ← `date`), falling back to the URL-only `citations[]` array only when `search_results` is absent — those sources carry just a `url`, which is why `title`/`snippet`/`publishedAt` are optional on the seam. Provider failures surface as `WebError` `WEB_PROVIDER_ERROR`; an aborted request surfaces as `WEB_ABORTED`. HTTP redirects are rejected before the `Location` target is contacted and surface as `WEB_PROVIDER_ERROR`. Perplexity has no result-count control, so `maxResults` is enforced by the seam (truncating `sources[]` and setting `truncated`).
## Model Experience

View File

@@ -82,7 +82,7 @@ export function mapPerplexityResponse(response: PerplexityResponse): WebSearchRe
}
}
/** The Perplexity-backed search provider. */
/** The Perplexity-backed search provider; HTTP redirects fail as `WEB_PROVIDER_ERROR`. */
export class PerplexitySearchProvider implements WebSearchProvider {
readonly id = PERPLEXITY_PROVIDER_ID
@@ -103,6 +103,7 @@ export class PerplexitySearchProvider implements WebSearchProvider {
try {
response = await fetch(`${this.options.baseURL}/chat/completions`, {
method: 'POST',
redirect: 'error',
headers: {
'authorization': `Bearer ${this.options.apiKey}`,
'content-type': 'application/json',

View File

@@ -90,6 +90,7 @@ describe('PerplexitySearchProvider request mapping', () => {
await new PerplexitySearchProvider(options).search({ query: 'hello' })
const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]
expect(url).toBe('https://api.perplexity.test/chat/completions')
expect(init).toMatchObject({ method: 'POST', redirect: 'error' })
expect((init.headers as Record<string, string>)['authorization']).toBe('Bearer pplx-key')
expect(JSON.parse(init.body as string)).toEqual({ model: 'sonar', max_tokens: 1024, messages: [{ role: 'user', content: 'hello' }] })
})