web_fetch fixes
This commit is contained in:
+54
-9
@@ -1765,6 +1765,40 @@ function bareWebResolveFetch(ctx) {
|
|||||||
return null
|
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} a
|
||||||
* @param {AbortSignal | null | undefined} b
|
* @param {AbortSignal | null | undefined} b
|
||||||
@@ -1774,18 +1808,23 @@ function bareWebUnionAbort(a, b) {
|
|||||||
if (!b) return a
|
if (!b) return a
|
||||||
if (typeof AbortSignal.any === 'function') return AbortSignal.any([a, b])
|
if (typeof AbortSignal.any === 'function') return AbortSignal.any([a, b])
|
||||||
const c = new AbortController()
|
const c = new AbortController()
|
||||||
const fn = () => {
|
/**
|
||||||
|
* @param {AbortSignal} sig
|
||||||
|
*/
|
||||||
|
const forward = (sig) => {
|
||||||
try {
|
try {
|
||||||
c.abort()
|
c.abort(bareWebSignalAbortReason(sig))
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore — second source may fire after controller already aborted */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (a.aborted) fn()
|
const as = /** @type {AbortSignal} */ (a)
|
||||||
else a.addEventListener('abort', fn, { once: true })
|
const bs = /** @type {AbortSignal} */ (b)
|
||||||
if (b.aborted) fn()
|
if (as.aborted) forward(as)
|
||||||
else b.addEventListener('abort', fn, { once: true })
|
else as.addEventListener('abort', () => forward(as), { once: true })
|
||||||
|
if (bs.aborted) forward(bs)
|
||||||
|
else bs.addEventListener('abort', () => forward(bs), { once: true })
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
@@ -2109,7 +2148,7 @@ async function bareWebRunTool(o) {
|
|||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
controller.abort()
|
controller.abort(bareWebTimeoutAbortReason(timeoutMs))
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
@@ -2152,7 +2191,13 @@ async function bareWebRunTool(o) {
|
|||||||
res = await fetchFn(currentUrl, init)
|
res = await fetchFn(currentUrl, init)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
clearTimeout(timer)
|
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 {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: 'web_fetch: request failed: ' + msg.slice(0, 400),
|
error: 'web_fetch: request failed: ' + msg.slice(0, 400),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schema": 2,
|
"schema": 2,
|
||||||
"profileId": "bare-os-posix-like",
|
"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.",
|
"note": "Sparse POSIX Issue 7 coverage hints for /bin utilities. Omitted command names are not yet profiled here.",
|
||||||
"commandIndex": [
|
"commandIndex": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schema": 1,
|
"schema": 1,
|
||||||
"atMs": 1776836314634,
|
"atMs": 1776836750441,
|
||||||
"commands": [
|
"commands": [
|
||||||
"agent",
|
"agent",
|
||||||
"arch",
|
"arch",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -100,6 +100,40 @@ function bareWebResolveFetch(ctx) {
|
|||||||
return null
|
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} a
|
||||||
* @param {AbortSignal | null | undefined} b
|
* @param {AbortSignal | null | undefined} b
|
||||||
@@ -109,18 +143,23 @@ function bareWebUnionAbort(a, b) {
|
|||||||
if (!b) return a
|
if (!b) return a
|
||||||
if (typeof AbortSignal.any === 'function') return AbortSignal.any([a, b])
|
if (typeof AbortSignal.any === 'function') return AbortSignal.any([a, b])
|
||||||
const c = new AbortController()
|
const c = new AbortController()
|
||||||
const fn = () => {
|
/**
|
||||||
|
* @param {AbortSignal} sig
|
||||||
|
*/
|
||||||
|
const forward = (sig) => {
|
||||||
try {
|
try {
|
||||||
c.abort()
|
c.abort(bareWebSignalAbortReason(sig))
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore — second source may fire after controller already aborted */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (a.aborted) fn()
|
const as = /** @type {AbortSignal} */ (a)
|
||||||
else a.addEventListener('abort', fn, { once: true })
|
const bs = /** @type {AbortSignal} */ (b)
|
||||||
if (b.aborted) fn()
|
if (as.aborted) forward(as)
|
||||||
else b.addEventListener('abort', fn, { once: true })
|
else as.addEventListener('abort', () => forward(as), { once: true })
|
||||||
|
if (bs.aborted) forward(bs)
|
||||||
|
else bs.addEventListener('abort', () => forward(bs), { once: true })
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
@@ -444,7 +483,7 @@ async function bareWebRunTool(o) {
|
|||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
controller.abort()
|
controller.abort(bareWebTimeoutAbortReason(timeoutMs))
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
@@ -487,7 +526,13 @@ async function bareWebRunTool(o) {
|
|||||||
res = await fetchFn(currentUrl, init)
|
res = await fetchFn(currentUrl, init)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
clearTimeout(timer)
|
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 {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: 'web_fetch: request failed: ' + msg.slice(0, 400),
|
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')
|
withCause.cause = new Error('inner')
|
||||||
t.ok(fmt(withCause).includes('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
|
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} a
|
||||||
* @param {AbortSignal | null | undefined} b
|
* @param {AbortSignal | null | undefined} b
|
||||||
@@ -1774,18 +1808,23 @@ function bareWebUnionAbort(a, b) {
|
|||||||
if (!b) return a
|
if (!b) return a
|
||||||
if (typeof AbortSignal.any === 'function') return AbortSignal.any([a, b])
|
if (typeof AbortSignal.any === 'function') return AbortSignal.any([a, b])
|
||||||
const c = new AbortController()
|
const c = new AbortController()
|
||||||
const fn = () => {
|
/**
|
||||||
|
* @param {AbortSignal} sig
|
||||||
|
*/
|
||||||
|
const forward = (sig) => {
|
||||||
try {
|
try {
|
||||||
c.abort()
|
c.abort(bareWebSignalAbortReason(sig))
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore — second source may fire after controller already aborted */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (a.aborted) fn()
|
const as = /** @type {AbortSignal} */ (a)
|
||||||
else a.addEventListener('abort', fn, { once: true })
|
const bs = /** @type {AbortSignal} */ (b)
|
||||||
if (b.aborted) fn()
|
if (as.aborted) forward(as)
|
||||||
else b.addEventListener('abort', fn, { once: true })
|
else as.addEventListener('abort', () => forward(as), { once: true })
|
||||||
|
if (bs.aborted) forward(bs)
|
||||||
|
else bs.addEventListener('abort', () => forward(bs), { once: true })
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
@@ -2109,7 +2148,7 @@ async function bareWebRunTool(o) {
|
|||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
controller.abort()
|
controller.abort(bareWebTimeoutAbortReason(timeoutMs))
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
@@ -2152,7 +2191,13 @@ async function bareWebRunTool(o) {
|
|||||||
res = await fetchFn(currentUrl, init)
|
res = await fetchFn(currentUrl, init)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
clearTimeout(timer)
|
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 {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: 'web_fetch: request failed: ' + msg.slice(0, 400),
|
error: 'web_fetch: request failed: ' + msg.slice(0, 400),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schema": 2,
|
"schema": 2,
|
||||||
"profileId": "bare-os-posix-like",
|
"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.",
|
"note": "Sparse POSIX Issue 7 coverage hints for /bin utilities. Omitted command names are not yet profiled here.",
|
||||||
"commandIndex": [
|
"commandIndex": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schema": 1,
|
"schema": 1,
|
||||||
"atMs": 1776836314634,
|
"atMs": 1776836750441,
|
||||||
"commands": [
|
"commands": [
|
||||||
"agent",
|
"agent",
|
||||||
"arch",
|
"arch",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -112,6 +112,32 @@ async function main() {
|
|||||||
return
|
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({
|
const result = await bareWebRunToolFn({
|
||||||
ctx: { httpFetch: fetch },
|
ctx: { httpFetch: fetch },
|
||||||
url: 'https://example.com/',
|
url: 'https://example.com/',
|
||||||
|
|||||||
Reference in New Issue
Block a user