@byndyusoft-ui/http-client 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +479 -0
- package/dist/cjs/adapters/FetchAdapter.cjs +218 -0
- package/dist/cjs/adapters/XhrAdapter.cjs +357 -0
- package/dist/cjs/asserts/assertNonBlankString.cjs +11 -0
- package/dist/cjs/asserts/assertValidAdapter.cjs +15 -0
- package/dist/cjs/asserts/assertValidBaseUrl.cjs +10 -0
- package/dist/cjs/asserts/assertValidBody.cjs +17 -0
- package/dist/cjs/asserts/assertValidFetchAdapterOptions.cjs +70 -0
- package/dist/cjs/asserts/assertValidHeader.cjs +18 -0
- package/dist/cjs/asserts/assertValidHeaders.cjs +17 -0
- package/dist/cjs/asserts/assertValidHttpClientOptions.cjs +60 -0
- package/dist/cjs/asserts/assertValidMethod.cjs +13 -0
- package/dist/cjs/asserts/assertValidParam.cjs +23 -0
- package/dist/cjs/asserts/assertValidParams.cjs +17 -0
- package/dist/cjs/asserts/assertValidRequestConfig.cjs +55 -0
- package/dist/cjs/asserts/assertValidResponseType.cjs +13 -0
- package/dist/cjs/asserts/assertValidSignal.cjs +16 -0
- package/dist/cjs/asserts/assertValidTimeout.cjs +12 -0
- package/dist/cjs/asserts/assertValidUrl.cjs +10 -0
- package/dist/cjs/asserts/assertValidValidateStatus.cjs +13 -0
- package/dist/cjs/asserts/assertValidWithCredentials.cjs +12 -0
- package/dist/cjs/asserts/assertValidXhrAdapterOptions.cjs +39 -0
- package/dist/cjs/asserts/headerValueLineBreakPattern.cjs +5 -0
- package/dist/cjs/asserts/isRecord.cjs +11 -0
- package/dist/cjs/constants/httpMethods.cjs +13 -0
- package/dist/cjs/constants/httpResponseTypes.cjs +11 -0
- package/dist/cjs/constants/httpStatusCodes.cjs +68 -0
- package/dist/cjs/constants/requestBuilderErrorCodes.cjs +26 -0
- package/dist/cjs/core/HttpClient.cjs +141 -0
- package/dist/cjs/core/HttpRequestBuilder.cjs +129 -0
- package/dist/cjs/errors/AbortError.cjs +12 -0
- package/dist/cjs/errors/HttpClientError.cjs +18 -0
- package/dist/cjs/errors/HttpResponseError.cjs +26 -0
- package/dist/cjs/errors/NetworkError.cjs +12 -0
- package/dist/cjs/errors/ParseError.cjs +20 -0
- package/dist/cjs/errors/RequestBuilderError.cjs +18 -0
- package/dist/cjs/errors/RequestPreparationError.cjs +13 -0
- package/dist/cjs/errors/TimeoutError.cjs +18 -0
- package/dist/cjs/index.cjs +45 -0
- package/dist/cjs/utilities/buildUrl.cjs +94 -0
- package/dist/cjs/utilities/getErrorMessage.cjs +8 -0
- package/dist/cjs/utilities/isStatusAccepted.cjs +8 -0
- package/dist/cjs/utilities/mergeHeaders.cjs +30 -0
- package/dist/cjs/utilities/mergeParams.cjs +37 -0
- package/dist/cjs/utilities/prepareRequestBody.cjs +43 -0
- package/dist/esm/adapters/FetchAdapter.js +216 -0
- package/dist/esm/adapters/XhrAdapter.js +355 -0
- package/dist/esm/asserts/assertNonBlankString.js +9 -0
- package/dist/esm/asserts/assertValidAdapter.js +13 -0
- package/dist/esm/asserts/assertValidBaseUrl.js +8 -0
- package/dist/esm/asserts/assertValidBody.js +15 -0
- package/dist/esm/asserts/assertValidFetchAdapterOptions.js +68 -0
- package/dist/esm/asserts/assertValidHeader.js +16 -0
- package/dist/esm/asserts/assertValidHeaders.js +15 -0
- package/dist/esm/asserts/assertValidHttpClientOptions.js +58 -0
- package/dist/esm/asserts/assertValidMethod.js +11 -0
- package/dist/esm/asserts/assertValidParam.js +21 -0
- package/dist/esm/asserts/assertValidParams.js +15 -0
- package/dist/esm/asserts/assertValidRequestConfig.js +53 -0
- package/dist/esm/asserts/assertValidResponseType.js +11 -0
- package/dist/esm/asserts/assertValidSignal.js +14 -0
- package/dist/esm/asserts/assertValidTimeout.js +10 -0
- package/dist/esm/asserts/assertValidUrl.js +8 -0
- package/dist/esm/asserts/assertValidValidateStatus.js +11 -0
- package/dist/esm/asserts/assertValidWithCredentials.js +10 -0
- package/dist/esm/asserts/assertValidXhrAdapterOptions.js +37 -0
- package/dist/esm/asserts/headerValueLineBreakPattern.js +3 -0
- package/dist/esm/asserts/isRecord.js +9 -0
- package/dist/esm/constants/httpMethods.js +11 -0
- package/dist/esm/constants/httpResponseTypes.js +9 -0
- package/dist/esm/constants/httpStatusCodes.js +66 -0
- package/dist/esm/constants/requestBuilderErrorCodes.js +24 -0
- package/dist/esm/core/HttpClient.js +139 -0
- package/dist/esm/core/HttpRequestBuilder.js +127 -0
- package/dist/esm/errors/AbortError.js +9 -0
- package/dist/esm/errors/HttpClientError.js +15 -0
- package/dist/esm/errors/HttpResponseError.js +23 -0
- package/dist/esm/errors/NetworkError.js +9 -0
- package/dist/esm/errors/ParseError.js +17 -0
- package/dist/esm/errors/RequestBuilderError.js +15 -0
- package/dist/esm/errors/RequestPreparationError.js +10 -0
- package/dist/esm/errors/TimeoutError.js +15 -0
- package/dist/esm/index.js +16 -0
- package/dist/esm/utilities/buildUrl.js +92 -0
- package/dist/esm/utilities/getErrorMessage.js +6 -0
- package/dist/esm/utilities/isStatusAccepted.js +6 -0
- package/dist/esm/utilities/mergeHeaders.js +27 -0
- package/dist/esm/utilities/mergeParams.js +35 -0
- package/dist/esm/utilities/prepareRequestBody.js +41 -0
- package/dist/index.d.ts +354 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
# @byndyusoft-ui/http-client
|
|
2
|
+
|
|
3
|
+
HTTP-клиент с неизменяемым builder, адаптерами Fetch и XMLHttpRequest, типизированными ответами, hooks, отменой и таймаутами.
|
|
4
|
+
|
|
5
|
+
## Установка
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @byndyusoft-ui/http-client@alpha
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Текущая версия — `0.1.0-alpha.0`, предварительный выпуск с npm-тегом `alpha`.
|
|
12
|
+
|
|
13
|
+
Пакет требует Node.js 20 или новее для сборки и серверного выполнения. Браузерные приложения получают ESM-вход, CommonJS-потребители используют отдельную CJS-сборку. Обе сборки сохраняют отдельные файлы модулей в `dist/esm/` и `dist/cjs/`.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { HttpClient } from '@byndyusoft-ui/http-client';
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
const { HttpClient } = require('@byndyusoft-ui/http-client');
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Публичный API доступен только из корня пакета. Импорты внутренних путей `dist/*` не входят в контракт. Пакет помечен как не имеющий побочных эффектов при импорте, поэтому глобальную регистрацию обработчиков и полифиллов следует выполнять в коде приложения.
|
|
24
|
+
|
|
25
|
+
### Поддерживаемые окружения
|
|
26
|
+
|
|
27
|
+
- `FetchAdapter` использует глобальные `fetch`, `Headers`, `AbortController` и другие стандартные Fetch API. Они доступны в современных браузерах и Node.js 20.
|
|
28
|
+
- `XhrAdapter` предназначен для браузерного окружения и требует `XMLHttpRequest`. Для серверного выполнения нужен совместимый полифилл.
|
|
29
|
+
- `asStream()` требует `ReadableStream`; XHR-вариант также использует `TextEncoder`.
|
|
30
|
+
- Пакет не устанавливает полифиллы и не изменяет глобальное окружение.
|
|
31
|
+
|
|
32
|
+
## Быстрый старт
|
|
33
|
+
|
|
34
|
+
По умолчанию используется `FetchAdapter`:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { HttpClient } from '@byndyusoft-ui/http-client';
|
|
38
|
+
|
|
39
|
+
interface IUser {
|
|
40
|
+
id: number;
|
|
41
|
+
name: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const httpClient = new HttpClient({
|
|
45
|
+
baseUrl: 'https://api.example.com'
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const response = await httpClient.get('/users/1').asJson<IUser>().execute();
|
|
49
|
+
const user = response.data;
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Формат ответа выбирается до выполнения запроса. `execute()` не принимает generic.
|
|
53
|
+
|
|
54
|
+
| Метод | Тип `data` | Когда использовать |
|
|
55
|
+
| ----------------- | ----------------------------------------- | ---------------------------------- |
|
|
56
|
+
| `asJson<T>()` | `T \| undefined` | JSON API |
|
|
57
|
+
| `asText()` | `string \| undefined` | текст, HTML, CSV |
|
|
58
|
+
| `asBlob()` | `Blob \| undefined` | файлы и бинарные данные в браузере |
|
|
59
|
+
| `asArrayBuffer()` | `ArrayBuffer \| undefined` | низкоуровневая бинарная обработка |
|
|
60
|
+
| `asStream()` | `ReadableStream<Uint8Array> \| undefined` | потоковое чтение ответа |
|
|
61
|
+
|
|
62
|
+
Вызов без селектора допустим и возвращает `IHttpResponse<unknown>`. `FetchAdapter` и `XhrAdapter` без собственного `responseType` разбирают такое тело как JSON; настройка `XhrAdapter.responseType` может изменить формат по умолчанию. Generic `asJson<T>()` описывает ожидаемую схему и не проверяет данные во время выполнения.
|
|
63
|
+
|
|
64
|
+
Каждый выполненный запрос возвращает `IHttpResponse<T>`:
|
|
65
|
+
|
|
66
|
+
| Поле | Тип | Описание |
|
|
67
|
+
| ------------ | ------------------------ | ------------------------------------------------------------------------ |
|
|
68
|
+
| `data` | `T \| undefined` | декодированное тело; отсутствует для пустого ответа |
|
|
69
|
+
| `status` | `number` | HTTP-статус |
|
|
70
|
+
| `statusText` | `string` | текст HTTP-статуса |
|
|
71
|
+
| `headers` | `Record<string, string>` | заголовки ответа; стандартные адаптеры приводят имена к нижнему регистру |
|
|
72
|
+
| `config` | `IHttpRequestConfig` | фактическая конфигурация, переданная адаптеру |
|
|
73
|
+
|
|
74
|
+
## Настройка клиента
|
|
75
|
+
|
|
76
|
+
Конструктор принимает `IHttpClientOptions`. Пустой объект создаёт клиент с `FetchAdapter` и без общих настроек:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
const httpClient = new HttpClient({
|
|
80
|
+
baseUrl: 'https://api.example.com/v1',
|
|
81
|
+
headers: { Accept: 'application/json' },
|
|
82
|
+
params: { locale: 'ru' },
|
|
83
|
+
timeout: 10_000,
|
|
84
|
+
validateStatus: status => status >= 200 && status < 400,
|
|
85
|
+
withCredentials: true
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
| Настройка | Назначение | Значение по умолчанию |
|
|
90
|
+
| ----------------- | ---------------------------------------------------- | --------------------- |
|
|
91
|
+
| `adapter` | транспорт, реализующий `IHttpClientAdapter` | новый `FetchAdapter` |
|
|
92
|
+
| `baseUrl` | базовая часть относительных URL | отсутствует |
|
|
93
|
+
| `headers` | заголовки всех запросов | отсутствуют |
|
|
94
|
+
| `params` | query-параметры всех запросов | отсутствуют |
|
|
95
|
+
| `timeout` | таймаут в миллисекундах | не задан на клиенте |
|
|
96
|
+
| `validateStatus` | определяет успешность HTTP-статуса | статусы `200–299` |
|
|
97
|
+
| `withCredentials` | отправка credentials | зависит от адаптера |
|
|
98
|
+
| `onRequest` | преобразование итоговой конфигурации перед адаптером | отсутствует |
|
|
99
|
+
| `onRequestError` | восстановление после ошибки request-hook | отсутствует |
|
|
100
|
+
| `onResponse` | преобразование успешного ответа | отсутствует |
|
|
101
|
+
| `onResponseError` | обработка или восстановление после ошибки ответа | отсутствует |
|
|
102
|
+
|
|
103
|
+
## Построение запроса
|
|
104
|
+
|
|
105
|
+
Builder неизменяемый: каждый метод возвращает новый экземпляр.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
const request = httpClient
|
|
109
|
+
.post('/users')
|
|
110
|
+
.header('X-Request-Id', requestId)
|
|
111
|
+
.param('source', 'admin')
|
|
112
|
+
.body({ name: 'Jane' })
|
|
113
|
+
.timeout(5_000)
|
|
114
|
+
.asJson<IUser>();
|
|
115
|
+
|
|
116
|
+
const response = await request.execute();
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Поддерживаются методы `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `OPTIONS` и `PATCH`. Для `GET` и `HEAD` тело запрещено.
|
|
120
|
+
|
|
121
|
+
| Метод builder | Назначение |
|
|
122
|
+
| --------------------------------- | ------------------------------------------------------------ |
|
|
123
|
+
| `baseUrl(value)` | переопределяет базовый URL |
|
|
124
|
+
| `header(name, value)` | добавляет или заменяет один заголовок без учёта регистра |
|
|
125
|
+
| `headers(values)` | объединяет несколько заголовков |
|
|
126
|
+
| `param(name, value)` | добавляет, заменяет или удаляет один query-параметр |
|
|
127
|
+
| `params(values)` | объединяет несколько query-параметров |
|
|
128
|
+
| `body(data)` | задаёт тело запроса |
|
|
129
|
+
| `signal(signal)` | привязывает пользовательский `AbortSignal` |
|
|
130
|
+
| `timeout(milliseconds)` | задаёт таймаут; `0` отключает также унаследованный таймаут |
|
|
131
|
+
| `validateStatus(predicate)` | определяет успешность статуса конкретного ответа |
|
|
132
|
+
| `withCredentials(value)` | управляет credentials конкретного запроса |
|
|
133
|
+
| `bearer(token)` | устанавливает `Authorization: Bearer <token>` |
|
|
134
|
+
| `asJson<T>()` и остальные `as*()` | выбирают формат чтения и тип успешного ответа |
|
|
135
|
+
| `build()` | возвращает независимый снимок `Readonly<IHttpRequestConfig>` |
|
|
136
|
+
| `execute()` | проверяет конфигурацию и выполняет запрос |
|
|
137
|
+
|
|
138
|
+
## URL, заголовки и query-параметры
|
|
139
|
+
|
|
140
|
+
Относительный URL объединяется с `baseUrl` как путь: завершающий slash базового URL и начальный slash запроса не дублируются. Абсолютный URL запроса используется без `baseUrl`. Query-параметры базового URL и запроса сохраняются, а fragment берётся из URL запроса.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const response = await httpClient
|
|
144
|
+
.get('/users?sort=name#list')
|
|
145
|
+
.params({ page: 2, active: true, role: ['admin', 'editor'] })
|
|
146
|
+
.execute();
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Значения query-параметров могут быть строками, числами, boolean или массивами этих значений. Массив сериализуется повторяющимися ключами. `null` и `undefined` удаляют ранее накопленный ключ в одной карте или цепочке builder, а такие элементы массива пропускаются. Непустые параметры запроса имеют приоритет над параметрами клиента. Значение `null` или `undefined` из запроса нормализуется до объединения и поэтому не удаляет одноимённый параметр, заданный в `HttpClient`.
|
|
150
|
+
|
|
151
|
+
Заголовки также объединяются слева направо, но их имена сравниваются без учёта регистра. Последнее значение заменяет предыдущее и сохраняет написание последнего имени.
|
|
152
|
+
|
|
153
|
+
## Тело запроса
|
|
154
|
+
|
|
155
|
+
`body()` принимает JSON-совместимые значения и готовые транспортные тела:
|
|
156
|
+
|
|
157
|
+
| Значение | Преобразование и `Content-Type` |
|
|
158
|
+
| ------------------------------------------ | ------------------------------------------------------------------------------------- |
|
|
159
|
+
| объект, массив, number, boolean или `null` | JSON; при отсутствии заголовка добавляется `application/json` |
|
|
160
|
+
| string | отправляется без изменений; заголовок автоматически не добавляется |
|
|
161
|
+
| `URLSearchParams` | строка form-urlencoded; добавляется `application/x-www-form-urlencoded;charset=UTF-8` |
|
|
162
|
+
| `FormData` | отправляется без изменений; boundary формирует транспорт |
|
|
163
|
+
| `Blob`, `ArrayBuffer`, `ArrayBufferView` | отправляется без изменений |
|
|
164
|
+
|
|
165
|
+
Пользовательский `Content-Type` никогда не заменяется автоматически. Для `FormData` его не следует устанавливать вручную, иначе в заголовке может отсутствовать корректный boundary.
|
|
166
|
+
|
|
167
|
+
`body(undefined)` синхронно выбрасывает `RequestBuilderError`. Тело также запрещено для `GET` и `HEAD`. Ошибка `JSON.stringify`, например циклическая ссылка или несериализуемое значение, преобразуется адаптером в `RequestPreparationError` с исходной причиной в `cause`.
|
|
168
|
+
|
|
169
|
+
## Проверка HTTP-статуса
|
|
170
|
+
|
|
171
|
+
По умолчанию Fetch- и XHR-адаптеры считают успешными статусы от `200` до `299`. Настройка `validateStatus` позволяет изменить это правило для всего клиента или одного запроса:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
const httpClient = new HttpClient({
|
|
175
|
+
baseUrl: 'https://api.example.com',
|
|
176
|
+
validateStatus: status => status >= 200 && status < 400
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const response = await httpClient
|
|
180
|
+
.get('/users/42')
|
|
181
|
+
.validateStatus(status => status === 200 || status === 404)
|
|
182
|
+
.asJson<IUser | null>()
|
|
183
|
+
.execute();
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Предикат запроса заменяет предикат клиента. Чтобы для отдельного запроса вернуть стандартное поведение поверх клиентской настройки, его нужно задать явно:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
const response = await httpClient
|
|
190
|
+
.get('/health')
|
|
191
|
+
.validateStatus(status => status >= 200 && status < 300)
|
|
192
|
+
.execute();
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Предикат вызывается один раз с числовым статусом ответа. Если он возвращает `true`, тело разбирается в выбранном через `as*()` формате и ответ проходит через `onResponse`. Если он возвращает `false`, адаптер создаёт `HttpResponseError`, а тело ошибки пытается разобрать как JSON или текст. Исключение из предиката передаётся без замены в `onResponseError` и вызывающему коду.
|
|
196
|
+
|
|
197
|
+
## Отмена и таймауты
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
import { isAbortError } from '@byndyusoft-ui/http-client';
|
|
201
|
+
|
|
202
|
+
const controller = new AbortController();
|
|
203
|
+
const request = httpClient.get('/report').signal(controller.signal).timeout(5_000).asBlob().execute();
|
|
204
|
+
|
|
205
|
+
controller.abort('Navigation changed');
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await request;
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (!isAbortError(error)) {
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`timeout()` принимает конечное неотрицательное число миллисекунд. Значение `0` отключает таймаут, включая заданный в `HttpClient` или `XhrAdapter`. Пользовательская отмена приводит к `AbortError`, истечение таймаута — к `TimeoutError` с фактическим значением в поле `timeout`.
|
|
217
|
+
|
|
218
|
+
## Адаптеры
|
|
219
|
+
|
|
220
|
+
### Fetch
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import { FetchAdapter, HttpClient } from '@byndyusoft-ui/http-client';
|
|
224
|
+
|
|
225
|
+
const httpClient = new HttpClient({
|
|
226
|
+
adapter: new FetchAdapter({
|
|
227
|
+
cache: 'no-store',
|
|
228
|
+
mode: 'cors',
|
|
229
|
+
redirect: 'follow'
|
|
230
|
+
})
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Fetch-специфичные параметры задаются в конструкторе адаптера:
|
|
235
|
+
|
|
236
|
+
| Настройка | Назначение |
|
|
237
|
+
| ---------------- | ------------------------------------------------- |
|
|
238
|
+
| `cache` | режим браузерного HTTP-кэша |
|
|
239
|
+
| `credentials` | базовый режим `omit`, `same-origin` или `include` |
|
|
240
|
+
| `integrity` | Subresource Integrity |
|
|
241
|
+
| `keepalive` | разрешает запросу пережить закрытие страницы |
|
|
242
|
+
| `mode` | `cors`, `no-cors` или `same-origin` |
|
|
243
|
+
| `redirect` | `follow`, `error` или `manual` |
|
|
244
|
+
| `referrer` | значение referrer |
|
|
245
|
+
| `referrerPolicy` | политика передачи referrer |
|
|
246
|
+
|
|
247
|
+
`mode: 'navigate'` запрещён для программного Fetch. `cache: 'only-if-cached'` допустим только вместе с `mode: 'same-origin'`. При `redirect: 'manual'` браузер может вернуть `opaque-redirect` со статусом `0`; продолжить такой редирект вручную нельзя.
|
|
248
|
+
|
|
249
|
+
### XMLHttpRequest
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
import { HttpClient, XhrAdapter } from '@byndyusoft-ui/http-client';
|
|
253
|
+
|
|
254
|
+
const httpClient = new HttpClient({
|
|
255
|
+
adapter: new XhrAdapter({
|
|
256
|
+
mimeType: 'application/json',
|
|
257
|
+
timeout: 10_000,
|
|
258
|
+
withCredentials: true,
|
|
259
|
+
onDownloadProgress: (event, config) => {
|
|
260
|
+
console.log(config.url, event.loaded, event.total);
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
XHR полезен для сценариев, которым нужны возможности `XMLHttpRequest`. `onDownloadProgress` и `onUploadProgress` получают нативный `ProgressEvent` и итоговую конфигурацию запроса. События передаются без throttling; если `lengthComputable === false`, значение `total` нельзя считать достоверным.
|
|
267
|
+
|
|
268
|
+
| Настройка | Назначение |
|
|
269
|
+
| -------------------- | --------------------------------------------------- |
|
|
270
|
+
| `mimeType` | переопределяет MIME type через `overrideMimeType()` |
|
|
271
|
+
| `responseType` | формат ответа по умолчанию для запросов без `as*()` |
|
|
272
|
+
| `timeout` | таймаут по умолчанию |
|
|
273
|
+
| `withCredentials` | credentials по умолчанию |
|
|
274
|
+
| `onDownloadProgress` | события загрузки ответа |
|
|
275
|
+
| `onUploadProgress` | события отправки тела |
|
|
276
|
+
|
|
277
|
+
Обработчики задаются на весь экземпляр адаптера. Для изолированной загрузки можно создать scoped-клиент:
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
interface IUploadResult {
|
|
281
|
+
id: string;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const uploadClient = httpClient.withAdapter(
|
|
285
|
+
new XhrAdapter({
|
|
286
|
+
onUploadProgress: (event, config) => {
|
|
287
|
+
if (event.lengthComputable) {
|
|
288
|
+
console.log(config.url, event.loaded / event.total);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
await uploadClient.post('/files').body(file).asJson<IUploadResult>().execute();
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
`withAdapter()` возвращает новый клиент со снимком текущих defaults и hooks. Исходный клиент и его адаптер не изменяются; последующая замена hooks в одном клиенте не влияет на другой.
|
|
298
|
+
|
|
299
|
+
Обработчик upload подключается только при наличии `onUploadProgress` и фактического тела запроса. Для cross-origin запроса такая подписка принудительно включает CORS preflight согласно [спецификации XMLHttpRequest](<https://xhr.spec.whatwg.org/#the-send()-method>), поэтому сервер должен корректно обрабатывать `OPTIONS`. `FetchAdapter` не предоставляет стандартный upload progress.
|
|
300
|
+
|
|
301
|
+
Текущий `asStream()` для XHR не является настоящим сетевым стримом: полученный текст накапливается в памяти. Прогресс загрузки и stream могут использоваться одновременно.
|
|
302
|
+
|
|
303
|
+
### Пользовательский адаптер
|
|
304
|
+
|
|
305
|
+
Транспорт можно реализовать самостоятельно через `IHttpClientAdapter`. Адаптер получает полностью объединённый `IHttpRequestConfig` и должен вернуть `IHttpResponse<T>` либо выбросить подходящую ошибку:
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
import {
|
|
309
|
+
FetchAdapter,
|
|
310
|
+
HttpClient,
|
|
311
|
+
type IHttpClientAdapter,
|
|
312
|
+
type IHttpRequestConfig,
|
|
313
|
+
type IHttpResponse
|
|
314
|
+
} from '@byndyusoft-ui/http-client';
|
|
315
|
+
|
|
316
|
+
class LoggingAdapter implements IHttpClientAdapter {
|
|
317
|
+
public constructor(private readonly inner = new FetchAdapter()) {}
|
|
318
|
+
|
|
319
|
+
public request<T>(config: IHttpRequestConfig): Promise<IHttpResponse<T>> {
|
|
320
|
+
console.log(config.method, config.url);
|
|
321
|
+
|
|
322
|
+
return this.inner.request<T>(config);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const httpClient = new HttpClient({ adapter: new LoggingAdapter() });
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
`validateStatus` входит в конфигурацию запроса и исполняется транспортом. Пользовательский адаптер, который не делегирует выполнение Fetch- или XHR-адаптеру, должен самостоятельно применить предикат и сформировать `HttpResponseError` для отклонённого статуса.
|
|
330
|
+
|
|
331
|
+
## Приоритет конфигурации
|
|
332
|
+
|
|
333
|
+
Общие настройки разрешаются в следующем порядке:
|
|
334
|
+
|
|
335
|
+
1. Настройки конкретного запроса в builder.
|
|
336
|
+
2. Настройки `HttpClient`.
|
|
337
|
+
3. Значения по умолчанию адаптера.
|
|
338
|
+
|
|
339
|
+
Предикат `validateStatus` запроса имеет приоритет над предикатом клиента; при отсутствии обоих используется диапазон `200–299`. Для XHR общий порядок применяется также к `timeout`, `responseType` и `withCredentials`. Заголовки, params и `baseUrl` не имеют значений по умолчанию на уровне адаптера. Специфичные для Fetch и XHR параметры задаются только в конструкторах соответствующих адаптеров.
|
|
340
|
+
|
|
341
|
+
## Credentials и CORS
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
const response = await httpClient.get('/profile').withCredentials(true).asJson<IUser>().execute();
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Для Fetch `withCredentials` преобразуется в `credentials`:
|
|
348
|
+
|
|
349
|
+
- `true` → `include`;
|
|
350
|
+
- `false` → `same-origin`;
|
|
351
|
+
- отсутствие значения → `credentials` адаптера или `same-origin`.
|
|
352
|
+
|
|
353
|
+
Для XHR `true` устанавливает `xhr.withCredentials = true`, а `false` — `false`. При отсутствии значения используется настройка `XhrAdapter` или `false`.
|
|
354
|
+
|
|
355
|
+
Клиент не может самостоятельно разрешить CORS. Сервер должен возвращать подходящие `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`, `Access-Control-Allow-Headers` и, для credentialed-запросов, `Access-Control-Allow-Credentials: true`. Cookie также подчиняются правилам `SameSite` и `Secure` браузера.
|
|
356
|
+
|
|
357
|
+
## Ошибки
|
|
358
|
+
|
|
359
|
+
Все ошибки пакета наследуются от `HttpClientError`. Базовый класс содержит `message`, стандартное поле `cause` и, если запрос уже был сформирован, `config`.
|
|
360
|
+
|
|
361
|
+
| Ошибка | Причина | Дополнительные поля |
|
|
362
|
+
| ------------------------- | ------------------------------------------ | ----------------------------------------- |
|
|
363
|
+
| `RequestBuilderError` | некорректные настройки builder | `code` |
|
|
364
|
+
| `RequestPreparationError` | не удалось подготовить транспортный запрос | `cause`, `config` |
|
|
365
|
+
| `HttpResponseError` | статус отклонён функцией `validateStatus` | `status`, `statusText`, `headers`, `data` |
|
|
366
|
+
| `ParseError` | не удалось декодировать успешный ответ | `responseType`, `raw`, `cause`, `config` |
|
|
367
|
+
| `NetworkError` | сетевая ошибка | `cause`, `config` |
|
|
368
|
+
| `AbortError` | запрос отменён через `AbortSignal` | `cause`, `config` |
|
|
369
|
+
| `TimeoutError` | истёк таймаут | `timeout`, `cause`, `config` |
|
|
370
|
+
|
|
371
|
+
Для каждого класса экспортируется guard: `isHttpClientError`, `isRequestBuilderError`, `isRequestPreparationError`, `isHttpResponseError`, `isParseError`, `isNetworkError`, `isAbortError` и `isTimeoutError`. Они используют `instanceof`; `isHttpClientError()` позволяет одним условием обработать любую ошибку пакета.
|
|
372
|
+
|
|
373
|
+
Коды ошибок builder экспортируются в `REQUEST_BUILDER_ERROR_CODES` и доступны через `RequestBuilderError.code`.
|
|
374
|
+
|
|
375
|
+
При отклонённом статусе адаптер пытается разобрать тело как JSON, затем как текст. Пустое или недоступное тело даёт `data === undefined`. Тип данных ошибки не связан с типом успешного ответа и проверяется отдельно:
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
import { isHttpResponseError } from '@byndyusoft-ui/http-client';
|
|
379
|
+
|
|
380
|
+
interface IValidationError {
|
|
381
|
+
message: string;
|
|
382
|
+
errors: Record<string, string[]>;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function isValidationError(data: unknown): data is IValidationError {
|
|
386
|
+
if (typeof data !== 'object' || data === null) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const candidate = data as Partial<IValidationError>;
|
|
391
|
+
|
|
392
|
+
if (
|
|
393
|
+
typeof candidate.message !== 'string' ||
|
|
394
|
+
typeof candidate.errors !== 'object' ||
|
|
395
|
+
candidate.errors === null ||
|
|
396
|
+
Array.isArray(candidate.errors)
|
|
397
|
+
) {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return Object.values(candidate.errors).every(
|
|
402
|
+
value => Array.isArray(value) && value.every(item => typeof item === 'string')
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
await httpClient.get('/users/1').asJson<IUser>().execute();
|
|
408
|
+
} catch (error) {
|
|
409
|
+
if (isHttpResponseError(error) && isValidationError(error.data) && (error.status === 400 || error.status === 422)) {
|
|
410
|
+
console.error(error.data.message, error.data.errors);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
`isHttpResponseError(error)` проверяет класс ошибки и открывает доступ к `data` типа `unknown`. Схема тела принадлежит конкретному API, поэтому проверяется отдельным пользовательским type guard. После обеих проверок `error.data` имеет точный тип `IValidationError` без `undefined`.
|
|
416
|
+
|
|
417
|
+
## Hooks
|
|
418
|
+
|
|
419
|
+
Hooks можно передать в конструктор или назначить методами клиента. Цепочка выполняется в следующем порядке:
|
|
420
|
+
|
|
421
|
+
1. Настройки клиента и builder объединяются.
|
|
422
|
+
2. `onRequest` получает итоговую конфигурацию и обязан вернуть конфигурацию для продолжения.
|
|
423
|
+
3. Если `onRequest` выбрасывает ошибку или возвращает невалидную конфигурацию, вызывается `onRequestError`. Возвращённая конфигурация восстанавливает запрос; `undefined` повторно выбрасывает исходную ошибку.
|
|
424
|
+
4. Адаптер выполняет запрос.
|
|
425
|
+
5. Успешный ответ проходит через `onResponse`, а его возвращаемое значение передаётся вызывающему коду.
|
|
426
|
+
6. Ошибка адаптера или `onResponse` передаётся в `onResponseError`. Возвращённый ответ восстанавливает выполнение; `undefined` повторно выбрасывает исходную ошибку. Восстановленный ответ повторно через `onResponse` не проходит.
|
|
427
|
+
|
|
428
|
+
Request-ошибки, возникшие до вызова адаптера, не передаются в response hooks.
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
const httpClient = new HttpClient({
|
|
432
|
+
onRequest: config => ({
|
|
433
|
+
...config,
|
|
434
|
+
headers: { ...config.headers, Authorization: `Bearer ${token}` }
|
|
435
|
+
}),
|
|
436
|
+
onResponse: response => response,
|
|
437
|
+
onResponseError: error => {
|
|
438
|
+
throw error;
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Поддерживаются `onRequest`, `onRequestError`, `onResponse` и `onResponseError`. Повторное назначение hook заменяет предыдущее значение.
|
|
444
|
+
|
|
445
|
+
```ts
|
|
446
|
+
httpClient
|
|
447
|
+
.onRequest(addAuthorization)
|
|
448
|
+
.onRequestError(recoverRequest)
|
|
449
|
+
.onResponse(normalizeResponse)
|
|
450
|
+
.onResponseError(recoverResponse);
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Методы возвращают тот же клиент для построения цепочки вызовов. `withAdapter()` копирует ссылки на текущие hooks в новый клиент; последующая замена hook в одном экземпляре не влияет на другой.
|
|
454
|
+
|
|
455
|
+
## Публичные типы и константы
|
|
456
|
+
|
|
457
|
+
Основные типы доступны из корня пакета: `IHttpClientOptions`, `IHttpClientAdapter`, `IHttpRequestConfig`, `IHttpResponse`, `IFetchAdapterOptions`, `IXhrAdapterOptions`, `THttpHeaders`, `THttpParams`, `THttpRequestBody`, `THttpMethod`, `THttpResponseType`, `TValidateStatus`, типы hooks и опций ошибок.
|
|
458
|
+
|
|
459
|
+
Также экспортируются `HTTP_METHODS`, `HTTP_STATUS_CODES`, `HTTP_RESPONSE_TYPES` и `REQUEST_BUILDER_ERROR_CODES`. Внутренние asserts и utilities не входят в корневой публичный API.
|
|
460
|
+
|
|
461
|
+
## Ограничения текущей версии
|
|
462
|
+
|
|
463
|
+
- Встроенных повторных запросов нет; retry должен выполняться отдельным слоем оркестрации.
|
|
464
|
+
- Для каждой фазы хранится только один hook, а повторное назначение заменяет предыдущий.
|
|
465
|
+
- `asJson<T>()` задаёт ожидаемый TypeScript-тип, но не проверяет схему данных во время выполнения.
|
|
466
|
+
- Формат ответа `FormData` не поддерживается; `FormData` можно использовать только как тело запроса.
|
|
467
|
+
- Fetch stream является нативным потоком. После возврата `ReadableStream` таймаут Fetch больше не контролирует его чтение.
|
|
468
|
+
- XHR stream формируется из накопленного `responseText`, поэтому весь текст остаётся в памяти. Promise успешного stream-запроса может разрешиться после получения заголовков, а последующая сетевая ошибка, abort или timeout передаётся через ошибку самого потока. Этот пограничный сценарий пока считается экспериментальным и может быть уточнён до стабильной версии.
|
|
469
|
+
- Поведение CORS с credentials, cookies, redirects и `keepalive` зависит от браузера и должно проверяться интеграционно в целевом окружении.
|
|
470
|
+
|
|
471
|
+
## Публикация альфа-версии
|
|
472
|
+
|
|
473
|
+
После слияния изменений в `master` и сборки пакет публикуется из корня репозитория с явным тегом `alpha`:
|
|
474
|
+
|
|
475
|
+
```bash
|
|
476
|
+
npm publish --workspace @byndyusoft-ui/http-client --access public --tag alpha
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
При публикации через Changesets также нужно передавать `--tag alpha`: Changesets задаёт тег явно и по умолчанию использует `latest`. Указанный тег применяется ко всем пакетам, публикуемым в этом запуске.
|