feat: переработать кастомизацию HttpClient
- добавлен плоский ApiConfig с lifecycle hooks - добавлены ApiError, retry context, timeout и кастомные parser/serializer - обновлены примеры, документация и тесты под новый API
This commit is contained in:
185
README.md
185
README.md
@@ -137,36 +137,189 @@ const pharmacies = await v1AdminPharmaciesList(http, {});
|
||||
|
||||
## Кастомный HTTP Клиент
|
||||
|
||||
`HttpClient` создается отдельно и передается в generated operations. Это позволяет централизованно настраивать авторизацию, headers, baseUrl и transport behavior.
|
||||
`HttpClient` создается отдельно и передается в generated operations. Это позволяет централизованно настраивать `baseUrl`, headers, авторизацию, timeout и transport behavior.
|
||||
|
||||
```typescript
|
||||
import { HttpClient } from './generated';
|
||||
import { ApiError, HttpClient } from './generated';
|
||||
|
||||
type SecurityData = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
const http = new HttpClient<SecurityData>({
|
||||
const http = new HttpClient({
|
||||
baseUrl: 'https://api.example.com',
|
||||
baseApiParams: {
|
||||
headers: {
|
||||
'X-App-Version': '1.0.0',
|
||||
},
|
||||
credentials: 'include',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'X-App-Version': '1.0.0',
|
||||
},
|
||||
securityWorker: (securityData) => {
|
||||
if (!securityData?.token) {
|
||||
return undefined;
|
||||
onRequest: (params) => {
|
||||
const token = localStorage.getItem('access_token');
|
||||
|
||||
if (!params.secure || !token) {
|
||||
return params;
|
||||
}
|
||||
|
||||
return {
|
||||
...params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${securityData.token}`,
|
||||
...params.headers,
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
onResponse: (response) => response,
|
||||
onError: async (error, context) => {
|
||||
if (error instanceof ApiError && error.status === 401 && context.retryCount === 0) {
|
||||
await refreshToken();
|
||||
return context.retry();
|
||||
}
|
||||
|
||||
throw error;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`onRequest` вызывается перед `fetch`, `onResponse` после успешного ответа, `onError` для HTTP-ошибок, network errors и ошибок парсинга. Для защищенных endpoints generated operation передает `secure: true`, поэтому авторизацию можно добавлять только там, где она нужна.
|
||||
|
||||
### Опции HttpClient
|
||||
|
||||
`HttpClient` принимает плоский конфиг. Стандартные `fetch`-опции можно задавать прямо в конструкторе вместе с кастомными hooks клиента.
|
||||
|
||||
| Опция | Тип | Назначение |
|
||||
| --- | --- | --- |
|
||||
| `baseUrl` | `string` | Базовый URL API. По умолчанию берется из `servers[0].url` OpenAPI спецификации. |
|
||||
| `headers` | `HeadersInit` | Заголовки по умолчанию для всех запросов. |
|
||||
| `credentials` | `RequestCredentials` | Политика отправки cookies/auth credentials: `omit`, `same-origin`, `include`. |
|
||||
| `mode` | `RequestMode` | Fetch request mode, например `cors`, `same-origin`, `no-cors`. |
|
||||
| `cache` | `RequestCache` | Fetch cache policy. |
|
||||
| `redirect` | `RequestRedirect` | Поведение при redirect: `follow`, `error`, `manual`. |
|
||||
| `referrer` | `string` | Значение referrer для запросов. |
|
||||
| `referrerPolicy` | `ReferrerPolicy` | Политика referrer. |
|
||||
| `integrity` | `string` | Subresource integrity value. |
|
||||
| `keepalive` | `boolean` | Позволяет запросу пережить unload страницы. |
|
||||
| `secure` | `boolean` | Дефолтный маркер защищенного запроса. Обычно выставляется generated operation по OpenAPI `security`. |
|
||||
| `type` | `ContentType` | Дефолтный `Content-Type` для body. Обычно выставляется generated operation. |
|
||||
| `format` | `ResponseFormat` | Дефолтный способ парсинга ответа: `json`, `text`, `blob`, `formData`, `arrayBuffer`. |
|
||||
| `timeout` | `number` | Таймаут запроса в миллисекундах. Работает через `AbortSignal`. |
|
||||
| `customFetch` | `typeof fetch` | Замена стандартного `fetch`, например для тестов, SSR или custom transport. |
|
||||
| `paramsSerializer` | `(query) => string` | Кастомная сериализация query params в URL. |
|
||||
| `responseParser` | `(response, format) => unknown` | Кастомный парсинг response body. |
|
||||
| `onRequest` | `(params, context) => params` | Request interceptor перед вызовом `fetch`. |
|
||||
| `onResponse` | `(response, context) => response` | Response interceptor после успешного HTTP ответа. |
|
||||
| `onError` | `(error, context) => result` | Error interceptor для HTTP ошибок, network errors, retry и refresh-token сценариев. |
|
||||
|
||||
Также можно использовать другие стандартные поля `RequestInit`, доступные в вашей TypeScript/Runtime среде, если они не относятся к конкретному request body или cancellation lifecycle.
|
||||
|
||||
Эти поля не задаются на уровне конструктора и передаются конкретной operation или самому `request`:
|
||||
|
||||
| Поле | Почему не в конструкторе |
|
||||
| --- | --- |
|
||||
| `path` | Генерируется из OpenAPI path для конкретной operation. |
|
||||
| `method` | Генерируется из OpenAPI HTTP метода. |
|
||||
| `query` | Уникален для конкретного вызова operation. |
|
||||
| `body` | Уникален для конкретного запроса. |
|
||||
| `signal` | Один общий `AbortSignal` может случайно отменить группу независимых запросов. |
|
||||
| `cancelToken` | Всегда относится к одному конкретному запросу. |
|
||||
|
||||
### Рецепты Кастомизации
|
||||
|
||||
Авторизация через `onRequest`:
|
||||
|
||||
```typescript
|
||||
const http = new HttpClient({
|
||||
onRequest: (params) => {
|
||||
const token = localStorage.getItem('access_token');
|
||||
|
||||
if (!params.secure || !token) {
|
||||
return params;
|
||||
}
|
||||
|
||||
return {
|
||||
...params,
|
||||
headers: {
|
||||
...params.headers,
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
http.setSecurityData({ token: 'jwt-token' });
|
||||
Refresh token и повтор запроса:
|
||||
|
||||
```typescript
|
||||
const http = new HttpClient({
|
||||
onError: async (error, context) => {
|
||||
if (error instanceof ApiError && error.status === 401 && context.retryCount === 0) {
|
||||
await refreshToken();
|
||||
return context.retry();
|
||||
}
|
||||
|
||||
throw error;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Логирование ответов через `onResponse`:
|
||||
|
||||
```typescript
|
||||
const http = new HttpClient({
|
||||
onResponse: (response, context) => {
|
||||
console.log(context.request.method, context.url, response.status);
|
||||
return response;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Кастомная сериализация query params:
|
||||
|
||||
```typescript
|
||||
const http = new HttpClient({
|
||||
paramsSerializer: (query) => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
Object.entries(query).forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
params.set(key, value.join(','));
|
||||
return;
|
||||
}
|
||||
|
||||
if (value !== undefined) {
|
||||
params.set(key, String(value));
|
||||
}
|
||||
});
|
||||
|
||||
return params.toString();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Замена транспорта через `customFetch`:
|
||||
|
||||
```typescript
|
||||
const http = new HttpClient({
|
||||
customFetch: async (url, init) => {
|
||||
console.log('request', url);
|
||||
return fetch(url, init);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Кастомный парсинг ответа:
|
||||
|
||||
```typescript
|
||||
const http = new HttpClient({
|
||||
responseParser: async (response) => {
|
||||
if (response.status === 204) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
|
||||
if (contentType.includes('json')) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
return response.text();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Для отдельного вызова можно передать дополнительные request params последним аргументом operation.
|
||||
|
||||
Reference in New Issue
Block a user