1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-26 17:03:12 +03:00

Automatically reconnect sessions when sliding sync expires them

This can happen when you close your laptop overnight,
as the server will not hold onto in-memory resources
for your connection indefinitely. When this happen,
the server will HTTP 400 you with "session expired".

At this point, it is no longer safe to remember anything
and you must forget everything and resend any sticky
parameters. This commit does the sticky parameters and
re-establishes the connection, but it may need additional
work to make the JS SDK forget now invalid data.
This commit is contained in:
Kegan Dougal
2022-08-19 17:33:07 +01:00
parent 3ae974e23e
commit 2cda229bc4
2 changed files with 127 additions and 3 deletions

View File

@@ -80,6 +80,103 @@ describe("SlidingSync", () => {
slidingSync.stop(); slidingSync.stop();
httpBackend.verifyNoOutstandingExpectation(); httpBackend.verifyNoOutstandingExpectation();
}); });
it("should reset the connection on HTTP 400 and send everything again", async () => {
// seed the connection with some lists, extensions and subscriptions to verify they are sent again
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
const roomId = "!sub:localhost";
const subInfo = {
timeline_limit: 42,
required_state: [["m.room.create", ""]],
};
const listInfo = {
ranges: [[0,10]],
filters: {
is_dm: true,
},
};
const ext = {
name: () => "custom_extension",
onRequest: (initial) => { return { initial: initial } },
onResponse: (res) => { return {} },
when: () => ExtensionState.PreProcess,
};
slidingSync.modifyRoomSubscriptions(new Set([roomId]));
slidingSync.modifyRoomSubscriptionInfo(subInfo);
slidingSync.setList(0, listInfo);
slidingSync.registerExtension(ext);
slidingSync.start();
// expect everything to be sent
let txnId;
httpBackend.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toEqual({
[roomId]: subInfo,
});
expect(body.lists[0]).toEqual(listInfo);
expect(body.extensions).toBeTruthy();
expect(body.extensions["custom_extension"]).toEqual({initial:true});
txnId = body.txn_id;
}).respond(200, function() {
return {
pos: "11",
lists: [{ count: 5 }],
extensions: {},
txn_id: txnId,
};
});
await httpBackend.flushAllExpected();
// expect nothing but ranges and non-initial extensions to be sent
httpBackend.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy();
expect(body.lists[0]).toEqual({
ranges: [[0,10]],
});
expect(body.extensions).toBeTruthy();
expect(body.extensions["custom_extension"]).toEqual({initial:false});
}).respond(200, function() {
return {
pos: "12",
lists: [{ count: 5 }],
extensions: {},
};
});
await httpBackend.flushAllExpected();
// now we expire the session
httpBackend.when("POST", syncUrl).respond(400, function() {
logger.debug("sending session expired 400");
return {
error: "HTTP 400 : session expired",
};
});
await httpBackend.flushAllExpected();
// ...and everything should be sent again
httpBackend.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toEqual({
[roomId]: subInfo,
});
expect(body.lists[0]).toEqual(listInfo);
expect(body.extensions).toBeTruthy();
expect(body.extensions["custom_extension"]).toEqual({initial:true});
}).respond(200, function() {
return {
pos: "1",
lists: [{ count: 6 }],
extensions: {},
};
});
await httpBackend.flushAllExpected();
slidingSync.stop();
});
}); });
describe("room subscriptions", () => { describe("room subscriptions", () => {

View File

@@ -694,6 +694,26 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
this.removeAllListeners(SlidingSyncEvent.RoomData); this.removeAllListeners(SlidingSyncEvent.RoomData);
} }
/**
* Re-setup this connection e.g in the event of an expired session.
*/
private resetup(): void {
logger.warn("SlidingSync: resetting connection info");
// any pending txn ID defers will be forgotten already by the server, so clear them out
this.txnIdDefers.forEach((d) => {
d.reject(d.txnId);
});
this.txnIdDefers = [];
// resend sticky params and de-confirm all subscriptions
this.lists.forEach((l) => {
l.setModified(true);
});
this.confirmedRoomSubscriptions = new Set<string>(); // leave desired ones alone though!
// reset the connection as we might be wedged
this.needsResend = true;
this.pendingReq?.abort();
}
/** /**
* Start syncing with the server. Blocks until stopped. * Start syncing with the server. Blocks until stopped.
*/ */
@@ -732,7 +752,6 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
} }
this.pendingReq = this.client.slidingSync(reqBody, this.proxyBaseUrl); this.pendingReq = this.client.slidingSync(reqBody, this.proxyBaseUrl);
resp = await this.pendingReq; resp = await this.pendingReq;
logger.debug(resp);
currentPos = resp.pos; currentPos = resp.pos;
// update what we think we're subscribed to. // update what we think we're subscribed to.
for (const roomId of newSubscriptions) { for (const roomId of newSubscriptions) {
@@ -770,7 +789,15 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
null, null,
err, err,
); );
await sleep(3000); if (err.httpStatus === 400) {
// session probably expired TODO: assign an errcode
// so drop state and re-request
this.resetup();
currentPos = undefined;
await sleep(50); // in case the 400 was for something else; don't tightloop
continue;
}
await sleep(5000);
} else if (this.needsResend || err === "aborted") { } else if (this.needsResend || err === "aborted") {
// don't sleep as we caused this error by abort()ing the request. // don't sleep as we caused this error by abort()ing the request.
// we check for 'aborted' because that's the error Jest returns and without it // we check for 'aborted' because that's the error Jest returns and without it
@@ -778,7 +805,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
continue; continue;
} else { } else {
logger.error(err); logger.error(err);
await sleep(3000); await sleep(5000);
} }
} }
if (!resp) { if (!resp) {