Skip to content

Commit 7f0c21f

Browse files
sawork1987claude
authored andcommitted
fix: fail closed on partial engine capability discovery
Treat any unreachable SearXNG /config instance as unknown when validating explicit time ranges, with regression coverage and README guidance. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 0f0df19 commit 7f0c21f

4 files changed

Lines changed: 62 additions & 1 deletion

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ For SearXNG deployment, configuration, and troubleshooting, see
131131
- `min_score` (number, optional): Minimum relevance score from 0.0 to 1.0. Results below this score are filtered out.
132132
- `num_results` (number, optional): Maximum number of results to return, from 1 to 20. `SEARXNG_MAX_RESULTS` applies as an operator ceiling.
133133
- `categories` (string, optional): Comma-separated SearXNG categories (e.g. `"news"`, `"it,science"`). Live `/config` capabilities are aggregated across reachable instances; prefer `searxng_instance_info` `categories.common` for consistent multi-instance results. Known values are trimmed and normalized case-insensitively; unknown values are forwarded trimmed so SearXNG can ignore or honor them. If `/config` is unavailable, values are forwarded as-is with a warning. If omitted, each instance uses its server-side default.
134-
- `engines` (string, optional): Comma-separated SearXNG engine names (e.g. `"google,bing,ddg"`, `"semantic scholar"`). Live `/config` capabilities are aggregated across reachable instances; prefer `searxng_instance_info` `engines.common.enabled` for consistent multi-instance results. Known values are trimmed and normalized case-insensitively, including engines disabled by default; unknown values are forwarded trimmed so SearXNG can ignore or honor them. If `/config` is unavailable, values are forwarded as-is with a warning. If omitted, each instance uses its server-side default.
134+
- `engines` (string, optional): Comma-separated SearXNG engine names (e.g. `"google,bing,ddg"`, `"semantic scholar"`). Live `/config` capabilities are aggregated across reachable instances; prefer `searxng_instance_info` `engines.common.enabled` for consistent multi-instance results. Known values are trimmed and normalized case-insensitively, including engines disabled by default; unknown values are forwarded trimmed so SearXNG can ignore or honor them. If `/config` is unavailable, values are forwarded as-is with a warning, except when combined with `time_range`.
135+
- When `engines` and `time_range` are both provided, every configured SearXNG instance must return `/config` successfully and every selected engine must explicitly report `time_range_support=true`. If any instance is unreachable or any engine is unsupported or unknown, the request fails before `/search` to avoid a misleading empty result. Omit `time_range` or use an engine-specific query filter instead.
135136
- `response_format` (string, optional): Response format, either `"text"` for formatted agent-readable output or `"json"` for raw SearXNG JSON with filtered/sliced `results`. If omitted, `SEARXNG_DEFAULT_RESPONSE_FORMAT` applies; if unset or invalid, `text` is used. An explicit `response_format` always takes precedence.
136137
- `result_detail` (string, optional): `"full"` (the default) preserves SearXNG metadata, warnings, provenance, answers, infoboxes, corrections, and suggestions. `"compact"` returns only title, URL, and the description/content snippet for every result; compact JSON uses exactly the `title`, `url`, and `content` keys. Use full when those research signals matter.
137138
- Clients that explicitly send or auto-inject `response_format=text` continue to override the operator default. If omitted calls still return text after configuring JSON, inspect the arguments emitted by the MCP client.

__tests__/unit/instance-info.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,29 @@ async function runTests() {
137137
envManager.restore();
138138
}, results);
139139

140+
await testFunction('fails closed when any configured instance /config is unavailable', async () => {
141+
clearInstanceInfoCacheForTests();
142+
envManager.set('SEARXNG_URL', 'https://up.example.com;https://down.example.com');
143+
const mockServer = createMockServer();
144+
fetchMocker.mock(async (url, options) => {
145+
if (new URL(url.toString()).origin === 'https://down.example.com') {
146+
throw new Error('temporary outage');
147+
}
148+
return createMockFetch({ json: makeConfig() })(url, options);
149+
});
150+
151+
const support = await getEngineTimeRangeSupport(mockServer as any, ['google', 'brave']);
152+
153+
assert.deepEqual(support, {
154+
supported: [],
155+
unsupported: [],
156+
unknown: ['google', 'brave'],
157+
});
158+
159+
fetchMocker.restore();
160+
envManager.restore();
161+
}, results);
162+
140163
await testFunction('returns category names when /config categories is an array of strings', async () => {
141164
clearInstanceInfoCacheForTests();
142165
envManager.set('SEARXNG_URL', 'https://test-searx.example.com');

__tests__/unit/search.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1700,6 +1700,35 @@ async function runTests() {
17001700
envManager.restore();
17011701
}, results);
17021702

1703+
await testFunction('partial multi-instance /config failure rejects time_range before search', async () => {
1704+
clearInstanceInfoCacheForTests();
1705+
envManager.set('SEARXNG_URL', 'https://up.example.com;https://down.example.com');
1706+
1707+
const mockServer = createMockServer();
1708+
const requestedUrls: string[] = [];
1709+
fetchMocker.mock(async (url, options) => {
1710+
requestedUrls.push(url.toString());
1711+
if (new URL(url.toString()).origin === 'https://down.example.com') {
1712+
throw new Error('temporary outage');
1713+
}
1714+
return createMockFetch({ json: makeConfigWithEngines() })(url, options);
1715+
});
1716+
1717+
try {
1718+
await performWebSearch(mockServer as any, 'AI', 1, 'year', undefined, undefined, undefined, undefined, undefined, 'google');
1719+
assert.fail('Expected partial capability discovery failure to reject the search');
1720+
} catch (error: any) {
1721+
assert.ok(error.message.includes('google'), error.message);
1722+
assert.ok(error.message.includes('time_range=year'), error.message);
1723+
}
1724+
1725+
assert.equal(requestedUrls.length, 2);
1726+
assert.ok(requestedUrls.every((requestedUrl) => new URL(requestedUrl).pathname.endsWith('/config')));
1727+
1728+
fetchMocker.restore();
1729+
envManager.restore();
1730+
}, results);
1731+
17031732
await testFunction('mixed-case engines and categories normalize to canonical /config names', async () => {
17041733
clearInstanceInfoCacheForTests();
17051734
envManager.set('SEARXNG_URL', 'https://test-searx.example.com');

src/instance-info.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,14 @@ export async function getEngineTimeRangeSupport(
410410
return null;
411411
}
412412

413+
if (result.failures.length > 0) {
414+
return {
415+
supported: [],
416+
unsupported: [],
417+
unknown: [...requestedEngines],
418+
};
419+
}
420+
413421
const supported: string[] = [];
414422
const unsupported: string[] = [];
415423
const unknown: string[] = [];

0 commit comments

Comments
 (0)