web_fetch fixes
This commit is contained in:
+54
-9
@@ -1765,6 +1765,40 @@ function bareWebResolveFetch(ctx) {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* bundled bare-fetch rejects the fetch promise with `signal.reason` on abort.
|
||||
* `controller.abort()` with no argument sets `reason === undefined`, so callers
|
||||
* see `promise_rejected_with_undefined`. Always pass an explicit reason.
|
||||
* @param {number} timeoutMs
|
||||
*/
|
||||
function bareWebTimeoutAbortReason(timeoutMs) {
|
||||
const msg = 'web_fetch: exceeded ' + timeoutMs + 'ms (timeout)'
|
||||
try {
|
||||
if (typeof DOMException === 'function')
|
||||
return new DOMException(msg, 'TimeoutError')
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const e = new Error(msg)
|
||||
e.name = 'TimeoutError'
|
||||
return e
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbortSignal} sig
|
||||
*/
|
||||
function bareWebSignalAbortReason(sig) {
|
||||
try {
|
||||
const r = /** @type {{ reason?: unknown }} */ (sig).reason
|
||||
if (r !== undefined && r !== null) return r
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const e = new Error('web_fetch aborted (signal)')
|
||||
e.name = 'AbortError'
|
||||
return e
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbortSignal | null | undefined} a
|
||||
* @param {AbortSignal | null | undefined} b
|
||||
@@ -1774,18 +1808,23 @@ function bareWebUnionAbort(a, b) {
|
||||
if (!b) return a
|
||||
if (typeof AbortSignal.any === 'function') return AbortSignal.any([a, b])
|
||||
const c = new AbortController()
|
||||
const fn = () => {
|
||||
/**
|
||||
* @param {AbortSignal} sig
|
||||
*/
|
||||
const forward = (sig) => {
|
||||
try {
|
||||
c.abort()
|
||||
c.abort(bareWebSignalAbortReason(sig))
|
||||
} catch {
|
||||
/* ignore */
|
||||
/* ignore — second source may fire after controller already aborted */
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (a.aborted) fn()
|
||||
else a.addEventListener('abort', fn, { once: true })
|
||||
if (b.aborted) fn()
|
||||
else b.addEventListener('abort', fn, { once: true })
|
||||
const as = /** @type {AbortSignal} */ (a)
|
||||
const bs = /** @type {AbortSignal} */ (b)
|
||||
if (as.aborted) forward(as)
|
||||
else as.addEventListener('abort', () => forward(as), { once: true })
|
||||
if (bs.aborted) forward(bs)
|
||||
else bs.addEventListener('abort', () => forward(bs), { once: true })
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
@@ -2109,7 +2148,7 @@ async function bareWebRunTool(o) {
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => {
|
||||
try {
|
||||
controller.abort()
|
||||
controller.abort(bareWebTimeoutAbortReason(timeoutMs))
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
@@ -2152,7 +2191,13 @@ async function bareWebRunTool(o) {
|
||||
res = await fetchFn(currentUrl, init)
|
||||
} catch (e) {
|
||||
clearTimeout(timer)
|
||||
const msg = bareWebFmtErr(e)
|
||||
const msg = bareWebFmtErr(
|
||||
e === undefined
|
||||
? new Error(
|
||||
'web_fetch: fetch rejected with undefined (bare-fetch uses signal.reason; upstream abort() had no reason)'
|
||||
)
|
||||
: e
|
||||
)
|
||||
return {
|
||||
ok: false,
|
||||
error: 'web_fetch: request failed: ' + msg.slice(0, 400),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schema": 2,
|
||||
"profileId": "bare-os-posix-like",
|
||||
"generatedAt": "2026-04-22T05:38:34.635Z",
|
||||
"generatedAt": "2026-04-22T05:45:50.442Z",
|
||||
"note": "Sparse POSIX Issue 7 coverage hints for /bin utilities. Omitted command names are not yet profiled here.",
|
||||
"commandIndex": [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"atMs": 1776836314634,
|
||||
"atMs": 1776836750441,
|
||||
"commands": [
|
||||
"agent",
|
||||
"arch",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -100,6 +100,40 @@ function bareWebResolveFetch(ctx) {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* bundled bare-fetch rejects the fetch promise with `signal.reason` on abort.
|
||||
* `controller.abort()` with no argument sets `reason === undefined`, so callers
|
||||
* see `promise_rejected_with_undefined`. Always pass an explicit reason.
|
||||
* @param {number} timeoutMs
|
||||
*/
|
||||
function bareWebTimeoutAbortReason(timeoutMs) {
|
||||
const msg = 'web_fetch: exceeded ' + timeoutMs + 'ms (timeout)'
|
||||
try {
|
||||
if (typeof DOMException === 'function')
|
||||
return new DOMException(msg, 'TimeoutError')
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const e = new Error(msg)
|
||||
e.name = 'TimeoutError'
|
||||
return e
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbortSignal} sig
|
||||
*/
|
||||
function bareWebSignalAbortReason(sig) {
|
||||
try {
|
||||
const r = /** @type {{ reason?: unknown }} */ (sig).reason
|
||||
if (r !== undefined && r !== null) return r
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const e = new Error('web_fetch aborted (signal)')
|
||||
e.name = 'AbortError'
|
||||
return e
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbortSignal | null | undefined} a
|
||||
* @param {AbortSignal | null | undefined} b
|
||||
@@ -109,18 +143,23 @@ function bareWebUnionAbort(a, b) {
|
||||
if (!b) return a
|
||||
if (typeof AbortSignal.any === 'function') return AbortSignal.any([a, b])
|
||||
const c = new AbortController()
|
||||
const fn = () => {
|
||||
/**
|
||||
* @param {AbortSignal} sig
|
||||
*/
|
||||
const forward = (sig) => {
|
||||
try {
|
||||
c.abort()
|
||||
c.abort(bareWebSignalAbortReason(sig))
|
||||
} catch {
|
||||
/* ignore */
|
||||
/* ignore — second source may fire after controller already aborted */
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (a.aborted) fn()
|
||||
else a.addEventListener('abort', fn, { once: true })
|
||||
if (b.aborted) fn()
|
||||
else b.addEventListener('abort', fn, { once: true })
|
||||
const as = /** @type {AbortSignal} */ (a)
|
||||
const bs = /** @type {AbortSignal} */ (b)
|
||||
if (as.aborted) forward(as)
|
||||
else as.addEventListener('abort', () => forward(as), { once: true })
|
||||
if (bs.aborted) forward(bs)
|
||||
else bs.addEventListener('abort', () => forward(bs), { once: true })
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
@@ -444,7 +483,7 @@ async function bareWebRunTool(o) {
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => {
|
||||
try {
|
||||
controller.abort()
|
||||
controller.abort(bareWebTimeoutAbortReason(timeoutMs))
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
@@ -487,7 +526,13 @@ async function bareWebRunTool(o) {
|
||||
res = await fetchFn(currentUrl, init)
|
||||
} catch (e) {
|
||||
clearTimeout(timer)
|
||||
const msg = bareWebFmtErr(e)
|
||||
const msg = bareWebFmtErr(
|
||||
e === undefined
|
||||
? new Error(
|
||||
'web_fetch: fetch rejected with undefined (bare-fetch uses signal.reason; upstream abort() had no reason)'
|
||||
)
|
||||
: e
|
||||
)
|
||||
return {
|
||||
ok: false,
|
||||
error: 'web_fetch: request failed: ' + msg.slice(0, 400),
|
||||
|
||||
@@ -156,3 +156,38 @@ test('bareWebFmtErr null/undefined and node-like errors', async (t) => {
|
||||
withCause.cause = new Error('inner')
|
||||
t.ok(fmt(withCause).includes('inner'))
|
||||
})
|
||||
|
||||
test('bareWebRunTool timeout passes abort reason (matches bare-fetch contract)', async (t) => {
|
||||
/** Simulates bare-fetch: rejects with `signal.reason` when aborted. */
|
||||
const fetchFn = async (_url, init) => {
|
||||
const sig = init && /** @type {{ signal?: AbortSignal }} */ (init).signal
|
||||
if (!sig)
|
||||
return new Promise(() => {
|
||||
/* hang */
|
||||
})
|
||||
return new Promise((_res, rej) => {
|
||||
if (sig.aborted) {
|
||||
rej(/** @type {AbortSignal} */ (sig).reason)
|
||||
return
|
||||
}
|
||||
sig.addEventListener(
|
||||
'abort',
|
||||
() => rej(/** @type {AbortSignal} */ (sig).reason),
|
||||
{ once: true }
|
||||
)
|
||||
})
|
||||
}
|
||||
const s = loadSandbox({ fetch: fetchFn })
|
||||
const run = /** @type {typeof bareWebRunTool} */ (s.bareWebRunTool)
|
||||
const out = await run({
|
||||
ctx: { httpFetch: fetchFn },
|
||||
url: 'https://slow.example/hang',
|
||||
format: 'meta',
|
||||
timeout_ms: 25
|
||||
})
|
||||
t.absent(out.ok)
|
||||
const err = String(/** @type {{ error?: string }} */ (out).error || '')
|
||||
t.ok(err.includes('web_fetch: request failed'))
|
||||
t.ok(err.includes('timeout') || err.includes('TimeoutError') || err.includes('exceeded'))
|
||||
t.absent(err.includes('promise_rejected_with_undefined'))
|
||||
})
|
||||
|
||||
@@ -1765,6 +1765,40 @@ function bareWebResolveFetch(ctx) {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* bundled bare-fetch rejects the fetch promise with `signal.reason` on abort.
|
||||
* `controller.abort()` with no argument sets `reason === undefined`, so callers
|
||||
* see `promise_rejected_with_undefined`. Always pass an explicit reason.
|
||||
* @param {number} timeoutMs
|
||||
*/
|
||||
function bareWebTimeoutAbortReason(timeoutMs) {
|
||||
const msg = 'web_fetch: exceeded ' + timeoutMs + 'ms (timeout)'
|
||||
try {
|
||||
if (typeof DOMException === 'function')
|
||||
return new DOMException(msg, 'TimeoutError')
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const e = new Error(msg)
|
||||
e.name = 'TimeoutError'
|
||||
return e
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbortSignal} sig
|
||||
*/
|
||||
function bareWebSignalAbortReason(sig) {
|
||||
try {
|
||||
const r = /** @type {{ reason?: unknown }} */ (sig).reason
|
||||
if (r !== undefined && r !== null) return r
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const e = new Error('web_fetch aborted (signal)')
|
||||
e.name = 'AbortError'
|
||||
return e
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbortSignal | null | undefined} a
|
||||
* @param {AbortSignal | null | undefined} b
|
||||
@@ -1774,18 +1808,23 @@ function bareWebUnionAbort(a, b) {
|
||||
if (!b) return a
|
||||
if (typeof AbortSignal.any === 'function') return AbortSignal.any([a, b])
|
||||
const c = new AbortController()
|
||||
const fn = () => {
|
||||
/**
|
||||
* @param {AbortSignal} sig
|
||||
*/
|
||||
const forward = (sig) => {
|
||||
try {
|
||||
c.abort()
|
||||
c.abort(bareWebSignalAbortReason(sig))
|
||||
} catch {
|
||||
/* ignore */
|
||||
/* ignore — second source may fire after controller already aborted */
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (a.aborted) fn()
|
||||
else a.addEventListener('abort', fn, { once: true })
|
||||
if (b.aborted) fn()
|
||||
else b.addEventListener('abort', fn, { once: true })
|
||||
const as = /** @type {AbortSignal} */ (a)
|
||||
const bs = /** @type {AbortSignal} */ (b)
|
||||
if (as.aborted) forward(as)
|
||||
else as.addEventListener('abort', () => forward(as), { once: true })
|
||||
if (bs.aborted) forward(bs)
|
||||
else bs.addEventListener('abort', () => forward(bs), { once: true })
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
@@ -2109,7 +2148,7 @@ async function bareWebRunTool(o) {
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => {
|
||||
try {
|
||||
controller.abort()
|
||||
controller.abort(bareWebTimeoutAbortReason(timeoutMs))
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
@@ -2152,7 +2191,13 @@ async function bareWebRunTool(o) {
|
||||
res = await fetchFn(currentUrl, init)
|
||||
} catch (e) {
|
||||
clearTimeout(timer)
|
||||
const msg = bareWebFmtErr(e)
|
||||
const msg = bareWebFmtErr(
|
||||
e === undefined
|
||||
? new Error(
|
||||
'web_fetch: fetch rejected with undefined (bare-fetch uses signal.reason; upstream abort() had no reason)'
|
||||
)
|
||||
: e
|
||||
)
|
||||
return {
|
||||
ok: false,
|
||||
error: 'web_fetch: request failed: ' + msg.slice(0, 400),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schema": 2,
|
||||
"profileId": "bare-os-posix-like",
|
||||
"generatedAt": "2026-04-22T05:38:34.635Z",
|
||||
"generatedAt": "2026-04-22T05:45:50.442Z",
|
||||
"note": "Sparse POSIX Issue 7 coverage hints for /bin utilities. Omitted command names are not yet profiled here.",
|
||||
"commandIndex": [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"atMs": 1776836314634,
|
||||
"atMs": 1776836750441,
|
||||
"commands": [
|
||||
"agent",
|
||||
"arch",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -112,6 +112,32 @@ async function main() {
|
||||
return
|
||||
}
|
||||
|
||||
/** Same contract as bare-fetch: reject with `signal.reason` when aborted. */
|
||||
async function hangUntilAbort(_url, init) {
|
||||
const sig = init && init.signal
|
||||
if (!sig) return new Promise(() => {})
|
||||
return new Promise((_res, rej) => {
|
||||
if (sig.aborted) rej(sig.reason)
|
||||
else sig.addEventListener('abort', () => rej(sig.reason), { once: true })
|
||||
})
|
||||
}
|
||||
const timeoutProbe = await bareWebRunToolFn({
|
||||
ctx: { httpFetch: hangUntilAbort },
|
||||
url: 'https://hang.test/',
|
||||
format: 'meta',
|
||||
timeout_ms: 50
|
||||
})
|
||||
if (timeoutProbe.ok)
|
||||
throw new Error('[smoke-bare-web-fetch] timeout probe expected ok=false')
|
||||
const te = String(timeoutProbe.error || '')
|
||||
if (te.includes('promise_rejected_with_undefined'))
|
||||
throw new Error(
|
||||
'[smoke-bare-web-fetch] abort must carry a reason for bare-fetch: ' + te
|
||||
)
|
||||
if (!/timeout|TimeoutError|exceeded/i.test(te))
|
||||
throw new Error('[smoke-bare-web-fetch] expected timeout message: ' + te)
|
||||
console.log('[smoke-bare-web-fetch] timeout-abort contract ok')
|
||||
|
||||
const result = await bareWebRunToolFn({
|
||||
ctx: { httpFetch: fetch },
|
||||
url: 'https://example.com/',
|
||||
|
||||
Reference in New Issue
Block a user