# Other HTTP clients (https://session-kit.pages.dev/docs/client/other-http-clients)

[`authFetch`](https://session-kit.pages.dev/docs/client/requests) takes a `URL` or a `Request` as well as
a path, as `fetch` does. An HTTP client that lets you choose its `fetch` can
send its requests through it, and they get everything `authFetch` does: the
token stays on your API's origin, a 401 refreshes once, and only a GET or
HEAD is sent again.

A client with its own transport uses
[the calls `authFetch` is built on](#clients-without-a-fetch-option) instead.

## Clients that take a `fetch`

With [ky](https://github.com/sindresorhus/ky), pass `authFetch` as `fetch`:

```ts title="src/ky.ts"
import ky from "ky";

import { authFetch } from "./api.js";
import { API } from "./backend.js";

export const api = ky.create({ baseUrl: `${API}/`, fetch: authFetch });

export async function fetchCarts(userId: number): Promise<unknown> {
  return api.get(`auth/carts/user/${userId}`).json();
}
```

With [axios](https://axios-http.com), choose its `fetch` adapter and pass
`authFetch` in `env`:

```ts title="src/axios.ts"
import axios from "axios";

import { authFetch } from "./api.js";
import { API } from "./backend.js";

export const http = axios.create({
  baseURL: API,
  adapter: "fetch",
  env: { fetch: authFetch },
});
```

[openapi-fetch](https://openapi-ts.dev/openapi-fetch/) takes it the same way
as ky:

```ts
const client = createClient<paths>({ baseUrl: API, fetch: authFetch });
```

What changes for the client's calls:

- **Give the client the same API.** Its base URL must be on the origin that
  `authFetch` was created for. A request for another origin rejects with
  `INVALID_OPTIONS`, and the token isn't sent.
- **Signed out, every call rejects** with `UNAUTHENTICATED`. If the client
  also calls public endpoints, such as your sign-in, create `authFetch` with
  [`optional: true`](https://session-kit.pages.dev/docs/client/requests). Those requests then go without
  a token.
- **A 401 reaches the client after the refresh.** For a GET or HEAD, the
  client sees the answer to the request that was sent again. For a write, it
  sees the 401, as ky's `HTTPError` or axios's `AxiosError`. Retry it where
  sending it twice is safe.
- **Session kit's errors keep their code.** ky rejects with the `AuthError`
  itself. axios wraps it in an `AxiosError` that has the same `code`, with
  the `AuthError` as its `cause`. Match on `error.code`, which works with
  both, rather than on `instanceof AuthError`.
- **`authFetch` sets `Authorization`.** While signed in, it replaces a
  header the client set itself.
- **Only what a `Request` holds is sent.** ky hands `fetch` the options it
  doesn't know itself, such as Next.js's `next`. `authFetch` doesn't pass
  those on.

## Clients without a `fetch` option

`createAuthFetch` and the
[effetch plugin](https://session-kit.pages.dev/docs/effect#6-call-your-api-with-effetch) are built on three
calls, and a client with its own transport can use the same ones:

1. **Before a request**, `auth.credentials.get()` resolves the credentials to
   send, waiting for a restore or a refresh that is under way. It resolves
   `null` when nobody is signed in.
2. **After a 401**, `auth.credentials.renew(credentials)` takes the
   credentials that request was sent with, and resolves new ones. However many
   requests failed, they share one refresh. It resolves `null` when the user
   was signed out instead.
3. **After a second 401**, `auth.credentials.reject(credentials)` ends the
   session, unless newer credentials have replaced the ones you pass.

An axios instance that keeps its default adapter does it with two
interceptors:

```ts title="src/axios-interceptors.ts"
import type { Credentials } from "@monarcode/session-kit";
import axios, { type InternalAxiosRequestConfig } from "axios";

import { auth } from "./auth.js";
import { API } from "./backend.js";

type Config = InternalAxiosRequestConfig & { replayed?: boolean };

// One instance for one API, so the token goes nowhere else.
export const http = axios.create({ baseURL: API });

/** The credentials each request was sent with. */
const sent = new WeakMap<Config, Credentials>();

http.interceptors.request.use(async (config) => {
  const credentials = await auth.credentials.get();
  // Signed out, the request goes without a token, as a public one would.
  if (credentials) {
    config.headers.set("Authorization", `Bearer ${credentials.accessToken}`);
    sent.set(config, credentials);
  }
  return config;
});

http.interceptors.response.use(undefined, async (error: unknown) => {
  if (!axios.isAxiosError(error) || error.response?.status !== 401) throw error;
  const config: Config | undefined = error.config;
  const used = config && sent.get(config);
  if (!config || !used) throw error;

  if (config.replayed) {
    // The new token was refused too: end the session.
    await auth.credentials.reject(used);
    throw error;
  }
  const fresh = await auth.credentials.renew(used);
  // As with authFetch, only a GET or HEAD is sent twice. For anything else
  // the caller decides.
  if (!fresh || !["get", "head"].includes(config.method ?? "")) throw error;
  const replay: Config = { ...config, replayed: true };
  return http.request(replay);
});
```

Three things `createAuthFetch` does for you are yours to keep here:

- **Keep the token on one origin.** Give the client one base URL, and don't
  pass it absolute URLs to other hosts.
- **Don't read the token early.** Ask for credentials in the request hook,
  never once at startup. A token saved in a variable goes stale at the next
  refresh.
- **Check the account after slow work.** `auth.isCurrent(credentials)` tells
  whether the session that sent a request is still the signed-in one, before
  you show its answer.