[API reference](https://evolu.dev/docs/api-reference) › [@evolu/common](https://evolu.dev/docs/api-reference/common) › [Http](https://evolu.dev/docs/api-reference/common/Http) › fetch

## Call Signature

```ts
function fetch(
  input: RequestInfo | URL,
  mode: "text",
  init?: Omit<RequestInit, "signal">,
): Task<string, FetchError>;
```

Defined in: [packages/common/src/Http.ts:407](https://github.com/evoluhq/evolu/blob/037c390af081e9944d616ff298a729515c5c5ab7/packages/common/src/Http.ts#L407)

Fetches a resource and consumes the [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) inside the Task, so the
body is read while the request signal is still alive.

The request runs through [run.deps.nativeFetch](https://evolu.dev/docs/api-reference/common/Http/interfaces/NativeFetchDep).
Because native fetch is a default dependency, platforms and tests can replace
it without changing call sites.

With a [FetchMode](https://evolu.dev/docs/api-reference/common/Http/type-aliases/FetchMode), non-2xx responses return [FetchStatusError](https://evolu.dev/docs/api-reference/common/Http/interfaces/FetchStatusError)
(except `"headers"`, which reports status as a value) and unreadable bodies
return [FetchBodyError](https://evolu.dev/docs/api-reference/common/Http/interfaces/FetchBodyError). With a [FetchConsume](https://evolu.dev/docs/api-reference/common/Http/type-aliases/FetchConsume) callback, native
status semantics apply: HTTP error statuses resolve, and the consumer decides
how to interpret the status and body.

`signal` is not accepted in init because abort is controlled by the current
Run.

Aborting the Run aborts the underlying request, any response that arrives
after abort, and any in-progress body read. Abort is represented as
[AbortError](https://evolu.dev/docs/api-reference/common/Task/variables/AbortError), not FetchError: `run(fetch(...))` rejects with AbortError,
and `run.abortable(fetch(...))` returns it as an [Err](https://evolu.dev/docs/api-reference/common/Result/interfaces/Err).

Some runtimes reject aborted fetches with their own error instead of
`signal.reason`. This wrapper normalizes abort rejections from native fetch,
built-in body reads, and consumer callbacks back to the Run's AbortError.

`fetch` owns request lifetime and Response containment. It does not transform
requests or interpret app protocols beyond the built-in modes. Use Task
helpers for resilience, app helpers for app conventions, a replacement
[NativeFetch](https://evolu.dev/docs/api-reference/common/Http/type-aliases/NativeFetch) for request-wide behavior (base URLs, auth, logging), and
consumers for response interpretation.

### Composing fetch

Resilience is ordinary Task composition: wrap `fetch(url, "json")` in
[timeout](https://evolu.dev/docs/api-reference/common/Task/functions/timeout), then in [retry](https://evolu.dev/docs/api-reference/common/Task/functions/retry).

```ts
import {
  assertEqual,
  assertOk,
  assertType,
  createRun,
  Data,
  exponential,
  fetch,
  retry,
  take,
  timeout,
  type NativeFetch,
} from "@evolu/common";

const fetchWithRetry = (url: string) =>
  retry(timeout(fetch(url, "json"), "30s"), take(2)(exponential("100ms")));

let requestCount = 0;
const nativeFetch: NativeFetch = () => {
  requestCount++;
  return Promise.resolve(
    requestCount === 1
      ? new Response("Try again", { status: 503 })
      : new Response('{"name":"Ada"}'),
  );
};
await using run = createRun({ nativeFetch });

const result = await run(fetchWithRetry("/api/user"));
assertOk(result);
assertType(Data, result.value);
assertEqual(result.value, { name: "Ada" });
```

App conventions belong in small app-owned helpers. For example, posting JSON
is native `init` plus two conventions worth centralizing — the content-type
header and the stringify:

```ts
import {
  assertEqual,
  assertOk,
  assertType,
  createRun,
  Data,
  fetch,
  type FetchError,
  type NativeFetch,
  type Task,
} from "@evolu/common";

const postJson = (url: string, data: unknown): Task<unknown, FetchError> =>
  fetch(url, "json", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(data),
  });

const nativeFetch: NativeFetch = () =>
  Promise.resolve(new Response('{"id":"user-1"}'));
await using run = createRun({ nativeFetch });

const result = await run(postJson("/api/users", { name: "Ada" }));
assertOk(result);
assertType(Data, result.value);
assertEqual(result.value, { id: "user-1" });
```

Your app's version will grow your conventions — auth, envelopes, error
mapping — which is why it belongs to the app, not to `fetch`.

### Intercepting requests

Request-wide behavior belongs to a replacement [NativeFetch](https://evolu.dev/docs/api-reference/common/Http/type-aliases/NativeFetch) installed
at the composition root. This is the equivalent of interceptors or hooks in
libraries that expose client instances.

```ts
import {
  assertEqual,
  assertOk,
  createRun,
  fetch,
  type NativeFetch,
} from "@evolu/common";

const token = "secret-token";
const baseUrl = "https://api.example.com/v1/";
let interceptedRequest: Request | undefined;
const baseFetch: NativeFetch = (input, init) => {
  interceptedRequest = new Request(input, init);
  return Promise.resolve(new Response("ok"));
};

const nativeFetch: NativeFetch = (input, init) => {
  const headers = new Headers(init?.headers);
  headers.set("authorization", `Bearer ${token}`);

  // Only string inputs are resolved against the base URL; URL and Request
  // inputs are passed through unchanged.
  const url = typeof input === "string" ? new URL(input, baseUrl) : input;
  return baseFetch(url, { ...init, headers });
};

await using run = createRun({ nativeFetch });
assertOk(await run(fetch("users", "text")), "ok");
assertEqual(
  {
    url: interceptedRequest?.url,
    authorization: interceptedRequest?.headers.get("authorization"),
  },
  {
    url: "https://api.example.com/v1/users",
    authorization: "Bearer secret-token",
  },
);
```

### Consuming responses

Built-in modes handle common bodies. Specialized response interpretation
belongs in a consumer. Typed decoders, response envelopes, streaming, and
custom status semantics can be built on top without changing `fetch`.

```ts
import {
  assertEqual,
  assertOk,
  assertType,
  createRun,
  Data,
  fetch,
  ok,
  type FetchTransportError,
  type NativeFetch,
  type Task,
} from "@evolu/common";

const nativeFetch: NativeFetch = (input) =>
  Promise.resolve(
    input === "/api/user/metadata"
      ? new Response(null, {
          status: 204,
          headers: { "cache-control": "max-age=60" },
        })
      : new Response('{"name":"Ada"}'),
  );
await using run = createRun({ nativeFetch });

const user = await run(fetch("/api/user", "json"));
const metadata = fetch("/api/user/metadata", (response) =>
  ok({
    status: response.status,
    cache: response.headers.get("cache-control"),
  }),
);
assertType<
  Task<{ status: number; cache: string | null }, FetchTransportError>,
  typeof metadata
>();
assertOk(user);
assertType(Data, user.value);
assertEqual(user.value, { name: "Ada" });
assertOk(await run(metadata), { status: 204, cache: "max-age=60" });
```

### Aborting fetch

Abort follows the standard Task rules: a Fiber from `run(fetch(...))` rejects
with [AbortError](https://evolu.dev/docs/api-reference/common/Task/variables/AbortError), and `run.abortable(fetch(...))` returns it as a
Result error.

```ts
import {
  assert,
  AbortError,
  createRun,
  fetch,
  type NativeFetch,
} from "@evolu/common";

const nativeFetch: NativeFetch = (_input, init) =>
  new Promise<Response>((_resolve, reject) => {
    const signal = init?.signal;
    if (!signal) throw new Error("Missing signal");
    // oxlint-disable-next-line typescript/prefer-promise-reject-errors -- Fetch aborts with Task's structured AbortError.
    signal.addEventListener("abort", () => reject(signal.reason), {
      once: true,
    });
  });
await using run = createRun({ nativeFetch });

const fiber = run.abortable(fetch("/api/user", "json"));
fiber.abort();
const result = await fiber;

assert(!result.ok && AbortError.is(result.error), "Expected an AbortError.");
```

## Call Signature

```ts
function fetch(
  input: RequestInfo | URL,
  mode: "json",
  init?: Omit<RequestInit, "signal">,
): Task<unknown, FetchError>;
```

Defined in: [packages/common/src/Http.ts:414](https://github.com/evoluhq/evolu/blob/037c390af081e9944d616ff298a729515c5c5ab7/packages/common/src/Http.ts#L414)

Fetches a resource and parses a successful response body as JSON.

## Call Signature

```ts
function fetch(
  input: RequestInfo | URL,
  mode: "bytes",
  init?: Omit<RequestInit, "signal">,
): Task<Uint8Array<ArrayBuffer>, FetchError>;
```

Defined in: [packages/common/src/Http.ts:421](https://github.com/evoluhq/evolu/blob/037c390af081e9944d616ff298a729515c5c5ab7/packages/common/src/Http.ts#L421)

Fetches a resource and reads a successful response body as bytes.

## Call Signature

```ts
function fetch(
  input: RequestInfo | URL,
  mode: "headers",
  init?: Omit<RequestInit, "signal">,
): Task<FetchResponse, FetchTransportError>;
```

Defined in: [packages/common/src/Http.ts:433](https://github.com/evoluhq/evolu/blob/037c390af081e9944d616ff298a729515c5c5ab7/packages/common/src/Http.ts#L433)

Fetches a resource and returns its body-free response metadata.

HTTP status is returned as data rather than interpreted as success or
failure.

## Call Signature

```ts
function fetch<T, E>(
  input: RequestInfo | URL,
  consume: FetchConsume<T, E>,
  init?: Omit<RequestInit, "signal">,
): Task<T, FetchTransportError | E>;
```

Defined in: [packages/common/src/Http.ts:440](https://github.com/evoluhq/evolu/blob/037c390af081e9944d616ff298a729515c5c5ab7/packages/common/src/Http.ts#L440)

Fetches a resource and consumes its response with a custom callback.