@hey-api/openapi-ts 0.96.1 → 0.97.1

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.
@@ -1,10 +1,10 @@
1
- import type { HTTPError, Options as KyOptions } from 'ky';
2
- import ky from 'ky';
1
+ import type { KyResponse, Options as KyOptions } from 'ky';
2
+ import ky, { isHTTPError } from 'ky';
3
3
 
4
4
  import { createSseClient } from '../core/serverSentEvents';
5
5
  import type { HttpMethod } from '../core/types';
6
6
  import { getValidRequestBody } from '../core/utils';
7
- import type { Client, Config, RequestOptions, ResolvedRequestOptions, RetryOptions } from './types';
7
+ import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types';
8
8
  import type { Middleware } from './utils';
9
9
  import {
10
10
  buildUrl,
@@ -41,6 +41,11 @@ export const createClient = (config: Config = {}): Client => {
41
41
  ...options,
42
42
  headers: mergeHeaders(_config.headers, options.headers),
43
43
  ky: options.ky ?? _config.ky ?? ky,
44
+ // deep merge kyOptions to ensure base _config is being respected
45
+ kyOptions: {
46
+ ..._config.kyOptions,
47
+ ...options.kyOptions,
48
+ },
44
49
  serializedBody: undefined as string | undefined,
45
50
  };
46
51
 
@@ -95,7 +100,7 @@ export const createClient = (config: Config = {}): Client => {
95
100
 
96
101
  for (const fn of interceptorsMiddleware.error.fns) {
97
102
  if (fn) {
98
- finalError = (await fn(error, response, request, opts)) as string;
103
+ finalError = (await fn(finalError, response, request, opts)) as string;
99
104
  }
100
105
  }
101
106
 
@@ -114,161 +119,192 @@ export const createClient = (config: Config = {}): Client => {
114
119
  };
115
120
 
116
121
  const request: Client['request'] = async (options) => {
117
- const { opts, url } = await beforeRequest(options);
122
+ const throwOnError = options.throwOnError ?? _config.throwOnError;
123
+ const responseStyle = options.responseStyle ?? _config.responseStyle;
118
124
 
119
- const kyInstance = opts.ky!;
120
-
121
- const validBody = getValidRequestBody(opts);
122
-
123
- const kyOptions: KyOptions = {
124
- body: validBody as BodyInit,
125
- cache: opts.cache,
126
- credentials: opts.credentials,
127
- headers: opts.headers,
128
- integrity: opts.integrity,
129
- keepalive: opts.keepalive,
130
- method: opts.method as KyOptions['method'],
131
- mode: opts.mode,
132
- redirect: 'follow',
133
- referrer: opts.referrer,
134
- referrerPolicy: opts.referrerPolicy,
135
- signal: opts.signal,
136
- throwHttpErrors: opts.throwOnError ?? false,
137
- timeout: opts.timeout,
138
- ...opts.kyOptions,
139
- };
125
+ let request: Request | undefined;
126
+ let response: KyResponse | undefined;
127
+ let errorInterceptorsInvoked = false;
140
128
 
141
- if (opts.retry && typeof opts.retry === 'object') {
142
- const retryOpts = opts.retry as RetryOptions;
143
- kyOptions.retry = {
144
- limit: retryOpts.limit ?? 2,
145
- methods: retryOpts.methods as Array<
146
- 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete' | 'options' | 'trace'
147
- >,
148
- statusCodes: retryOpts.statusCodes,
129
+ try {
130
+ const { opts, url } = await beforeRequest(options);
131
+
132
+ const kyInstance = opts.ky!;
133
+
134
+ const validBody = getValidRequestBody(opts);
135
+
136
+ const kyOptions: KyOptions = {
137
+ body: validBody as BodyInit,
138
+ ...(opts.cache !== undefined ? { cache: opts.cache } : {}),
139
+ ...(opts.credentials !== undefined ? { credentials: opts.credentials } : {}),
140
+ ...(opts.headers !== undefined ? { headers: opts.headers } : {}),
141
+ ...(opts.integrity !== undefined ? { integrity: opts.integrity } : {}),
142
+ ...(opts.keepalive !== undefined ? { keepalive: opts.keepalive } : {}),
143
+ ...(opts.method !== undefined ? { method: opts.method } : {}),
144
+ ...(opts.mode !== undefined ? { mode: opts.mode } : {}),
145
+ redirect: opts.redirect ?? 'follow',
146
+ ...(opts.referrer !== undefined ? { referrer: opts.referrer } : {}),
147
+ ...(opts.referrerPolicy !== undefined ? { referrerPolicy: opts.referrerPolicy } : {}),
148
+ ...(opts.signal !== undefined ? { signal: opts.signal } : {}),
149
+ throwHttpErrors: opts.throwOnError ?? false,
150
+ ...(opts.timeout !== undefined ? { timeout: opts.timeout } : {}),
151
+ ...opts.kyOptions,
152
+ retry: opts.retry ?? opts.kyOptions?.retry ?? 2,
149
153
  };
150
- }
151
154
 
152
- let request = new Request(url, {
153
- body: kyOptions.body as BodyInit,
154
- headers: kyOptions.headers as HeadersInit,
155
- method: kyOptions.method,
156
- });
155
+ request = new Request(url, {
156
+ body: kyOptions.body,
157
+ headers: kyOptions.headers as HeadersInit,
158
+ method: kyOptions.method,
159
+ });
157
160
 
158
- for (const fn of interceptors.request.fns) {
159
- if (fn) {
160
- request = await fn(request, opts);
161
+ for (const fn of interceptors.request.fns) {
162
+ if (fn) {
163
+ request = await fn(request, opts);
164
+ }
161
165
  }
162
- }
163
166
 
164
- let response: Response;
167
+ try {
168
+ response = await kyInstance(request, kyOptions);
169
+ } catch (error) {
170
+ if (isHTTPError(error)) {
171
+ response = error.response;
165
172
 
166
- try {
167
- response = await kyInstance(request, kyOptions);
168
- } catch (error) {
169
- if (error && typeof error === 'object' && 'response' in error) {
170
- const httpError = error as HTTPError;
171
- response = httpError.response;
172
-
173
- for (const fn of interceptors.response.fns) {
174
- if (fn) {
175
- response = await fn(response, request, opts);
173
+ for (const fn of interceptors.response.fns) {
174
+ if (fn) {
175
+ response = await fn(response, request, opts);
176
+ }
176
177
  }
178
+
179
+ // parseErrorResponse will run error interceptors, and re-throw when
180
+ // throwOnError is true, which bubbles already intercepted error to
181
+ // outer catch. With this flag, we can avoid outer catch running interceptors again
182
+ errorInterceptorsInvoked = true;
183
+ return parseErrorResponse(response, request, opts, interceptors);
177
184
  }
178
185
 
179
- return parseErrorResponse(response, request, opts, interceptors);
186
+ throw error;
180
187
  }
181
188
 
182
- throw error;
183
- }
184
-
185
- for (const fn of interceptors.response.fns) {
186
- if (fn) {
187
- response = await fn(response, request, opts);
189
+ for (const fn of interceptors.response.fns) {
190
+ if (fn) {
191
+ response = await fn(response, request, opts);
192
+ }
188
193
  }
189
- }
190
194
 
191
- const result = {
192
- request,
193
- response,
194
- };
195
+ const result = {
196
+ request,
197
+ response,
198
+ };
195
199
 
196
- if (response.ok) {
197
- const parseAs =
198
- (opts.parseAs === 'auto'
199
- ? getParseAs(response.headers.get('Content-Type'))
200
- : opts.parseAs) ?? 'json';
200
+ if (response.ok) {
201
+ const parseAs =
202
+ (opts.parseAs === 'auto'
203
+ ? getParseAs(response.headers.get('Content-Type'))
204
+ : opts.parseAs) ?? 'json';
205
+
206
+ if (response.status === 204 || response.headers.get('Content-Length') === '0') {
207
+ let emptyData: any;
208
+ switch (parseAs) {
209
+ case 'arrayBuffer':
210
+ case 'blob':
211
+ case 'text':
212
+ emptyData = await response[parseAs]();
213
+ break;
214
+ case 'formData':
215
+ emptyData = new FormData();
216
+ break;
217
+ case 'stream':
218
+ emptyData = response.body;
219
+ break;
220
+ case 'json':
221
+ default:
222
+ emptyData = {};
223
+ break;
224
+ }
225
+ return opts.responseStyle === 'data'
226
+ ? emptyData
227
+ : {
228
+ data: emptyData,
229
+ ...result,
230
+ };
231
+ }
201
232
 
202
- if (response.status === 204 || response.headers.get('Content-Length') === '0') {
203
- let emptyData: any;
233
+ let data: any;
204
234
  switch (parseAs) {
205
235
  case 'arrayBuffer':
206
236
  case 'blob':
237
+ case 'formData':
207
238
  case 'text':
208
- emptyData = await response[parseAs]();
239
+ data = await response[parseAs]();
209
240
  break;
210
- case 'formData':
211
- emptyData = new FormData();
241
+ case 'json': {
242
+ // Some servers return 200 with no Content-Length and empty body.
243
+ // response.json() would throw; read as text and parse if non-empty.
244
+ const text = await response.text();
245
+ data = text ? JSON.parse(text) : {};
212
246
  break;
247
+ }
213
248
  case 'stream':
214
- emptyData = response.body;
215
- break;
216
- case 'json':
217
- default:
218
- emptyData = {};
219
- break;
249
+ return opts.responseStyle === 'data'
250
+ ? response.body
251
+ : {
252
+ data: response.body,
253
+ ...result,
254
+ };
220
255
  }
256
+
257
+ if (parseAs === 'json') {
258
+ if (opts.responseValidator) {
259
+ await opts.responseValidator(data);
260
+ }
261
+
262
+ if (opts.responseTransformer) {
263
+ data = await opts.responseTransformer(data);
264
+ }
265
+ }
266
+
221
267
  return opts.responseStyle === 'data'
222
- ? emptyData
268
+ ? data
223
269
  : {
224
- data: emptyData,
270
+ data,
225
271
  ...result,
226
272
  };
227
273
  }
228
274
 
229
- let data: any;
230
- switch (parseAs) {
231
- case 'arrayBuffer':
232
- case 'blob':
233
- case 'formData':
234
- case 'text':
235
- data = await response[parseAs]();
236
- break;
237
- case 'json': {
238
- // Some servers return 200 with no Content-Length and empty body.
239
- // response.json() would throw; read as text and parse if non-empty.
240
- const text = await response.text();
241
- data = text ? JSON.parse(text) : {};
242
- break;
243
- }
244
- case 'stream':
245
- return opts.responseStyle === 'data'
246
- ? response.body
247
- : {
248
- data: response.body,
249
- ...result,
250
- };
251
- }
275
+ // parseErrorResponse will run error interceptors, and re-throw when
276
+ // throwOnError is true, which bubbles already intercepted error to
277
+ // outer catch. With this flag, we can avoid outer catch running interceptors again
278
+ errorInterceptorsInvoked = true;
279
+ return parseErrorResponse(response, request, opts, interceptors);
280
+ } catch (error) {
281
+ let finalError = error;
252
282
 
253
- if (parseAs === 'json') {
254
- if (opts.responseValidator) {
255
- await opts.responseValidator(data);
283
+ // error may already be processed by parseErrorResponse, in this case
284
+ // we can skip running interceptors again
285
+ if (!errorInterceptorsInvoked) {
286
+ // run error interceptors for errors not already handled by parseErrorResponse
287
+ for (const fn of interceptors.error.fns) {
288
+ if (fn) {
289
+ finalError = await fn(finalError, response, request, options as ResolvedRequestOptions);
290
+ }
256
291
  }
257
292
 
258
- if (opts.responseTransformer) {
259
- data = await opts.responseTransformer(data);
260
- }
293
+ finalError = finalError || ({} as string);
261
294
  }
262
295
 
263
- return opts.responseStyle === 'data'
264
- ? data
296
+ if (throwOnError) {
297
+ throw finalError;
298
+ }
299
+
300
+ return responseStyle === 'data'
301
+ ? undefined
265
302
  : {
266
- data,
267
- ...result,
303
+ error: finalError,
304
+ request,
305
+ response,
268
306
  };
269
307
  }
270
-
271
- return parseErrorResponse(response, request, opts, interceptors);
272
308
  };
273
309
 
274
310
  const makeMethodFn = (method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
@@ -280,7 +316,6 @@ export const createClient = (config: Config = {}): Client => {
280
316
  ...opts,
281
317
  body: opts.body as BodyInit | null | undefined,
282
318
  fetch: globalThis.fetch,
283
- headers: opts.headers as unknown as Record<string, string>,
284
319
  method,
285
320
  onRequest: async (url, init) => {
286
321
  let request = new Request(url, init);
@@ -18,7 +18,6 @@ export type {
18
18
  RequestResult,
19
19
  ResolvedRequestOptions,
20
20
  ResponseStyle,
21
- RetryOptions,
22
21
  TDataShape,
23
22
  } from './types';
24
23
  export { createConfig, mergeHeaders } from './utils';
@@ -11,30 +11,22 @@ import type { Middleware } from './utils';
11
11
 
12
12
  export type ResponseStyle = 'data' | 'fields';
13
13
 
14
- export interface RetryOptions {
15
- /**
16
- * Maximum number of retry attempts
17
- *
18
- * @default 2
19
- */
20
- limit?: number;
21
- /**
22
- * HTTP methods to retry
23
- *
24
- * @default ['get', 'put', 'head', 'delete', 'options', 'trace']
25
- */
26
- methods?: Array<'get' | 'post' | 'put' | 'delete' | 'patch' | 'head' | 'options' | 'trace'>;
27
- /**
28
- * HTTP status codes to retry
29
- *
30
- * @default [408, 413, 429, 500, 502, 503, 504]
31
- */
32
- statusCodes?: number[];
33
- }
34
-
35
14
  export interface Config<T extends ClientOptions = ClientOptions>
36
15
  extends
37
- Omit<KyOptions, 'body' | 'headers' | 'method' | 'prefixUrl' | 'retry' | 'throwHttpErrors'>,
16
+ Pick<
17
+ KyOptions,
18
+ | 'cache'
19
+ | 'credentials'
20
+ | 'retry'
21
+ | 'signal'
22
+ | 'integrity'
23
+ | 'keepalive'
24
+ | 'mode'
25
+ | 'redirect'
26
+ | 'referrer'
27
+ | 'referrerPolicy'
28
+ | 'timeout'
29
+ >,
38
30
  CoreConfig {
39
31
  /**
40
32
  * Base URL for all requests made by this client.
@@ -43,6 +35,10 @@ export interface Config<T extends ClientOptions = ClientOptions>
43
35
  /**
44
36
  * Ky instance to use. You can use this option to provide a custom
45
37
  * ky instance.
38
+ *
39
+ * Note that the `prefixUrl` of your ky instance will be ignored, as we
40
+ * will always build the full URL and pass it to your ky instance. You
41
+ * should configure `baseUrl` instead.
46
42
  */
47
43
  ky?: typeof ky;
48
44
  /**
@@ -65,22 +61,12 @@ export interface Config<T extends ClientOptions = ClientOptions>
65
61
  * @default 'fields'
66
62
  */
67
63
  responseStyle?: ResponseStyle;
68
- /**
69
- * Retry configuration
70
- */
71
- retry?: RetryOptions;
72
64
  /**
73
65
  * Throw an error instead of returning it in the response?
74
66
  *
75
67
  * @default false
76
68
  */
77
69
  throwOnError?: T['throwOnError'];
78
- /**
79
- * Request timeout in milliseconds
80
- *
81
- * @default 10000
82
- */
83
- timeout?: number;
84
70
  }
85
71
 
86
72
  export interface RequestOptions<
@@ -157,8 +143,10 @@ export type RequestResult<
157
143
  error: TError extends Record<string, unknown> ? TError[keyof TError] : TError;
158
144
  }
159
145
  ) & {
160
- request: Request;
161
- response: Response;
146
+ /** request may be undefined, because error may be from building the request object itself */
147
+ request?: Request;
148
+ /** response may be undefined due to a network error where no response object is produced */
149
+ response?: Response;
162
150
  }
163
151
  >;
164
152
 
@@ -212,8 +212,10 @@ export const mergeHeaders = (
212
212
 
213
213
  type ErrInterceptor<Err, Res, Req, Options> = (
214
214
  error: Err,
215
- response: Res,
216
- request: Req,
215
+ /** response may be undefined due to a network error where no response object is produced */
216
+ response: Res | undefined,
217
+ /** request may be undefined, because error may be from building the request object itself */
218
+ request: Req | undefined,
217
219
  options: Options,
218
220
  ) => Err | Promise<Err>;
219
221