MSC4311: Use full PDU's in stripped state (like invite_room_state) over federation and always include m.room.create event - #19723
Conversation
/sync should still use stripped events with MSC4311/sync should still use stripped events
/sync should still use stripped events/sync should still use stripped events
| and event.get_state_key() == "" | ||
| ): | ||
| return event.get_pdu_json() | ||
|
|
|
|
||
| return [strip_event(e) for e in state_to_include.values()] | ||
|
|
||
| async def get_stripped_room_state_ids_from_event_context( |
There was a problem hiding this comment.
Split some logic out of get_stripped_room_state_from_event_context(...) into get_stripped_room_state_ids_from_event_context(...)
This way we can use the same event selection logic but fetch/format them however we see fit.
| # TODO(paul): assert that room_id/event_id parsed from path actually | ||
| # match those given in content |
There was a problem hiding this comment.
Removed as we actually do this now in FederationServer.on_invite_request(...) (added in this PR)
/sync should still use stripped eventsinvite_room_state) over federation and always include m.room.create event
…al-implementation Conflicts: rust/src/room_versions.rs
| # Add the invite itself | ||
| # | ||
| # FIXME: Doesn't seem to be in the spec | ||
| invited_state.append(strip_event(room.invite)) |
There was a problem hiding this comment.
Found that we were accidentally providing the full invite event in this case. This is just something that's been happening since inception (2015)
The Complement tests check to make sure the client sees stripped state which is how I caught this.
Additionally, just documenting this existing behavior.
There was a problem hiding this comment.
To note, the stripped state event fields cover the use cases described in matrix-org/synapse#6739 (comment). The client has the invite sender to show who invited you.
There was a problem hiding this comment.
MSC4319 currently proposes that we "SHOULD" keep the full event behavior for at-least one spec version (discussed in matrix-org/matrix-spec-proposals#4319 (comment))
I find this pretty dubious given stripped state already covers the use cases described in matrix-org/synapse#6739 (comment) and only Draupnir was given as an example that relies on event_id (MSC discussion: matrix-org/matrix-spec-proposals#4319 (comment))
The desire for more information like event_id, origin_server_ts, etc is real but relying on this existing unspecced snowflake behavior is not something we should worry about. And it completely flies in the face of MSC4311 which still says clients should receive stripped state.
| state_res: StateResolutionVersions::V2_1, | ||
| msc4289_creator_power_enabled: true, | ||
| msc4291_room_ids_as_hashes: true, | ||
| msc4311_stripped_state: true, |
There was a problem hiding this comment.
Will this not break invites across new and old Synapses for v12 rooms?
There was a problem hiding this comment.
How so? We don't strictly validate anything (just warn when it fails validation in v12 rooms).
See "What does this PR do?" in the PR description.
There was a problem hiding this comment.
Ah, my bad. Feels very odd to have an option that merely causes things to log. Especially given that initially this will log for almost every invite in v12. I wonder if its worth it for now?
There was a problem hiding this comment.
It's house-keeping for the room versions. We use it today and will use it in the future.
| and err.errcode == Codes.MISSING_PARAM | ||
| ): | ||
| raise SynapseError( | ||
| 500, |
There was a problem hiding this comment.
If we're not doing a 4xx response then we shouldn't use 500 as it's not an internal server error. I'm not really sure what the closest HTTP code for "this server can't perform the action requested", it's half a 4xx and half a 5xx.
TBH it feels quite similar to the 400 above, "User's homeserver does not support this room version".
There was a problem hiding this comment.
As the comment above states, the MSC tells us this should be a 5xx error.
I chose 500 as the other server is saying our homeserver is doing something wrong.
And the description sounds perfect:
The HTTP 500 Internal Server Error server error response status code indicates that the server encountered an unexpected condition that prevented it from fulfilling the request.
-- https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/500
In other words, our homeserver implementation is preventing the invite from going through. Nothing the user can do.
There was a problem hiding this comment.
Hmm, bleurgh. The problem is that in practice returning a 500 is surprising behaviour for almost everyone: they expect a 500 to indicate there is a bug/misconfiguration in the server. Clients/users will retry when they hit a 500 as they assume it may be a transient problem.
Generally, regardless of what the web specs imply, clients view 5xx as possibly transient server issues, while 4xx as a final failure. Most 4xx are actually things the user can't do anything about, e.g. 403 or 404.
Concretely: returning a 500 will cause confusion for server operators (and us) as that should be reserved for bugs, and clients will keep retrying this endpoint. Intermediate proxies do sometimes infer uptime via 5xx errors (e.g. returning 502 often causes this).
Have taken this up wit the Spec Core Team.
| state_events = await self.store.get_events(state_ids) | ||
| assert set(state_ids) == set(state_events.keys()), ( | ||
| "We should have all events available that were set as stripped state." | ||
| ) |
There was a problem hiding this comment.
Hmmm, this is technically a behaviour change where we are recalculating the stripped state here rather than using the stripped state we calculated at event creation.
Generally, I'm wondering if we should change the format of what we store as invite_room_state in the event json in the DB, rather than having this somewhat weird thing going where we have to recalculate it. Not really sure how tractable it is, but you can imagine a world where we store in a separate table the stripped state event IDs for locally generated events.
There was a problem hiding this comment.
🤷 Future plans
…c/test_rooms_invites.SlidingSyncRoomsInvitesTestCase to use 'stripped state' to compare against responses instead of fleshing out a full PDU
…c/test_rooms_invites.SlidingSyncRoomsInvitesTestCase to use 'stripped state' to compare against responses instead of fleshing out a full PDU
…c/test_rooms_invites.SlidingSyncRoomsInvitesTestCase to use 'stripped state' to compare against responses instead of fleshing out a full PDU
…c/test_rooms_invites.SlidingSyncRoomsInvitesTestCase to use 'stripped state' to compare against responses instead of fleshing out a full PDU
| "(either one could be at fault).", | ||
| Codes.UNKNOWN, | ||
| additional_fields={ | ||
| "cause": err.msg, |
There was a problem hiding this comment.
Err, there's little point returning this to clients as nothing will read it. It can also be confusing if devs interpret it as the what this server is complaining about to the client
| f"Received {HTTPStatus.BAD_REQUEST} {Codes.MISSING_PARAM} response from remote homeserver " | ||
| "while trying to send the invite over federation. This indicates a compatibility problem " | ||
| "between your homeserver and the homeserver you're trying to send the invite to " | ||
| "(either one could be at fault).", |
There was a problem hiding this comment.
Sorry, missed this last time but I don't think this is a useful user facing error, it's too verbose and technical.
I think one of the following maybe:
Could not send the invite to the user's server.
The user's server rejected the invite.
Your server cannot send invite's to the remote server
We want something that the user can understand the net result: namely the invite failed for some reason.
There was a problem hiding this comment.
as a user, not knowing why the invite failed (beyond "computer said no") would be more frustrating than the confusion caused by a technical error that I can ask for help with. As a server admin, it'd be even more annoying because I would then have to open a terminal and scrape through logs to try and find out more information.
Since this is explicitly only caused by an incompatibility, if you want non-technical, I think something more akin to
The user's server did not understand our invite and may be outdated
would make sense. It's simple, concise, but still explains what the problem was, and a possible solution. It's also similar to the error returned when the remote server doesn't support the room version, just above.
There was a problem hiding this comment.
I'm in agreeance on providing enough info so that the error is useful to understand and for others (like us when people ask for help).
I've updated the error message to start off more plainly: The other user's server did not understand our invite. [...]
There was a problem hiding this comment.
Fair that we should communicate that it is likely an incompatibility issue.
The other user's server did not understand our invite. [...]
I think this is confusing to people who aren't technical, how can an invite be not "understood"?
The problem is that these are user visible and therefore affect the UX. In practice giving "complicated" error messages simply causes more confusion for the vast majority of users with little benefit (even to technical users). Especially in this case where nothing we're telling the user in the error messages is actually useful information. The fact that the remote server rejected it with a 400 M_MISSING_PARAM is pretty meaningless?
The overarching problem here is that we simply don't know why the other side rejected the invite, and so we can't give a useful error message to the user, just an unsatisfactory "something went went wrong".
I think the information that is useful for the user is:
- The invite failed.
- It's not the user's fault.
- Retrying immediately won't help.
So perhaps something like:
Cannot send the invite to the user's server; it was rejected. (This could be due to a compatibility issue between the servers).
It's not great, but I think says all that is useful to convey in a more friendly language. It's also unique enough that if more information is required server admins can scrape the logs easily.
There was a problem hiding this comment.
Better phrasing is good but we're losing too much information in my opinion.
The problem needs to be clear. Forcing a random user to get their homeserver admin to log dive is not nice. It's undue burden on a homeserver admin and not possible in unresponsive admin scenarios.
If someone posts that error message in Synapse Admins and asks why, my next question is what did the other server respond with? If I looked specifically at the Synapse code, I can deduce it in the this case because it corresponds to one specific scenario but it still requires me to dig. That's just not necessary. Then the next step to look at is the two homeserver versions and see what they're running.
These are the same questions I would have as a user if this happened to me. Knowing that the other server responded with 400 M_MISSING_PARAM is useful to me. It doesn't matter that either my homeserver or the other homeserver needs to change which I don't control.
I've updated to this (separated the simple explanation from the details with \n\n):
f"Invite was rejected by the recipient's server.\n\n"
f"The remote homeserver ({destination}) returned {HTTPStatus.BAD_REQUEST} {Codes.MISSING_PARAM} "
"which indicates a compatibility problem between your homeserver and the "
"homeserver you're trying to send the invite to (either one could be at fault).",To better illustrate, this is a Hulu error. This drives me crazy ![]()
We're having trouble playing...
Rest assured, we're working on it. In the meantime, it may help if you restart the video.
Need help? Visit help.hulu.com/support in a browser
Hulu Error Code: P-DEV318
05/31/2022 20:14:41 PM PDT
There was a problem hiding this comment.
I do agree that we shouldn't return a completely vacuous error. However, we do need to balance utility between super-power users and UX for normal users.
To that end: I don't really see what specifying {HTTPStatus.BAD_REQUEST} {Codes.MISSING_PARAM} in the error response gives users or helps with diagnostics. The proposed error response gives the information needed: one of the two servers is probably out-of-date. If that is not the case then you'll need to dig into the logs anyway to figure out what is wrong, you're not going to be able to tell just by the error codes.
I wouldn't be that adverse to adding extra information if our error messages had an extra collapsed-by-default section for "Advanced Information", but we don't have that.
Plus, generally we're reluctant to give out too much information to users (like stack traces), as they can too easily leak information we'd rather wasn't. At which point for most cases you're going to need to look at the logs anyway. (Not to mention that any remedies here require involvement by the server admins)
There was a problem hiding this comment.
From your three point criteria: the invite failed, it's not the user's fault, retrying won't help. A hedged/vague message asserts those. A specific one demonstrates them. Reassurance without specifics isn't believed. Everyone has been told "sorry, something went wrong" by a system that was in fact blaming them or hiding the fact that they could have changed the outcome by trying something different. The details help close those loops and let the person stop second-guessing themselves.
The \n\n separation means the details cost the non-technical user nothing as they read the first sentence and stop, which is effectively your ideal way to provide "Advanced Information" anyway.
I think we've struck a good balance with the last iteration.
| and err.errcode == Codes.MISSING_PARAM | ||
| ): | ||
| raise SynapseError( | ||
| 500, |
There was a problem hiding this comment.
Hmm, bleurgh. The problem is that in practice returning a 500 is surprising behaviour for almost everyone: they expect a 500 to indicate there is a bug/misconfiguration in the server. Clients/users will retry when they hit a 500 as they assume it may be a transient problem.
Generally, regardless of what the web specs imply, clients view 5xx as possibly transient server issues, while 4xx as a final failure. Most 4xx are actually things the user can't do anything about, e.g. 403 or 404.
Concretely: returning a 500 will cause confusion for server operators (and us) as that should be reserved for bugs, and clients will keep retrying this endpoint. Intermediate proxies do sometimes infer uptime via 5xx errors (e.g. returning 502 often causes this).
Have taken this up wit the Spec Core Team.
…al-implementation
…tand our invite.` See #19723 (comment)
…c/test_rooms_invites.SlidingSyncRoomsInvitesTestCase to use 'stripped state' to compare against responses instead of fleshing out a full PDU
…c/test_rooms_invites.SlidingSyncRoomsInvitesTestCase to use 'stripped state' to compare against responses instead of fleshing out a full PDU
MadLittleMods
left a comment
There was a problem hiding this comment.
LLM-assisted review
| # | ||
| # We do this after the above checks to make sure it's a valid event | ||
| # from this room. | ||
| if pdu.type == EventTypes.Create: |
There was a problem hiding this comment.
Needs check for state_key == ""
| # Validate signature/hashes | ||
| try: | ||
| pdu = await self.federation_client._check_sigs_and_hash( | ||
| room_version, pdu | ||
| ) |
There was a problem hiding this comment.
hmmm, MSC4311 says it only cares about when an event would "fail signature checks" but it links to the spec about validating hashes and signatures.
As a note, when the hash check fails, _check_sigs_and_hash doesn't actually raise, it just redacts the event. I guess if we care about checking hashes, we should be using the actual potentially redacted PDU returned.
This also runs the policy server and spam checker callbacks over the PDU's which we don't need/want here as far as I can tell.
| # | ||
| # Parse/validate `knock_room_state` | ||
| try: | ||
| stripped_room_state = await self._parse_stripped_room_state( |
There was a problem hiding this comment.
Trying to clarify whether the all or nothing behavior of invite validation should also apply to knocks. MSC4311 currently says that for knocks, it should just remove individual events and continue
| "Stripped" state means that only the `type`, `state_key`, `content` and `sender` keys | ||
| are included from each state event. |
There was a problem hiding this comment.
Not really relevant to this function which is only about getting the state event IDs.
| "Stripped" state means that only the `type`, `state_key`, `content` and `sender` keys | |
| are included from each state event. |
| # Ensure the event has been stripped | ||
| self.assertNotIn("signatures", event) |
There was a problem hiding this comment.
check_knock_room_state_against_room_state(...) is actually used in federation and client-side tests.
This could be still relevant for the client tests
…c/test_rooms_invites.SlidingSyncRoomsInvitesTestCase to use 'stripped state' to compare against responses instead of fleshing out a full PDU
Background
This PR was originally just trying to remove the flawed MSC4311 partial implementation as client side API's like
/syncshould still use stripped events. But it turns out we were just re-using the client logic for the federation side and things might break if we didn't include the fullm.room.createevent so this PR now introduces MSC4311 support to use full PDU's in theinvite_room_state/knock_room_statein the federation API's.The flawed implementation was originally introduced in 0eb7252 (no PR I assume because part of Hydra security fix)
Spawning from reviewing #19722 and noticing that we have
TestMSC4311FullCreateEventOnStrippedStatein Complement which already passes even though that test looks flawed:What does this PR do?
invite_room_state/knock_room_stateover federationinvite_room_state/knock_room_stateover federationComplement tests: matrix-org/complement#796
Part of #19414
Dev notes
Todo
TestMSC4311FullCreateEventOnStrippedStateComplement test so it doesn't look for the full PDU in/sync.knock_room_statein this PRknock_room_statesenderinstead ofuser_idfor any stripped state event usages matrix-org/sytest#1425senderinstead ofuser_idfor any stripped state event usages matrix-org/sytest#1425Pull Request Checklist
EventStoretoEventWorkerStore.".code blocks.