Skip to content

Commit 2ce2257

Browse files
committed
fix(url-reader): align failover review contracts
Coverage: 94.44%
1 parent 81a7b28 commit 2ce2257

5 files changed

Lines changed: 29 additions & 23 deletions

File tree

SECURITY.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,9 @@ To allow private URL reads and private DNS-resolved targets (e.g. for internal d
6161

6262
Setting `FLARESOLVERR_URL` or `BYPARR_URL` delegates challenge-page navigation
6363
to a trusted browser service. FlareSolverr 3.5.0 and Byparr 2.1.0 were verified
64-
on 2026-07-30. Both endpoints may be configured; FlareSolverr is always primary
65-
and Byparr is the fallback only for busy or transient-unavailable acquisition.
64+
on 2026-07-30. When both endpoints are configured, this release uses
65+
FlareSolverr as the fixed primary and Byparr as the fallback only for busy or
66+
transient-unavailable acquisition.
6667
Canonical duplicate endpoints fail closed.
6768

6869
The verified `linux/amd64` images came from multi-architecture manifests

__tests__/unit/browser-solver-state.test.ts

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@ function jsonSolution(url: string, extra: Record<string, unknown> = {}) {
5555
};
5656
}
5757

58+
function createProviderConfig(
59+
provider: "flaresolverr" | "byparr",
60+
endpoint: string,
61+
): BrowserSolverConfig {
62+
const byparr = provider === "byparr";
63+
return {
64+
provider,
65+
endpoint: new URL(`${endpoint}/v1`),
66+
timeoutMs: 1000,
67+
wireTimeout: byparr ? 1 : 1000,
68+
maxConcurrentRequests: 1,
69+
maxResponseBytes: byparr ? 5 * 1024 * 1024 : 256 * 1024,
70+
};
71+
}
72+
5873
async function waitForPending(
5974
pending: http.ServerResponse[],
6075
count: number,
@@ -217,14 +232,7 @@ async function runTests() {
217232
res.writeHead(200, { "content-type": "application/json" });
218233
res.end(JSON.stringify(jsonSolution(target.href)));
219234
});
220-
const flareConfig: BrowserSolverConfig = {
221-
provider: "flaresolverr",
222-
endpoint: new URL(`${flare.url}/v1`),
223-
timeoutMs: 1000,
224-
wireTimeout: 1000,
225-
maxConcurrentRequests: 1,
226-
maxResponseBytes: 256 * 1024,
227-
};
235+
const flareConfig = createProviderConfig("flaresolverr", flare.url);
228236
const heldPrimary = acquireBrowserSolverSolution(
229237
createMockServer() as any,
230238
flareConfig,
@@ -236,14 +244,7 @@ async function runTests() {
236244
createMockServer() as any,
237245
[
238246
flareConfig,
239-
{
240-
provider: "byparr",
241-
endpoint: new URL(`${byparr.url}/v1`),
242-
timeoutMs: 1000,
243-
wireTimeout: 1,
244-
maxConcurrentRequests: 1,
245-
maxResponseBytes: 5 * 1024 * 1024,
246-
},
247+
createProviderConfig("byparr", byparr.url),
247248
],
248249
target,
249250
);

__tests__/unit/documentation.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,12 +166,16 @@ export async function runTests(): Promise<TestResult> {
166166
assert.ok(document.includes('HTTP client'));
167167
assert.ok(document.includes('disconnects'));
168168
}
169-
for (const document of [readme, configuration, security]) {
169+
for (const document of [readme, configuration]) {
170170
const normalized = document.replace(/\s+/gu, ' ');
171171
assert.ok(normalized.includes('FlareSolverr is always primary'));
172172
assert.ok(normalized.includes('150 seconds'));
173173
assert.ok(normalized.includes('unavailable'));
174174
}
175+
const normalizedSecurity = security.replace(/\s+/gu, ' ');
176+
assert.ok(normalizedSecurity.includes('FlareSolverr as the fixed primary'));
177+
assert.ok(normalizedSecurity.includes('150 seconds'));
178+
assert.ok(normalizedSecurity.includes('unavailable'));
175179
assert.ok(readme.includes('no automatic reverse failover'));
176180
assert.ok(configuration.includes('automatic reverse failover is not performed'));
177181
assert.ok(security.includes('does not retain a health score'));

__tests__/unit/url-reader.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ async function runTests() {
363363
targetGets++;
364364
}
365365
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
366-
res.end(req.method === 'HEAD' ? '' : `<h1>${scenario.name} direct</h1>`);
366+
res.end(req.method === 'HEAD' ? '' : '<h1>Dual direct fallback</h1>');
367367
});
368368
const flare = await startHttpServer((_req, res) => {
369369
flarePosts++;
@@ -384,7 +384,7 @@ async function runTests() {
384384
createMockServer() as any,
385385
target.url,
386386
);
387-
assert.ok(result.includes(`# ${scenario.name} direct`), scenario.name);
387+
assert.ok(result.includes('# Dual direct fallback'), scenario.name);
388388
assert.equal(flarePosts, 1, scenario.name);
389389
assert.equal(byparrPosts, 1, scenario.name);
390390
assert.equal(targetGets, 1, scenario.name);

src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ export const LITE_INSTANCE_INFO_TOOL: Tool = {
350350
export const LITE_READ_URL_TOOL: Tool = {
351351
name: "web_url_read",
352352
description:
353-
"Fetch URL. Converts HTML to markdown; returns explicit JSON, plain text, YAML, TOML, and XML as readable markdown; supports bounded PDF text extraction; other binary/media/archive downloads are rejected. When browser solvers are configured, mcp-searxng attempts FlareSolverr first and then Byparr only after a busy or transient-unavailable acquisition; after the final unavailable provider it uses one uncached direct read.",
353+
"Fetch URL. Converts HTML to markdown; returns explicit JSON, plain text, YAML, TOML, and XML as readable markdown; supports bounded PDF text extraction; other binary/media/archive downloads are rejected. When browser solvers are configured, mcp-searxng attempts FlareSolverr first and then Byparr only after a busy or transient-unavailable acquisition; after a final busy or unavailable provider it uses one uncached direct read.",
354354
inputSchema: {
355355
type: "object",
356356
properties: { url: { type: "string", description: "URL to fetch." } },
@@ -365,7 +365,7 @@ export const READ_URL_TOOL: Tool = {
365365
"Content-type aware: HTML is converted to markdown; JSON is pretty-printed; plain text, YAML, TOML, and XML are returned as fenced readable text. " +
366366
"PDF text extraction is supported with bounded input, output, page count, time, concurrency, and memory; OCR is not supported. " +
367367
"Binary, media, archive, and octet-stream downloads other than PDFs are intentionally rejected instead of being returned as raw bytes. " +
368-
"When the operator configures browser solvers, mcp-searxng attempts FlareSolverr first and then Byparr only after a busy or transient-unavailable acquisition; cache hits bypass acquisition and the final unavailable provider uses one uncached direct-fetch fallback. " +
368+
"When the operator configures browser solvers, mcp-searxng attempts FlareSolverr first and then Byparr only after a busy or transient-unavailable acquisition; cache hits bypass acquisition and a final busy or unavailable provider uses one uncached direct-fetch fallback. " +
369369
"Three modes: " +
370370
"(1) Full content — omit filtering params; use `startChar`/`maxLength` to paginate large pages. " +
371371
"(2) Section extraction — set `section` to return content under a specific heading. " +

0 commit comments

Comments
 (0)