Repository navigation
Answer failed exchanges in monitor() with a network error, as for malformed URLs #4
Description
challenge / use case
monitor answers a request stating a malformed URL with Response.error(), but logs a failed exchange to
logger.warn and then rethrows it unchanged:
const client: Fetch = async () => Promise.reject(
new TypeError("fetch failed", { cause: Object.assign(new Error(), { code: "ETIMEDOUT" }) })
);
const send = createFetch(monitor(console), transport(client));
await send("not a url"); // resolves to Response.error()
await send("https://unreachable.test/"); // rejects with the TypeErrorCallers thus have to handle two failure channels for what is, from their side, the same outcome: a request that
yields no response from the origin. A check on ok covers malformed URLs and unsuccessful statuses, but a transport
failure escapes it.
Downstream, @metreeca/pipe-url's fetch() skips responses that are not ok, so a malformed URL or a 503 costs
one request, while a connection timeout aborts the whole feed and every request listed after the failing one is never
sent.
proposal
A failed exchange is logged as it is today and then answered with a network error Response, as a malformed URL
already is, so that every request yielding no response from the origin surfaces through a single channel, a response
whose ok is false, whatever the reason it failed.
- The Exchange fails entry and
@returnsstate that the failure is answered with a network error, not rethrown - The busy status still turns
falseonce a failed exchange is answered, as it does today when it is rethrown - Middlewares declared below
monitorkeep seeing rejections, so a bounded retry for transient failures (timeouts,
resets) remains possible alongsidethrottleandtimeout
alternatives
-
Absorbing middleware in the chain: declared ahead of
monitor, catching rejections and answering them with
Response.error(); works today, but every consumer has to know about it and declare itconst absorb: Middleware = fetch => async (input, init) => fetch(input, init).catch(() => Response.error()); const send = createFetch(absorb, monitor(console), transport(client));
-
Local handling in
@metreeca/pipe-url: catching rejections aroundsendin itsfetch()loop; fixes the feed
abort, but leaves the two channels in place for every othermonitorconsumer
notes
- The change breaks the documented contract of
monitor, so it belongs to a new release line - The module overview states that exchanges are relayed untouched, which already does not hold for malformed URLs; the
sentence has to be revised either way @metreeca/pipe-url'sfetch()documents that it throws whatever the exchange reports while connecting to a
resource; its@throwsand the skipped-response note need a matching updateResponse.error()carries no reason, so the error message and cause reach the log alone; callers needing them
would have to read the log or layer their own middleware belowmonitor
Activity
feat(monitor)!: answer failed exchanges with a network error
knoan commentedon Oct 27, 2026
Solution
monitor()now answers an exchange failing without a response with a network errorResponse(Response.error()) instead of rejecting, matching the handling of malformed URLs, so callers handle transport and server failures through a single channel.Changes
logger.warn, with the resource and the reason stated by the error cause, then answered with a network errormonitor()still receive failures as rejections, so retrying middlewares placed closer to the transport keep workingCHANGELOG.mdrecords the change under0.6.0(breaking, absorbed by the openrelease/0.6line)Related
release/0.6