1
- import { default as FormData } from "form-data" ;
2
1
import qs from "qs" ;
3
2
import { RUNTIME } from "../runtime" ;
4
3
import { APIResponse } from "./APIResponse" ;
@@ -16,6 +15,7 @@ export declare namespace Fetcher {
16
15
timeoutMs ?: number ;
17
16
maxRetries ?: number ;
18
17
withCredentials ?: boolean ;
18
+ abortSignal ?: AbortSignal ;
19
19
responseType ?: "json" | "blob" | "streaming" | "text" ;
20
20
}
21
21
@@ -67,13 +67,28 @@ async function fetcherImpl<R = unknown>(args: Fetcher.Args): Promise<APIResponse
67
67
: args . url ;
68
68
69
69
let body : BodyInit | undefined = undefined ;
70
- if ( args . body instanceof FormData ) {
71
- // @ts -expect-error
72
- body = args . body ;
73
- } else if ( args . body instanceof Uint8Array ) {
74
- body = args . body ;
70
+ const maybeStringifyBody = ( body : any ) => {
71
+ if ( body instanceof Uint8Array ) {
72
+ return body ;
73
+ } else {
74
+ return JSON . stringify ( body ) ;
75
+ }
76
+ } ;
77
+
78
+ if ( RUNTIME . type === "node" ) {
79
+ if ( args . body instanceof ( await import ( "formdata-node" ) ) . FormData ) {
80
+ // @ts -expect-error
81
+ body = args . body ;
82
+ } else {
83
+ body = maybeStringifyBody ( args . body ) ;
84
+ }
75
85
} else {
76
- body = JSON . stringify ( args . body ) ;
86
+ if ( args . body instanceof ( await import ( "form-data" ) ) . default ) {
87
+ // @ts -expect-error
88
+ body = args . body ;
89
+ } else {
90
+ body = maybeStringifyBody ( args . body ) ;
91
+ }
77
92
}
78
93
79
94
// In Node.js environments, the SDK always uses`node-fetch`.
@@ -89,21 +104,33 @@ async function fetcherImpl<R = unknown>(args: Fetcher.Args): Promise<APIResponse
89
104
: ( ( await import ( "node-fetch" ) ) . default as any ) ;
90
105
91
106
const makeRequest = async ( ) : Promise < Response > => {
92
- const controller = new AbortController ( ) ;
93
- let abortId = undefined ;
107
+ const signals : AbortSignal [ ] = [ ] ;
108
+
109
+ // Add timeout signal
110
+ let timeoutAbortId : NodeJS . Timeout | undefined = undefined ;
94
111
if ( args . timeoutMs != null ) {
95
- abortId = setTimeout ( ( ) => controller . abort ( ) , args . timeoutMs ) ;
112
+ const { signal, abortId } = getTimeoutSignal ( args . timeoutMs ) ;
113
+ timeoutAbortId = abortId ;
114
+ signals . push ( signal ) ;
96
115
}
116
+
117
+ // Add arbitrary signal
118
+ if ( args . abortSignal != null ) {
119
+ signals . push ( args . abortSignal ) ;
120
+ }
121
+
97
122
const response = await fetchFn ( url , {
98
123
method : args . method ,
99
124
headers,
100
125
body,
101
- signal : controller . signal ,
126
+ signal : anySignal ( signals ) ,
102
127
credentials : args . withCredentials ? "include" : undefined ,
103
128
} ) ;
104
- if ( abortId != null ) {
105
- clearTimeout ( abortId ) ;
129
+
130
+ if ( timeoutAbortId != null ) {
131
+ clearTimeout ( timeoutAbortId ) ;
106
132
}
133
+
107
134
return response ;
108
135
} ;
109
136
@@ -167,7 +194,15 @@ async function fetcherImpl<R = unknown>(args: Fetcher.Args): Promise<APIResponse
167
194
} ;
168
195
}
169
196
} catch ( error ) {
170
- if ( error instanceof Error && error . name === "AbortError" ) {
197
+ if ( args . abortSignal != null && args . abortSignal . aborted ) {
198
+ return {
199
+ ok : false ,
200
+ error : {
201
+ reason : "unknown" ,
202
+ errorMessage : "The user aborted a request" ,
203
+ } ,
204
+ } ;
205
+ } else if ( error instanceof Error && error . name === "AbortError" ) {
171
206
return {
172
207
ok : false ,
173
208
error : {
@@ -194,4 +229,43 @@ async function fetcherImpl<R = unknown>(args: Fetcher.Args): Promise<APIResponse
194
229
}
195
230
}
196
231
232
+ const TIMEOUT = "timeout" ;
233
+
234
+ function getTimeoutSignal ( timeoutMs : number ) : { signal : AbortSignal ; abortId : NodeJS . Timeout } {
235
+ const controller = new AbortController ( ) ;
236
+ const abortId = setTimeout ( ( ) => controller . abort ( TIMEOUT ) , timeoutMs ) ;
237
+ return { signal : controller . signal , abortId } ;
238
+ }
239
+
240
+ /**
241
+ * Returns an abort signal that is getting aborted when
242
+ * at least one of the specified abort signals is aborted.
243
+ *
244
+ * Requires at least node.js 18.
245
+ */
246
+ function anySignal ( ...args : AbortSignal [ ] | [ AbortSignal [ ] ] ) : AbortSignal {
247
+ // Allowing signals to be passed either as array
248
+ // of signals or as multiple arguments.
249
+ const signals = < AbortSignal [ ] > ( args . length === 1 && Array . isArray ( args [ 0 ] ) ? args [ 0 ] : args ) ;
250
+
251
+ const controller = new AbortController ( ) ;
252
+
253
+ for ( const signal of signals ) {
254
+ if ( signal . aborted ) {
255
+ // Exiting early if one of the signals
256
+ // is already aborted.
257
+ controller . abort ( ( signal as any ) ?. reason ) ;
258
+ break ;
259
+ }
260
+
261
+ // Listening for signals and removing the listeners
262
+ // when at least one symbol is aborted.
263
+ signal . addEventListener ( "abort" , ( ) => controller . abort ( ( signal as any ) ?. reason ) , {
264
+ signal : controller . signal ,
265
+ } ) ;
266
+ }
267
+
268
+ return controller . signal ;
269
+ }
270
+
197
271
export const fetcher : FetchFunction = fetcherImpl ;
0 commit comments