Skip to content

transports fetch

Eugene Lazutkin edited this page Jul 3, 2026 · 4 revisions

The fetch transport

double-meh's one built-in transport wraps the platform fetch() and returns its Response unchanged. It is registered under the name fetch and installed as io.defaultTransport, so every request goes through it unless you select another.

import io from 'double-meh';

// Uses the fetch transport by default — no configuration needed.
const user = await io.get('https://api.example.com/users/1');

What it does

The transport receives a PreparedRequest ({url, method, headers, body, signal, ...}) plus the request context, and calls real fetch(request.url, init). double-meh's own method, headers, and body are always applied last, so they win over anything else; signal is set only when the request actually carries one.

// Equivalent, low-level view of what the transport builds:
//   fetch(request.url, {method, headers, body, signal?})
const env = await io.full.get('https://api.example.com/x');
env.response; // the raw Response the transport returned

Automatic duplex: 'half' for streams

When the request body is a ReadableStream, the transport sets duplex: 'half' on the fetch init automatically — required by the platform to stream an upload.

const body = someReadableStream();
await io.put('https://api.example.com/upload', body);
// transport adds duplex: 'half' because body.getReader is a function

The fetch option sub-bag

options.fetch is a RequestInit sub-bag: every field on it is spread into the fetch init before double-meh applies its own method/headers/body (and signal, when present). Use it to reach RequestInit knobs double-meh does not model directly.

await io.get(url, data, {
  fetch: {
    credentials: 'include',
    mode: 'cors',
    redirect: 'manual',
    integrity: 'sha256-...',
    referrer: 'https://app.example.com/'
  }
});

Because double-meh's fields are set last, method, headers, and body cannot be overridden through this bag (signal only wins when the request has one — a fetch: {signal} on a signal-less request passes through):

// method stays 'GET' even though a stray method would be ignored here
await io.get(url, null, {fetch: {credentials: 'include', mode: 'cors'}});
// -> fetch init: {credentials: 'include', mode: 'cors', method: 'GET', ...}

Custom transports

Register your own transport with io.registerTransport(name, fn) and select it per request via options.transport. A transport is (request, ctx) => Promise<Response>; ctx.options holds the resolved options (including ctx.options.fetch).

io.registerTransport('logging', (request, ctx) => {
  console.log(request.method, request.url);
  return fetch(request.url, {
    method: request.method,
    headers: request.headers,
    body: request.body,
    signal: request.signal
  });
});

await io.get(url, null, {transport: 'logging'});

If options.transport names no registered transport, io.defaultTransport (the fetch transport) is used.

See also

Clone this wiki locally