Skip to content

envelope

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

Response envelope

Every io.full.* call resolves to an envelope: the parsed body plus the status, headers, and the raw Response. The same shape is mixed into thrown BadStatus errors, so a failed request is inspected exactly like a successful one.

import io from 'double-meh';

const env = await io.full.get('https://api.example.com/users/1');
env.data;    // parsed body
env.status;  // 200
env.ok;      // true

Eager core

Four fields plus the raw response are set immediately:

  • data — the decoded body (JSON parsed for *+json content types, else text; undefined for HEAD/OPTIONS, 204, or empty bodies).
  • status — the numeric HTTP status.
  • ok — the Response.ok boolean.
  • headers — a plain header dict (lower-cased keys; set-cookie collected into an array).
  • response — the untouched Response for anything not modelled here.
const env = await io.full.get(url);
if (env.ok) console.log(env.status, env.headers['content-type'], env.data);

Lazy header getters

The rest are lazy getters that parse a header on access, so you only pay for what you read:

  • etag — the raw ETag, or undefined.
  • weaktrue when the ETag is weak (W/"...").
  • lastModified — a Date from Last-Modified, or undefined.
  • locationLocation resolved to an absolute URL against the response URL.
  • links — parsed Link header as {rel: url}.
  • contentType — the raw Content-Type, or undefined.
  • retryAfterRetry-After as a number of seconds, or a Date, or undefined.
  • serverTimingServer-Timing as an array of {name, dur?, desc?}.
const env = await io.full.post('https://api.example.com/things', {name: 'Bob'});
env.status;   // 201
env.location; // 'https://api.example.com/things/7' (resolved absolute)
env.etag;     // '"v1"' or undefined

Errors carry the envelope

Failures throw one of three classes exported as io.FailedIO, io.TimedOut, and io.BadStatus (also importable by name). All extend Error and carry .response (a Response, or undefined when no response arrived) and .options. Only BadStatus mixes in the full envelope shape — .data, .status, .ok, .headers, and every lazy getter — so guard with instanceof io.BadStatus before reading them; FailedIO/TimedOut (network failure / timeout) carry just .response and .options.

  • FailedIO — base class; transport/network failure with no usable response.
  • TimedOut — a FailedIO whose message is 'Timed out'.
  • BadStatus — thrown for a non-2xx response (unless ignoreBadStatus); e.data is the parsed body, typically problem+json.
try {
  await io.get('https://api.example.com/missing');
} catch (e) {
  if (e instanceof io.BadStatus) {
    e.status;     // 404
    e.data;       // parsed problem+json, e.g. {title: 'Nope'}
    e.data.title; // 'Nope'
  }
}

Pass ignoreBadStatus: true to receive the envelope instead of a throw on non-2xx.

See also

Clone this wiki locally