Skip to content

Answer failed exchanges in monitor() with a network error, as for malformed URLs #4

Closed

Description

@knoan

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 TypeError

Callers 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 @returns state that the failure is answered with a network error, not rethrown
  • The busy status still turns false once a failed exchange is answered, as it does today when it is rethrown
  • Middlewares declared below monitor keep seeing rejections, so a bounded retry for transient failures (timeouts,
    resets) remains possible alongside throttle and timeout

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 it

    const 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 around send in its fetch() loop; fixes the feed
    abort, but leaves the two channels in place for every other monitor consumer

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's fetch() documents that it throws whatever the exchange reports while connecting to a
    resource; its @throws and the skipped-response note need a matching update
  • Response.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 below monitor

Activity

  1. added theissue type on Oct 27, 2026
  2. moved this from Backlog to Active in Packageson Oct 27, 2026
  3. added a commit that references this issue on Oct 27, 2026
    ce78f85
  4. knoan commented on Oct 27, 2026

    @knoan
    ContributorAuthor

    Solution

    monitor() now answers an exchange failing without a response with a network error Response (Response.error()) instead of rejecting, matching the handling of malformed URLs, so callers handle transport and server failures through a single channel.

    Changes

    • Failed exchanges are reported once to logger.warn, with the resource and the reason stated by the error cause, then answered with a network error
    • Layers declared below monitor() still receive failures as rejections, so retrying middlewares placed closer to the transport keep working
    • Busy status is still cleared when a failed exchange ends
    • Tests and TSDoc revised accordingly; CHANGELOG.md records the change under 0.6.0 (breaking, absorbed by the open release/0.6 line)

    Related

  5. moved this from Active to Done in Packageson Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    • Status
      Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions