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

Commit

Permalink
Remove trailing slashes from outbound federation requests and retry o…
Browse files Browse the repository at this point in the history
…n 400 (#4840)

As per #3622, we remove trailing slashes from outbound federation requests. However, to ensure that we remain backwards compatible with previous versions of Synapse, if we receive a HTTP 400 with `M_UNRECOGNIZED`, then we are likely talking to an older version of Synapse in which case we retry with a trailing slash appended to the request path.
  • Loading branch information
anoadragon453 authored Mar 21, 2019
2 parents 01e6b40 + b41c2ea commit 7bef97d
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 17 deletions.
1 change: 1 addition & 0 deletions changelog.d/4840.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Remove trailing slashes from certain outbound federation requests. Retry if receiving a 404. Context: #3622.
21 changes: 14 additions & 7 deletions synapse/federation/transport/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ def get_room_state(self, destination, room_id, event_id):
logger.debug("get_room_state dest=%s, room=%s",
destination, room_id)

path = _create_v1_path("/state/%s/", room_id)
path = _create_v1_path("/state/%s", room_id)
return self.client.get_json(
destination, path=path, args={"event_id": event_id},
try_trailing_slash_on_400=True,
)

@log_function
Expand All @@ -73,9 +74,10 @@ def get_room_state_ids(self, destination, room_id, event_id):
logger.debug("get_room_state_ids dest=%s, room=%s",
destination, room_id)

path = _create_v1_path("/state_ids/%s/", room_id)
path = _create_v1_path("/state_ids/%s", room_id)
return self.client.get_json(
destination, path=path, args={"event_id": event_id},
try_trailing_slash_on_400=True,
)

@log_function
Expand All @@ -95,8 +97,11 @@ def get_event(self, destination, event_id, timeout=None):
logger.debug("get_pdu dest=%s, event_id=%s",
destination, event_id)

path = _create_v1_path("/event/%s/", event_id)
return self.client.get_json(destination, path=path, timeout=timeout)
path = _create_v1_path("/event/%s", event_id)
return self.client.get_json(
destination, path=path, timeout=timeout,
try_trailing_slash_on_400=True,
)

@log_function
def backfill(self, destination, room_id, event_tuples, limit):
Expand All @@ -121,7 +126,7 @@ def backfill(self, destination, room_id, event_tuples, limit):
# TODO: raise?
return

path = _create_v1_path("/backfill/%s/", room_id)
path = _create_v1_path("/backfill/%s", room_id)

args = {
"v": event_tuples,
Expand All @@ -132,6 +137,7 @@ def backfill(self, destination, room_id, event_tuples, limit):
destination,
path=path,
args=args,
try_trailing_slash_on_400=True,
)

@defer.inlineCallbacks
Expand Down Expand Up @@ -176,6 +182,7 @@ def send_transaction(self, transaction, json_data_callback=None):
json_data_callback=json_data_callback,
long_retries=True,
backoff_on_404=True, # If we get a 404 the other side has gone
try_trailing_slash_on_400=True,
)

defer.returnValue(response)
Expand Down Expand Up @@ -959,7 +966,7 @@ def _create_v1_path(path, *args):
Example:
_create_v1_path("/event/%s/", event_id)
_create_v1_path("/event/%s", event_id)
Args:
path (str): String template for the path
Expand All @@ -980,7 +987,7 @@ def _create_v2_path(path, *args):
Example:
_create_v2_path("/event/%s/", event_id)
_create_v2_path("/event/%s", event_id)
Args:
path (str): String template for the path
Expand Down
85 changes: 75 additions & 10 deletions synapse/http/matrixfederationclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,57 @@ def schedule(x):

self._cooperator = Cooperator(scheduler=schedule)

@defer.inlineCallbacks
def _send_request_with_optional_trailing_slash(
self,
request,
try_trailing_slash_on_400=False,
**send_request_args
):
"""Wrapper for _send_request which can optionally retry the request
upon receiving a combination of a 400 HTTP response code and a
'M_UNRECOGNIZED' errcode. This is a workaround for Synapse <= v0.99.3
due to #3622.
Args:
request (MatrixFederationRequest): details of request to be sent
try_trailing_slash_on_400 (bool): Whether on receiving a 400
'M_UNRECOGNIZED' from the server to retry the request with a
trailing slash appended to the request path.
send_request_args (Dict): A dictionary of arguments to pass to
`_send_request()`.
Raises:
HttpResponseException: If we get an HTTP response code >= 300
(except 429).
Returns:
Deferred[Dict]: Parsed JSON response body.
"""
try:
response = yield self._send_request(
request, **send_request_args
)
except HttpResponseException as e:
# Received an HTTP error > 300. Check if it meets the requirements
# to retry with a trailing slash
if not try_trailing_slash_on_400:
raise

if e.code != 400 or e.to_synapse_error().errcode != "M_UNRECOGNIZED":
raise

# Retry with a trailing slash if we received a 400 with
# 'M_UNRECOGNIZED' which some endpoints can return when omitting a
# trailing slash on Synapse <= v0.99.3.
request.path += "/"

response = yield self._send_request(
request, **send_request_args
)

defer.returnValue(response)

@defer.inlineCallbacks
def _send_request(
self,
Expand All @@ -196,7 +247,7 @@ def _send_request(
timeout=None,
long_retries=False,
ignore_backoff=False,
backoff_on_404=False
backoff_on_404=False,
):
"""
Sends a request to the given server.
Expand Down Expand Up @@ -473,7 +524,8 @@ def put_json(self, destination, path, args={}, data={},
json_data_callback=None,
long_retries=False, timeout=None,
ignore_backoff=False,
backoff_on_404=False):
backoff_on_404=False,
try_trailing_slash_on_400=False):
""" Sends the specifed json data using PUT
Args:
Expand All @@ -493,7 +545,12 @@ def put_json(self, destination, path, args={}, data={},
and try the request anyway.
backoff_on_404 (bool): True if we should count a 404 response as
a failure of the server (and should therefore back off future
requests)
requests).
try_trailing_slash_on_400 (bool): True if on a 400 M_UNRECOGNIZED
response we should try appending a trailing slash to the end
of the request. Workaround for #3622 in Synapse <= v0.99.3. This
will be attempted before backing off if backing off has been
enabled.
Returns:
Deferred[dict|list]: Succeeds when we get a 2xx HTTP response. The
Expand All @@ -509,7 +566,6 @@ def put_json(self, destination, path, args={}, data={},
RequestSendFailed: If there were problems connecting to the
remote, due to e.g. DNS failures, connection timeouts etc.
"""

request = MatrixFederationRequest(
method="PUT",
destination=destination,
Expand All @@ -519,17 +575,19 @@ def put_json(self, destination, path, args={}, data={},
json=data,
)

response = yield self._send_request(
response = yield self._send_request_with_optional_trailing_slash(
request,
try_trailing_slash_on_400,
backoff_on_404=backoff_on_404,
ignore_backoff=ignore_backoff,
long_retries=long_retries,
timeout=timeout,
ignore_backoff=ignore_backoff,
backoff_on_404=backoff_on_404,
)

body = yield _handle_json_response(
self.hs.get_reactor(), self.default_timeout, request, response,
)

defer.returnValue(body)

@defer.inlineCallbacks
Expand Down Expand Up @@ -592,7 +650,8 @@ def post_json(self, destination, path, data={}, long_retries=False,

@defer.inlineCallbacks
def get_json(self, destination, path, args=None, retry_on_dns_fail=True,
timeout=None, ignore_backoff=False):
timeout=None, ignore_backoff=False,
try_trailing_slash_on_400=False):
""" GETs some json from the given host homeserver and path
Args:
Expand All @@ -606,6 +665,9 @@ def get_json(self, destination, path, args=None, retry_on_dns_fail=True,
be retried.
ignore_backoff (bool): true to ignore the historical backoff data
and try the request anyway.
try_trailing_slash_on_400 (bool): True if on a 400 M_UNRECOGNIZED
response we should try appending a trailing slash to the end of
the request. Workaround for #3622 in Synapse <= v0.99.3.
Returns:
Deferred[dict|list]: Succeeds when we get a 2xx HTTP response. The
result will be the decoded JSON body.
Expand All @@ -631,16 +693,19 @@ def get_json(self, destination, path, args=None, retry_on_dns_fail=True,
query=args,
)

response = yield self._send_request(
response = yield self._send_request_with_optional_trailing_slash(
request,
try_trailing_slash_on_400,
backoff_on_404=False,
ignore_backoff=ignore_backoff,
retry_on_dns_fail=retry_on_dns_fail,
timeout=timeout,
ignore_backoff=ignore_backoff,
)

body = yield _handle_json_response(
self.hs.get_reactor(), self.default_timeout, request, response,
)

defer.returnValue(body)

@defer.inlineCallbacks
Expand Down
2 changes: 2 additions & 0 deletions tests/handlers/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ def test_started_typing_remote_send(self):
json_data_callback=ANY,
long_retries=True,
backoff_on_404=True,
try_trailing_slash_on_400=True,
)

def test_started_typing_remote_recv(self):
Expand Down Expand Up @@ -269,6 +270,7 @@ def test_stopped_typing(self):
json_data_callback=ANY,
long_retries=True,
backoff_on_404=True,
try_trailing_slash_on_400=True,
)

self.assertEquals(self.event_source.get_current_key(), 1)
Expand Down
99 changes: 99 additions & 0 deletions tests/http/test_fedclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,105 @@ def test_client_headers_no_body(self):

self.assertIsInstance(f.value, TimeoutError)

def test_client_requires_trailing_slashes(self):
"""
If a connection is made to a client but the client rejects it due to
requiring a trailing slash. We need to retry the request with a
trailing slash. Workaround for Synapse <= v0.99.3, explained in #3622.
"""
d = self.cl.get_json(
"testserv:8008", "foo/bar", try_trailing_slash_on_400=True,
)

# Send the request
self.pump()

# there should have been a call to connectTCP
clients = self.reactor.tcpClients
self.assertEqual(len(clients), 1)
(_host, _port, factory, _timeout, _bindAddress) = clients[0]

# complete the connection and wire it up to a fake transport
client = factory.buildProtocol(None)
conn = StringTransport()
client.makeConnection(conn)

# that should have made it send the request to the connection
self.assertRegex(conn.value(), b"^GET /foo/bar")

# Clear the original request data before sending a response
conn.clear()

# Send the HTTP response
client.dataReceived(
b"HTTP/1.1 400 Bad Request\r\n"
b"Content-Type: application/json\r\n"
b"Content-Length: 59\r\n"
b"\r\n"
b'{"errcode":"M_UNRECOGNIZED","error":"Unrecognized request"}'
)

# We should get another request with a trailing slash
self.assertRegex(conn.value(), b"^GET /foo/bar/")

# Send a happy response this time
client.dataReceived(
b"HTTP/1.1 200 OK\r\n"
b"Content-Type: application/json\r\n"
b"Content-Length: 2\r\n"
b"\r\n"
b'{}'
)

# We should get a successful response
r = self.successResultOf(d)
self.assertEqual(r, {})

def test_client_does_not_retry_on_400_plus(self):
"""
Another test for trailing slashes but now test that we don't retry on
trailing slashes on a non-400/M_UNRECOGNIZED response.
See test_client_requires_trailing_slashes() for context.
"""
d = self.cl.get_json(
"testserv:8008", "foo/bar", try_trailing_slash_on_400=True,
)

# Send the request
self.pump()

# there should have been a call to connectTCP
clients = self.reactor.tcpClients
self.assertEqual(len(clients), 1)
(_host, _port, factory, _timeout, _bindAddress) = clients[0]

# complete the connection and wire it up to a fake transport
client = factory.buildProtocol(None)
conn = StringTransport()
client.makeConnection(conn)

# that should have made it send the request to the connection
self.assertRegex(conn.value(), b"^GET /foo/bar")

# Clear the original request data before sending a response
conn.clear()

# Send the HTTP response
client.dataReceived(
b"HTTP/1.1 404 Not Found\r\n"
b"Content-Type: application/json\r\n"
b"Content-Length: 2\r\n"
b"\r\n"
b"{}"
)

# We should not get another request
self.assertEqual(conn.value(), b"")

# We should get a 404 failure response
self.failureResultOf(d)

def test_client_sends_body(self):
self.cl.post_json(
"testserv:8008", "foo/bar", timeout=10000,
Expand Down

0 comments on commit 7bef97d

Please sign in to comment.