@@ -8,12 +8,17 @@ import {
8
8
import { HttpClient , HttpClientError , HttpClientResponse } from './http-client' ;
9
9
import { ParseError } from '../exceptions/parse-error' ;
10
10
11
+ interface FetchHttpClientOptions extends RequestInit {
12
+ timeout ?: number ;
13
+ }
14
+
15
+ const DEFAULT_FETCH_TIMEOUT = 60_000 ; // 60 seconds
11
16
export class FetchHttpClient extends HttpClient implements HttpClientInterface {
12
17
private readonly _fetchFn ;
13
18
14
19
constructor (
15
20
readonly baseURL : string ,
16
- readonly options ?: RequestInit ,
21
+ readonly options ?: FetchHttpClientOptions ,
17
22
fetchFn ?: typeof fetch ,
18
23
) {
19
24
super ( baseURL , options ) ;
@@ -167,50 +172,91 @@ export class FetchHttpClient extends HttpClient implements HttpClientInterface {
167
172
168
173
const requestBody = body || ( methodHasPayload ? '' : undefined ) ;
169
174
170
- const { 'User-Agent' : userAgent } = this . options ?. headers as RequestHeaders ;
175
+ const { 'User-Agent' : userAgent } = ( this . options ?. headers ||
176
+ { } ) as RequestHeaders ;
177
+
178
+ // Create AbortController for timeout if configured
179
+ let abortController : AbortController | undefined ;
180
+ let timeoutId : ReturnType < typeof setTimeout > | undefined ;
181
+
182
+ // Access timeout from the options with default of 60 seconds
183
+ const timeout = this . options ?. timeout ?? DEFAULT_FETCH_TIMEOUT ; // Default 60 seconds
184
+ abortController = new AbortController ( ) ;
185
+ timeoutId = setTimeout ( ( ) => {
186
+ abortController ?. abort ( ) ;
187
+ } , timeout ) ;
188
+
189
+ try {
190
+ const res = await this . _fetchFn ( url , {
191
+ method,
192
+ headers : {
193
+ Accept : 'application/json, text/plain, */*' ,
194
+ 'Content-Type' : 'application/json' ,
195
+ ...this . options ?. headers ,
196
+ ...headers ,
197
+ 'User-Agent' : this . addClientToUserAgent (
198
+ ( userAgent || 'workos-node' ) . toString ( ) ,
199
+ ) ,
200
+ } ,
201
+ body : requestBody ,
202
+ signal : abortController ?. signal ,
203
+ } ) ;
171
204
172
- const res = await this . _fetchFn ( url , {
173
- method,
174
- headers : {
175
- Accept : 'application/json, text/plain, */*' ,
176
- 'Content-Type' : 'application/json' ,
177
- ...this . options ?. headers ,
178
- ...headers ,
179
- 'User-Agent' : this . addClientToUserAgent ( userAgent . toString ( ) ) ,
180
- } ,
181
- body : requestBody ,
182
- } ) ;
205
+ // Clear timeout if request completed successfully
206
+ if ( timeoutId ) {
207
+ clearTimeout ( timeoutId ) ;
208
+ }
183
209
184
- if ( ! res . ok ) {
185
- const requestID = res . headers . get ( 'X-Request-ID' ) ?? '' ;
186
- const rawBody = await res . text ( ) ;
210
+ if ( ! res . ok ) {
211
+ const requestID = res . headers . get ( 'X-Request-ID' ) ?? '' ;
212
+ const rawBody = await res . text ( ) ;
213
+
214
+ let responseJson : any ;
215
+
216
+ try {
217
+ responseJson = JSON . parse ( rawBody ) ;
218
+ } catch ( error ) {
219
+ if ( error instanceof SyntaxError ) {
220
+ throw new ParseError ( {
221
+ message : error . message ,
222
+ rawBody,
223
+ requestID,
224
+ rawStatus : res . status ,
225
+ } ) ;
226
+ }
227
+ throw error ;
228
+ }
187
229
188
- let responseJson : any ;
230
+ throw new HttpClientError ( {
231
+ message : res . statusText ,
232
+ response : {
233
+ status : res . status ,
234
+ headers : res . headers ,
235
+ data : responseJson ,
236
+ } ,
237
+ } ) ;
238
+ }
239
+ return new FetchHttpClientResponse ( res ) ;
240
+ } catch ( error ) {
241
+ // Clear timeout if request failed
242
+ if ( timeoutId ) {
243
+ clearTimeout ( timeoutId ) ;
244
+ }
189
245
190
- try {
191
- responseJson = JSON . parse ( rawBody ) ;
192
- } catch ( error ) {
193
- if ( error instanceof SyntaxError ) {
194
- throw new ParseError ( {
195
- message : error . message ,
196
- rawBody,
197
- requestID,
198
- rawStatus : res . status ,
199
- } ) ;
200
- }
201
- throw error ;
246
+ // Handle timeout errors
247
+ if ( error instanceof Error && error . name === 'AbortError' ) {
248
+ throw new HttpClientError ( {
249
+ message : `Request timeout after ${ timeout } ms` ,
250
+ response : {
251
+ status : 408 ,
252
+ headers : { } ,
253
+ data : { error : 'Request timeout' } ,
254
+ } ,
255
+ } ) ;
202
256
}
203
257
204
- throw new HttpClientError ( {
205
- message : res . statusText ,
206
- response : {
207
- status : res . status ,
208
- headers : res . headers ,
209
- data : responseJson ,
210
- } ,
211
- } ) ;
258
+ throw error ;
212
259
}
213
- return new FetchHttpClientResponse ( res ) ;
214
260
}
215
261
216
262
private async fetchRequestWithRetry (
0 commit comments