@@ -18,10 +18,10 @@ limitations under the License.
18
18
* This is an internal module. See {@link MatrixHttpApi} for the public class.
19
19
*/
20
20
21
- import { checkObjectHasKeys , encodeParams } from "../utils.ts" ;
21
+ import { checkObjectHasKeys , deepCopy , encodeParams } from "../utils.ts" ;
22
22
import { type TypedEventEmitter } from "../models/typed-event-emitter.ts" ;
23
23
import { Method } from "./method.ts" ;
24
- import { ConnectionError , MatrixError , TokenRefreshError , TokenRefreshLogoutError } from "./errors.ts" ;
24
+ import { ConnectionError , MatrixError , TokenRefreshError } from "./errors.ts" ;
25
25
import {
26
26
HttpApiEvent ,
27
27
type HttpApiEventHandlerMap ,
@@ -31,7 +31,7 @@ import {
31
31
} from "./interface.ts" ;
32
32
import { anySignal , parseErrorResponse , timeoutSignal } from "./utils.ts" ;
33
33
import { type QueryDict } from "../utils.ts" ;
34
- import { singleAsyncExecution } from "../utils/decorators .ts" ;
34
+ import { TokenRefresher , TokenRefreshOutcome } from "./refresh .ts" ;
35
35
36
36
interface TypedResponse < T > extends Response {
37
37
json ( ) : Promise < T > ;
@@ -43,14 +43,9 @@ export type ResponseType<T, O extends IHttpOpts> = O extends { json: false }
43
43
? T
44
44
: TypedResponse < T > ;
45
45
46
- const enum TokenRefreshOutcome {
47
- Success = "success" ,
48
- Failure = "failure" ,
49
- Logout = "logout" ,
50
- }
51
-
52
46
export class FetchHttpApi < O extends IHttpOpts > {
53
47
private abortController = new AbortController ( ) ;
48
+ private readonly tokenRefresher : TokenRefresher ;
54
49
55
50
public constructor (
56
51
private eventEmitter : TypedEventEmitter < HttpApiEvent , HttpApiEventHandlerMap > ,
@@ -59,6 +54,8 @@ export class FetchHttpApi<O extends IHttpOpts> {
59
54
checkObjectHasKeys ( opts , [ "baseUrl" , "prefix" ] ) ;
60
55
opts . onlyData = ! ! opts . onlyData ;
61
56
opts . useAuthorizationHeader = opts . useAuthorizationHeader ?? true ;
57
+
58
+ this . tokenRefresher = new TokenRefresher ( opts ) ;
62
59
}
63
60
64
61
public abort ( ) : void {
@@ -113,12 +110,6 @@ export class FetchHttpApi<O extends IHttpOpts> {
113
110
return this . requestOtherUrl ( method , fullUri , body , opts ) ;
114
111
}
115
112
116
- /**
117
- * Promise used to block authenticated requests during a token refresh to avoid repeated expected errors.
118
- * @private
119
- */
120
- private tokenRefreshPromise ?: Promise < unknown > ;
121
-
122
113
/**
123
114
* Perform an authorised request to the homeserver.
124
115
* @param method - The HTTP method e.g. "GET".
@@ -146,36 +137,45 @@ export class FetchHttpApi<O extends IHttpOpts> {
146
137
* @returns Rejects with an error if a problem occurred.
147
138
* This includes network problems and Matrix-specific error JSON.
148
139
*/
149
- public async authedRequest < T > (
140
+ public authedRequest < T > (
150
141
method : Method ,
151
142
path : string ,
152
- queryParams ? : QueryDict ,
143
+ queryParams : QueryDict = { } ,
153
144
body ?: Body ,
154
- paramOpts : IRequestOpts & { doNotAttemptTokenRefresh ?: boolean } = { } ,
145
+ paramOpts : IRequestOpts = { } ,
155
146
) : Promise < ResponseType < T , O > > {
156
- if ( ! queryParams ) queryParams = { } ;
147
+ return this . doAuthedRequest < T > ( 1 , method , path , queryParams , body , paramOpts ) ;
148
+ }
157
149
150
+ // Wrapper around public method authedRequest to allow for tracking retry attempt counts
151
+ private async doAuthedRequest < T > (
152
+ attempt : number ,
153
+ method : Method ,
154
+ path : string ,
155
+ queryParams : QueryDict ,
156
+ body ?: Body ,
157
+ paramOpts : IRequestOpts = { } ,
158
+ ) : Promise < ResponseType < T , O > > {
158
159
// avoid mutating paramOpts so they can be used on retry
159
- const opts = { ...paramOpts } ;
160
-
161
- // Await any ongoing token refresh before we build the headers/params
162
- await this . tokenRefreshPromise ;
160
+ const opts = deepCopy ( paramOpts ) ;
161
+ // we have to manually copy the abortSignal over as it is not a plain object
162
+ opts . abortSignal = paramOpts . abortSignal ;
163
163
164
- // Take a copy of the access token so we have a record of the token we used for this request if it fails
165
- const accessToken = this . opts . accessToken ;
166
- if ( accessToken ) {
164
+ // Take a snapshot of the current token state before we start the request so we can reference it if we error
165
+ const requestSnapshot = await this . tokenRefresher . prepareForRequest ( ) ;
166
+ if ( requestSnapshot . accessToken ) {
167
167
if ( this . opts . useAuthorizationHeader ) {
168
168
if ( ! opts . headers ) {
169
169
opts . headers = { } ;
170
170
}
171
171
if ( ! opts . headers . Authorization ) {
172
- opts . headers . Authorization = `Bearer ${ accessToken } ` ;
172
+ opts . headers . Authorization = `Bearer ${ requestSnapshot . accessToken } ` ;
173
173
}
174
174
if ( queryParams . access_token ) {
175
175
delete queryParams . access_token ;
176
176
}
177
177
} else if ( ! queryParams . access_token ) {
178
- queryParams . access_token = accessToken ;
178
+ queryParams . access_token = requestSnapshot . accessToken ;
179
179
}
180
180
}
181
181
@@ -187,33 +187,19 @@ export class FetchHttpApi<O extends IHttpOpts> {
187
187
throw error ;
188
188
}
189
189
190
- if ( error . errcode === "M_UNKNOWN_TOKEN" && ! opts . doNotAttemptTokenRefresh ) {
191
- // If the access token has changed since we started the request, but before we refreshed it,
192
- // then it was refreshed due to another request failing, so retry before refreshing again.
193
- let outcome : TokenRefreshOutcome | null = null ;
194
- if ( accessToken === this . opts . accessToken ) {
195
- const tokenRefreshPromise = this . tryRefreshToken ( ) ;
196
- this . tokenRefreshPromise = tokenRefreshPromise ;
197
- outcome = await tokenRefreshPromise ;
198
- }
199
-
200
- if ( outcome === TokenRefreshOutcome . Success || outcome === null ) {
190
+ if ( error . errcode === "M_UNKNOWN_TOKEN" ) {
191
+ const outcome = await this . tokenRefresher . handleUnknownToken ( requestSnapshot , attempt ) ;
192
+ if ( outcome === TokenRefreshOutcome . Success ) {
201
193
// if we got a new token retry the request
202
- return this . authedRequest ( method , path , queryParams , body , {
203
- ...paramOpts ,
204
- // Only attempt token refresh once for each failed request
205
- doNotAttemptTokenRefresh : outcome !== null ,
206
- } ) ;
194
+ return this . doAuthedRequest ( attempt + 1 , method , path , queryParams , body , paramOpts ) ;
207
195
}
208
196
if ( outcome === TokenRefreshOutcome . Failure ) {
209
197
throw new TokenRefreshError ( error ) ;
210
198
}
211
- // Fall through to SessionLoggedOut handler below
212
- }
213
199
214
- // otherwise continue with error handling
215
- if ( error . errcode == "M_UNKNOWN_TOKEN" && ! opts ?. inhibitLogoutEmit ) {
216
- this . eventEmitter . emit ( HttpApiEvent . SessionLoggedOut , error ) ;
200
+ if ( ! opts ?. inhibitLogoutEmit ) {
201
+ this . eventEmitter . emit ( HttpApiEvent . SessionLoggedOut , error ) ;
202
+ }
217
203
} else if ( error . errcode == "M_CONSENT_NOT_GIVEN" ) {
218
204
this . eventEmitter . emit ( HttpApiEvent . NoConsent , error . message , error . data . consent_uri ) ;
219
205
}
@@ -222,33 +208,6 @@ export class FetchHttpApi<O extends IHttpOpts> {
222
208
}
223
209
}
224
210
225
- /**
226
- * Attempt to refresh access tokens.
227
- * On success, sets new access and refresh tokens in opts.
228
- * @returns Promise that resolves to a boolean - true when token was refreshed successfully
229
- */
230
- @singleAsyncExecution
231
- private async tryRefreshToken ( ) : Promise < TokenRefreshOutcome > {
232
- if ( ! this . opts . refreshToken || ! this . opts . tokenRefreshFunction ) {
233
- return TokenRefreshOutcome . Logout ;
234
- }
235
-
236
- try {
237
- const { accessToken, refreshToken } = await this . opts . tokenRefreshFunction ( this . opts . refreshToken ) ;
238
- this . opts . accessToken = accessToken ;
239
- this . opts . refreshToken = refreshToken ;
240
- // successfully got new tokens
241
- return TokenRefreshOutcome . Success ;
242
- } catch ( error ) {
243
- this . opts . logger ?. warn ( "Failed to refresh token" , error ) ;
244
- // If we get a TokenError or MatrixError, we should log out, otherwise assume transient
245
- if ( error instanceof TokenRefreshLogoutError || error instanceof MatrixError ) {
246
- return TokenRefreshOutcome . Logout ;
247
- }
248
- return TokenRefreshOutcome . Failure ;
249
- }
250
- }
251
-
252
211
/**
253
212
* Perform a request to the homeserver without any credentials.
254
213
* @param method - The HTTP method e.g. "GET".
0 commit comments