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

Remove trailing slashes from outbound federation requests #4840

Merged
merged 42 commits into from
Mar 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
525dd02
Remove trailing slashes from outbound federation requests
anoadragon453 Mar 8, 2019
64ff110
Retry certain federation requests on 404
anoadragon453 Mar 8, 2019
f8740d5
Add changelog
anoadragon453 Mar 8, 2019
a5dd335
lint
anoadragon453 Mar 8, 2019
a8a028d
Merge branch 'develop' into anoa/trailing_slashes_client
anoadragon453 Mar 11, 2019
f18dca2
Merge branch 'develop' into anoa/trailing_slashes_client
anoadragon453 Mar 11, 2019
66f205e
We're calling different functions now
anoadragon453 Mar 11, 2019
802cb5d
Fix syntax error
anoadragon453 Mar 11, 2019
4868b12
and again
anoadragon453 Mar 11, 2019
0ea8582
Cleaner way of implementing trailing slashes
anoadragon453 Mar 12, 2019
97653ef
Correct argument name
anoadragon453 Mar 12, 2019
cf301e3
Add workaround note
anoadragon453 Mar 13, 2019
7e75d96
Fix paranthesis indent
anoadragon453 Mar 13, 2019
7d053cf
Retry on 400:M_UNRECOGNIZED
anoadragon453 Mar 13, 2019
09626bf
Switch to wrapper function around _send_request
anoadragon453 Mar 13, 2019
5526b05
Fix syntax issues
anoadragon453 Mar 13, 2019
660b77f
Add missing docstring detail
anoadragon453 Mar 13, 2019
220607a
Remove testing code
anoadragon453 Mar 13, 2019
9dd0e34
Syntax test
anoadragon453 Mar 13, 2019
ee8ba39
Are you happy now
anoadragon453 Mar 13, 2019
c2d848b
Destructure again
anoadragon453 Mar 13, 2019
c991e7a
Syntax checker is bork
anoadragon453 Mar 13, 2019
bec3138
go home python, you're drunk
anoadragon453 Mar 13, 2019
66cdb84
Or perhaps I was the one who was drunk
anoadragon453 Mar 13, 2019
7c0295f
no kwargs today
anoadragon453 Mar 13, 2019
5ca857a
as above
anoadragon453 Mar 13, 2019
26f8e2d
there comes a time when you should give up. but you dont
anoadragon453 Mar 13, 2019
8d16ffa
i should have given up
anoadragon453 Mar 13, 2019
45524f2
i should have given up x2
anoadragon453 Mar 13, 2019
86c60bd
i should have given up x3
anoadragon453 Mar 13, 2019
9a2e22f
is this what purgatory feels like
anoadragon453 Mar 13, 2019
b2df0e8
receiving a 400 caused an exception. handle it
anoadragon453 Mar 13, 2019
ecea5af
Correct var name
anoadragon453 Mar 13, 2019
621e7f3
Better exception handling
anoadragon453 Mar 18, 2019
a8ad39e
lint
anoadragon453 Mar 18, 2019
94cb793
Federation test fixed!
anoadragon453 Mar 20, 2019
551ea11
Just return if not doing any trailing slash shennanigans
anoadragon453 Mar 20, 2019
c69df5d
Fix comments. v0.99.2 -> v0.99.3
anoadragon453 Mar 20, 2019
cd36a12
New test, fix issues
anoadragon453 Mar 20, 2019
bb52a2e
lint
anoadragon453 Mar 20, 2019
2150151
kwargs doesn't like commas on calling funcs either. TIL
anoadragon453 Mar 20, 2019
b41c2ea
Clean up backoff_on_404 and metehod calls
anoadragon453 Mar 21, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
richvdh marked this conversation as resolved.
Show resolved Hide resolved
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):
richvdh marked this conversation as resolved.
Show resolved Hide resolved
"""
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