diff --git a/packages/api/src/Domain/Http/HttpService.ts b/packages/api/src/Domain/Http/HttpService.ts index f33444a44..a1cd5a34d 100644 --- a/packages/api/src/Domain/Http/HttpService.ts +++ b/packages/api/src/Domain/Http/HttpService.ts @@ -99,7 +99,12 @@ export class HttpService implements HttpServiceInterface { } async runHttp(httpRequest: HttpRequest): Promise> { - if (this.inProgressRefreshSessionPromise) { + if (this.__latencySimulatorMs) { + await sleep(this.__latencySimulatorMs, true) + } + + const isRefreshRequest = httpRequest.url === joinPaths(this.host, Paths.v1.refreshSession) + if (this.inProgressRefreshSessionPromise && !isRefreshRequest) { await this.inProgressRefreshSessionPromise httpRequest.authentication = this.session?.accessToken.value @@ -107,17 +112,13 @@ export class HttpService implements HttpServiceInterface { const request = this.createXmlRequest(httpRequest) - if (this.__latencySimulatorMs) { - await sleep(this.__latencySimulatorMs, true) - } - const response = await this.runRequest(request, this.createRequestBody(httpRequest)) if (response.meta) { this.updateMetaCallback?.(response.meta) } - if (response.status === HttpStatusCode.ExpiredAccessToken) { + if (response.status === HttpStatusCode.ExpiredAccessToken && !isRefreshRequest) { if (this.inProgressRefreshSessionPromise) { await this.inProgressRefreshSessionPromise } else { diff --git a/packages/snjs/mocha/session.test.js b/packages/snjs/mocha/session.test.js index 20825f4f9..b1e9d51c3 100644 --- a/packages/snjs/mocha/session.test.js +++ b/packages/snjs/mocha/session.test.js @@ -105,6 +105,28 @@ describe('server session', function () { expect(sessionAfterSync.accessToken.expiresAt).to.be.greaterThan(Date.now()) }) + it('should not deadlock while renewing session', async function () { + this.timeout(Factory.TwentySecondTimeout) + + await Factory.registerUserToApplication({ + application: this.application, + email: this.email, + password: this.password, + }) + + await sleepUntilSessionExpires(this.application) + + // Apply a latency simulation so that ` this.inProgressRefreshSessionPromise = this.refreshSession()` does + // not have the chance to complete before it is assigned to the variable. This test came along with a fix + // where runHttp does not await a pending refreshSession promise if the request being made is itself a refreshSession request. + this.application.httpService.__latencySimulatorMs = 1000 + await this.application.sync.sync(syncOptions) + + const sessionAfterSync = this.application.apiService.getSession() + + expect(sessionAfterSync.accessToken.expiresAt).to.be.greaterThan(Date.now()) + }) + it('should succeed when a sync request is perfomed after signing into an ephemeral session', async function () { await Factory.registerUserToApplication({ application: this.application,