@@ -18,10 +18,10 @@ limitations under the License.
1818 * This is an internal module. See {@link MatrixHttpApi} for the public class.
1919 */
2020
21- import { checkObjectHasKeys , encodeParams } from "../utils.ts" ;
21+ import { checkObjectHasKeys , deepCopy , encodeParams } from "../utils.ts" ;
2222import { type TypedEventEmitter } from "../models/typed-event-emitter.ts" ;
2323import { Method } from "./method.ts" ;
24- import { ConnectionError , MatrixError , TokenRefreshError , TokenRefreshLogoutError } from "./errors.ts" ;
24+ import { ConnectionError , MatrixError , TokenRefreshError } from "./errors.ts" ;
2525import {
2626 HttpApiEvent ,
2727 type HttpApiEventHandlerMap ,
@@ -31,7 +31,7 @@ import {
3131} from "./interface.ts" ;
3232import { anySignal , parseErrorResponse , timeoutSignal } from "./utils.ts" ;
3333import { type QueryDict } from "../utils.ts" ;
34- import { singleAsyncExecution } from "../utils/decorators .ts" ;
34+ import { TokenRefresher , TokenRefreshOutcome } from "./refresh .ts" ;
3535
3636interface TypedResponse < T > extends Response {
3737 json ( ) : Promise < T > ;
@@ -43,14 +43,9 @@ export type ResponseType<T, O extends IHttpOpts> = O extends { json: false }
4343 ? T
4444 : TypedResponse < T > ;
4545
46- const enum TokenRefreshOutcome {
47- Success = "success" ,
48- Failure = "failure" ,
49- Logout = "logout" ,
50- }
51-
5246export class FetchHttpApi < O extends IHttpOpts > {
5347 private abortController = new AbortController ( ) ;
48+ private readonly tokenRefresher : TokenRefresher ;
5449
5550 public constructor (
5651 private eventEmitter : TypedEventEmitter < HttpApiEvent , HttpApiEventHandlerMap > ,
@@ -59,6 +54,8 @@ export class FetchHttpApi<O extends IHttpOpts> {
5954 checkObjectHasKeys ( opts , [ "baseUrl" , "prefix" ] ) ;
6055 opts . onlyData = ! ! opts . onlyData ;
6156 opts . useAuthorizationHeader = opts . useAuthorizationHeader ?? true ;
57+
58+ this . tokenRefresher = new TokenRefresher ( opts ) ;
6259 }
6360
6461 public abort ( ) : void {
@@ -113,12 +110,6 @@ export class FetchHttpApi<O extends IHttpOpts> {
113110 return this . requestOtherUrl ( method , fullUri , body , opts ) ;
114111 }
115112
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-
122113 /**
123114 * Perform an authorised request to the homeserver.
124115 * @param method - The HTTP method e.g. "GET".
@@ -146,36 +137,45 @@ export class FetchHttpApi<O extends IHttpOpts> {
146137 * @returns Rejects with an error if a problem occurred.
147138 * This includes network problems and Matrix-specific error JSON.
148139 */
149- public async authedRequest < T > (
140+ public authedRequest < T > (
150141 method : Method ,
151142 path : string ,
152- queryParams ? : QueryDict ,
143+ queryParams : QueryDict = { } ,
153144 body ?: Body ,
154- paramOpts : IRequestOpts & { doNotAttemptTokenRefresh ?: boolean } = { } ,
145+ paramOpts : IRequestOpts = { } ,
155146 ) : Promise < ResponseType < T , O > > {
156- if ( ! queryParams ) queryParams = { } ;
147+ return this . doAuthedRequest < T > ( 1 , method , path , queryParams , body , paramOpts ) ;
148+ }
157149
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 > > {
158159 // 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 ;
163163
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 ) {
167167 if ( this . opts . useAuthorizationHeader ) {
168168 if ( ! opts . headers ) {
169169 opts . headers = { } ;
170170 }
171171 if ( ! opts . headers . Authorization ) {
172- opts . headers . Authorization = `Bearer ${ accessToken } ` ;
172+ opts . headers . Authorization = `Bearer ${ requestSnapshot . accessToken } ` ;
173173 }
174174 if ( queryParams . access_token ) {
175175 delete queryParams . access_token ;
176176 }
177177 } else if ( ! queryParams . access_token ) {
178- queryParams . access_token = accessToken ;
178+ queryParams . access_token = requestSnapshot . accessToken ;
179179 }
180180 }
181181
@@ -187,33 +187,19 @@ export class FetchHttpApi<O extends IHttpOpts> {
187187 throw error ;
188188 }
189189
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 ) {
201193 // 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 ) ;
207195 }
208196 if ( outcome === TokenRefreshOutcome . Failure ) {
209197 throw new TokenRefreshError ( error ) ;
210198 }
211- // Fall through to SessionLoggedOut handler below
212- }
213199
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+ }
217203 } else if ( error . errcode == "M_CONSENT_NOT_GIVEN" ) {
218204 this . eventEmitter . emit ( HttpApiEvent . NoConsent , error . message , error . data . consent_uri ) ;
219205 }
@@ -222,33 +208,6 @@ export class FetchHttpApi<O extends IHttpOpts> {
222208 }
223209 }
224210
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-
252211 /**
253212 * Perform a request to the homeserver without any credentials.
254213 * @param method - The HTTP method e.g. "GET".
0 commit comments