Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 1350b05

Browse files
authored
Pass errors back to the client when trying multiple federation destinations. (#9868)
This ensures that something like an auth error (403) will be returned to the requester instead of attempting to try more servers, which will likely result in the same error, and then passing back a generic 400 error.
1 parent 0ffa5fb commit 1350b05

File tree

2 files changed

+61
-58
lines changed

2 files changed

+61
-58
lines changed

changelog.d/9868.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a long-standing bug where errors from federation did not propagate to the client.

synapse/federation/federation_client.py

Lines changed: 60 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,28 @@ async def get_event_auth(
451451

452452
return signed_auth
453453

454+
def _is_unknown_endpoint(
455+
self, e: HttpResponseException, synapse_error: Optional[SynapseError] = None
456+
) -> bool:
457+
"""
458+
Returns true if the response was due to an endpoint being unimplemented.
459+
460+
Args:
461+
e: The error response received from the remote server.
462+
synapse_error: The above error converted to a SynapseError. This is
463+
automatically generated if not provided.
464+
465+
"""
466+
if synapse_error is None:
467+
synapse_error = e.to_synapse_error()
468+
# There is no good way to detect an "unknown" endpoint.
469+
#
470+
# Dendrite returns a 404 (with no body); synapse returns a 400
471+
# with M_UNRECOGNISED.
472+
return e.code == 404 or (
473+
e.code == 400 and synapse_error.errcode == Codes.UNRECOGNIZED
474+
)
475+
454476
async def _try_destination_list(
455477
self,
456478
description: str,
@@ -468,9 +490,9 @@ async def _try_destination_list(
468490
callback: Function to run for each server. Passed a single
469491
argument: the server_name to try.
470492
471-
If the callback raises a CodeMessageException with a 300/400 code,
472-
attempts to perform the operation stop immediately and the exception is
473-
reraised.
493+
If the callback raises a CodeMessageException with a 300/400 code or
494+
an UnsupportedRoomVersionError, attempts to perform the operation
495+
stop immediately and the exception is reraised.
474496
475497
Otherwise, if the callback raises an Exception the error is logged and the
476498
next server tried. Normally the stacktrace is logged but this is
@@ -492,8 +514,7 @@ async def _try_destination_list(
492514
continue
493515

494516
try:
495-
res = await callback(destination)
496-
return res
517+
return await callback(destination)
497518
except InvalidResponseError as e:
498519
logger.warning("Failed to %s via %s: %s", description, destination, e)
499520
except UnsupportedRoomVersionError:
@@ -502,17 +523,15 @@ async def _try_destination_list(
502523
synapse_error = e.to_synapse_error()
503524
failover = False
504525

526+
# Failover on an internal server error, or if the destination
527+
# doesn't implemented the endpoint for some reason.
505528
if 500 <= e.code < 600:
506529
failover = True
507530

508-
elif failover_on_unknown_endpoint:
509-
# there is no good way to detect an "unknown" endpoint. Dendrite
510-
# returns a 404 (with no body); synapse returns a 400
511-
# with M_UNRECOGNISED.
512-
if e.code == 404 or (
513-
e.code == 400 and synapse_error.errcode == Codes.UNRECOGNIZED
514-
):
515-
failover = True
531+
elif failover_on_unknown_endpoint and self._is_unknown_endpoint(
532+
e, synapse_error
533+
):
534+
failover = True
516535

517536
if not failover:
518537
raise synapse_error from e
@@ -570,9 +589,8 @@ async def make_membership_event(
570589
UnsupportedRoomVersionError: if remote responds with
571590
a room version we don't understand.
572591
573-
SynapseError: if the chosen remote server returns a 300/400 code.
574-
575-
RuntimeError: if no servers were reachable.
592+
SynapseError: if the chosen remote server returns a 300/400 code, or
593+
no servers successfully handle the request.
576594
"""
577595
valid_memberships = {Membership.JOIN, Membership.LEAVE}
578596
if membership not in valid_memberships:
@@ -642,9 +660,8 @@ async def send_join(
642660
``auth_chain``.
643661
644662
Raises:
645-
SynapseError: if the chosen remote server returns a 300/400 code.
646-
647-
RuntimeError: if no servers were reachable.
663+
SynapseError: if the chosen remote server returns a 300/400 code, or
664+
no servers successfully handle the request.
648665
"""
649666

650667
async def send_request(destination) -> Dict[str, Any]:
@@ -673,7 +690,7 @@ async def send_request(destination) -> Dict[str, Any]:
673690
if create_event is None:
674691
# If the state doesn't have a create event then the room is
675692
# invalid, and it would fail auth checks anyway.
676-
raise SynapseError(400, "No create event in state")
693+
raise InvalidResponseError("No create event in state")
677694

678695
# the room version should be sane.
679696
create_room_version = create_event.content.get(
@@ -746,16 +763,11 @@ async def _do_send_join(self, destination: str, pdu: EventBase) -> JsonDict:
746763
content=pdu.get_pdu_json(time_now),
747764
)
748765
except HttpResponseException as e:
749-
if e.code in [400, 404]:
750-
err = e.to_synapse_error()
751-
752-
# If we receive an error response that isn't a generic error, or an
753-
# unrecognised endpoint error, we assume that the remote understands
754-
# the v2 invite API and this is a legitimate error.
755-
if err.errcode not in [Codes.UNKNOWN, Codes.UNRECOGNIZED]:
756-
raise err
757-
else:
758-
raise e.to_synapse_error()
766+
# If an error is received that is due to an unrecognised endpoint,
767+
# fallback to the v1 endpoint. Otherwise consider it a legitmate error
768+
# and raise.
769+
if not self._is_unknown_endpoint(e):
770+
raise
759771

760772
logger.debug("Couldn't send_join with the v2 API, falling back to the v1 API")
761773

@@ -802,6 +814,11 @@ async def _do_send_invite(
802814
803815
Returns:
804816
The event as a dict as returned by the remote server
817+
818+
Raises:
819+
SynapseError: if the remote server returns an error or if the server
820+
only supports the v1 endpoint and a room version other than "1"
821+
or "2" is requested.
805822
"""
806823
time_now = self._clock.time_msec()
807824

@@ -817,28 +834,19 @@ async def _do_send_invite(
817834
},
818835
)
819836
except HttpResponseException as e:
820-
if e.code in [400, 404]:
821-
err = e.to_synapse_error()
822-
823-
# If we receive an error response that isn't a generic error, we
824-
# assume that the remote understands the v2 invite API and this
825-
# is a legitimate error.
826-
if err.errcode != Codes.UNKNOWN:
827-
raise err
828-
829-
# Otherwise, we assume that the remote server doesn't understand
830-
# the v2 invite API. That's ok provided the room uses old-style event
831-
# IDs.
837+
# If an error is received that is due to an unrecognised endpoint,
838+
# fallback to the v1 endpoint if the room uses old-style event IDs.
839+
# Otherwise consider it a legitmate error and raise.
840+
err = e.to_synapse_error()
841+
if self._is_unknown_endpoint(e, err):
832842
if room_version.event_format != EventFormatVersions.V1:
833843
raise SynapseError(
834844
400,
835845
"User's homeserver does not support this room version",
836846
Codes.UNSUPPORTED_ROOM_VERSION,
837847
)
838-
elif e.code in (403, 429):
839-
raise e.to_synapse_error()
840848
else:
841-
raise
849+
raise err
842850

843851
# Didn't work, try v1 API.
844852
# Note the v1 API returns a tuple of `(200, content)`
@@ -865,9 +873,8 @@ async def send_leave(self, destinations: Iterable[str], pdu: EventBase) -> None:
865873
pdu: event to be sent
866874
867875
Raises:
868-
SynapseError if the chosen remote server returns a 300/400 code.
869-
870-
RuntimeError if no servers were reachable.
876+
SynapseError: if the chosen remote server returns a 300/400 code, or
877+
no servers successfully handle the request.
871878
"""
872879

873880
async def send_request(destination: str) -> None:
@@ -889,16 +896,11 @@ async def _do_send_leave(self, destination: str, pdu: EventBase) -> JsonDict:
889896
content=pdu.get_pdu_json(time_now),
890897
)
891898
except HttpResponseException as e:
892-
if e.code in [400, 404]:
893-
err = e.to_synapse_error()
894-
895-
# If we receive an error response that isn't a generic error, or an
896-
# unrecognised endpoint error, we assume that the remote understands
897-
# the v2 invite API and this is a legitimate error.
898-
if err.errcode not in [Codes.UNKNOWN, Codes.UNRECOGNIZED]:
899-
raise err
900-
else:
901-
raise e.to_synapse_error()
899+
# If an error is received that is due to an unrecognised endpoint,
900+
# fallback to the v1 endpoint. Otherwise consider it a legitmate error
901+
# and raise.
902+
if not self._is_unknown_endpoint(e):
903+
raise
902904

903905
logger.debug("Couldn't send_leave with the v2 API, falling back to the v1 API")
904906

0 commit comments

Comments
 (0)