Handle interrupted connections

This commit is contained in:
Jean-Paul Calderone 2020-10-21 12:21:01 -04:00
parent 9146d2a0b9
commit c31300fd0d
3 changed files with 51 additions and 2 deletions

View File

@ -13,13 +13,22 @@ from testtools.matchers import (
Equals,
Contains,
MatchesPredicate,
AfterPreprocessing,
)
from testtools.twistedsupport import (
failed,
succeeded,
has_no_result,
)
from twisted.python.failure import (
Failure,
)
from twisted.internet.error import (
ConnectionDone,
)
from twisted.internet.defer import (
Deferred,
fail,
)
from twisted.web.server import (
@ -52,9 +61,11 @@ class StaticResource(Resource, object):
def __init__(self, response):
Resource.__init__(self)
self._response = response
self._request = None
@render_exception
def render(self, request):
self._request = request
return self._response
@ -214,3 +225,25 @@ class RenderExceptionTests(SyncTestCase):
Equals(b"Internal Server Error"),
),
)
def test_disconnected(self):
"""
If the transport is disconnected before the response is available, nothing
is written to the request.
"""
result = Deferred()
resource = StaticResource(result)
d = render(resource, {})
resource._request.connectionLost(Failure(ConnectionDone()))
result.callback(b"Some result")
self.assertThat(
d,
failed(
AfterPreprocessing(
lambda reason: reason.type,
Equals(ConnectionDone),
),
),
)

View File

@ -41,6 +41,7 @@ from twisted.python.failure import (
Failure,
)
from twisted.internet.defer import (
CancelledError,
maybeDeferred,
)
from twisted.web.resource import (
@ -472,7 +473,17 @@ def render_exception(render):
# Apply `_finish` all of our result handling logic to whatever it
# returned.
result.addBoth(_finish, bound_render, request)
result.addActionFinish()
d = result.addActionFinish()
# If the connection is lost then there's no point running our _finish
# logic because it has nowhere to send anything. There may also be no
# point in finishing whatever operation was being performed because
# the client cannot be informed of its result. Also, Twisted Web
# raises exceptions from some Request methods if they're used after
# the connection is lost.
request.notifyFinish().addErrback(
lambda ignored: d.cancel(),
)
return NOT_DONE_YET
return g
@ -497,6 +508,8 @@ def _finish(result, render, request):
:return: ``None``
"""
if isinstance(result, Failure):
if result.check(CancelledError):
return
Message.log(
message_type=u"allmydata:web:common-render:failure",
message=result.getErrorMessage(),

View File

@ -480,7 +480,10 @@ class FileDownloader(Resource, object):
d = self.filenode.read(req, first, size)
def _error(f):
req._tahoe_request_had_error = f # for HTTP-style logging
if f.check(defer.CancelledError):
# The HTTP connection was lost and we no longer have anywhere
# to send our result. Let this pass through.
return f
if req.startedWriting:
# The content-type is already set, and the response code has
# already been sent, so we can't provide a clean error