# Request hooks and authentication

Use [`ky`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky) hooks when several requests share authentication, request changes, caching, or error handling. The hooks run at defined points in the request lifecycle, so put each change at the point where its inputs exist.

## Attach authentication and request metadata

Create an instance with `beforeRequest` when every request needs the current token. The hook receives the normalized request and can change its headers immediately before Ky sends it. Pass per-request values through `context` rather than adding them to the URL or body.

```ts
import ky from 'ky';

const getToken = (): string => 'secret123';

const api = ky.create({
  hooks: {
    beforeRequest: [({request, options}) => {
      const token = options.context.token;
      if (typeof token === 'string') {
        request.headers.set('Authorization', `Bearer ${token}`);
      }

      request.headers.set('X-Client', 'example-app');
    }],
  },
});

const run = async (): Promise<void> => {
  const profile = await api.get('https://api.example.com/profile', {
    context: {token: getToken()},
  }).json<{name: string}>();

  console.log(profile.name);
};

void run();
```

The outgoing request has `Authorization: Bearer secret123` and `X-Client: example-app`. `beforeRequest` runs once for the initial request; it does not run again for retries. To alter options before Ky constructs the request, use `init` instead. `init` is synchronous.

## Refresh authentication before a retry

Configure the status that can be retried, then refresh the token in `beforeRetry`. This hook runs only after Ky has selected a retry, so a 401 response does not refresh unless 401 is included in `retry.statusCodes`.

```ts
import ky from 'ky';

const refreshToken = async (): Promise<string> => 'fresh-token';

let accountAttempts = 0;

const api = ky.create({
  retry: {statusCodes: [401]},
  hooks: {
    beforeRequest: [() => {
      if (accountAttempts === 0) {
        accountAttempts++;
        return new Response('Unauthorized', {status: 401});
      }
    }],
    beforeRetry: [async ({request}) => {
      const token = await refreshToken();
      request.headers.set('Authorization', `Bearer ${token}`);
    }],
  },
});

const run = async (): Promise<void> => {
  try {
    const account = await api.get('https://api.example.com/account').json<{id: string}>();
    console.log(account.id);
  } catch (error) {
    console.log(error instanceof Error ? error.message : 'Request failed after retry');
  }
};

void run();
```

The retry sends the refreshed `Authorization` header and then returns the account response. `retryCount` is at least `1` in this hook. A `beforeRetry` hook can return a replacement `Request`, return a `Response` to skip the retry, throw to propagate an error, or return `ky.stop` to stop without propagating an error. A replacement request is used as-is, so remove credentials before sending it to another origin.

## Return a cached response

Return a `Response` from `beforeRequest` to satisfy a request without making an HTTP request. Cache a clone in `afterResponse`, because the response body can only be consumed once.

```ts
import ky from 'ky';

const responses = new Map<string, Response>();
responses.set('https://api.example.com/catalog', new Response(JSON.stringify({items: ['cached-item']}), {
  headers: {'Content-Type': 'application/json'},
}));

const api = ky.create({
  hooks: {
    beforeRequest: [({request}) => {
      const cached = responses.get(request.url);
      return cached?.clone();
    }],
    afterResponse: [({request, response}) => {
      responses.set(request.url, response.clone());
      return response;
    }],
  },
});

const run = async (): Promise<void> => {
  const catalog = await api.get('https://api.example.com/catalog').json<{items: string[]}>();

  console.log(catalog);
};

void run();
```

The call receives a cached clone from the map and does not make an HTTP request. Returning a cached response also skips the remaining `beforeRequest` hooks.

## Transform errors before they are thrown

Use `beforeError` to change the error that Ky is about to throw. This adds a final transformation step: return the `Error` you want the caller to receive, as this example changes its name and message. For HTTP-error narrowing and the consumed response-body details, see [Send JSON and read responses](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/json-requests-and-responses#handle-http-failures-separately).

```ts
import ky, {isHTTPError} from 'ky';

const api = ky.create({
  hooks: {
    beforeRequest: [() => new Response(JSON.stringify({message: 'Not found'}), {
      status: 404,
      headers: {'Content-Type': 'application/json'},
    })],
    beforeError: [({error}) => {
      if (isHTTPError(error)) {
        error.name = 'ApiError';
        error.message = `Request failed with status ${error.response.status}`;
      }

      return error;
    }],
  },
});

const run = async (): Promise<void> => {
  try {
    await api.get('https://api.example.com/missing').json<{id: string}>();
  } catch (error) {
    console.log(error instanceof Error ? error.message : 'Unknown failure');
  }
};

void run();
```

The thrown error has the changed name and message for an HTTP failure. `beforeError` receives Ky lifecycle errors such as HTTP, network, and timeout errors and must return an `Error` instance. Errors raised while a returned response is being consumed are handled through a body shortcut such as `.json()`.
## Choose the hook and preserve defaults

Use these hook responsibilities:

| Hook | Use it for | Result |
| --- | --- | --- |
| `init` | Change mutable options before request construction | Options change; the hook is synchronous |
| `beforeRequest` | Add authentication or replace the initial request | A request, a response that avoids the network, or no replacement |
| `beforeRetry` | Refresh credentials or modify a selected retry | A replacement request, a response that skips retry, `ky.stop`, or no replacement |
| `afterResponse` | Inspect or replace a response, or force a retry with `ky.retry()` | Ky uses a returned `Response`; `ky.retry()` starts a retry |
| `beforeError` | Rename or replace an error before it is thrown | Ky throws the returned `Error` |

When you extend an instance, Ky deep-merges options. That appends hook arrays and merges headers. Use [`replaceOption`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky) when the child instance must replace a merged value instead of inheriting it.

## Related

- [Ky options](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-options)
- [Error handling and validation](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/error-handling-and-validation)
- [Ky hooks](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-hooks)
