Showing with 198 additions and 16 deletions.
  1. +32 −2 twisted/web/http.py
  2. +63 −10 twisted/web/resource.py
  3. +1 −1 twisted/web/server.py
  4. +101 −2 twisted/web/test/test_web.py
  5. +1 −1 twisted/web/test/test_wsgi.py
34 changes: 32 additions & 2 deletions twisted/web/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,12 +597,17 @@ class Request:
_forceSSL = 0
_disconnected = False

def __init__(self, channel, queued=_QUEUED_SENTINEL):
def __init__(self, channel, queued=_QUEUED_SENTINEL, reactor=None):
"""
@param channel: the channel we're connected to.
@param queued: (deprecated) are we in the request queue, or can we
start writing to the transport?
"""
if not reactor:
from twisted.internet import reactor

self._reactor = reactor

self.notifications = []
self.channel = channel
self.requestHeaders = Headers()
Expand Down Expand Up @@ -1668,7 +1673,13 @@ class HTTPChannel(basic.LineReceiver, policies.TimeoutMixin):
_receivedHeaderCount = 0
_receivedHeaderSize = 0

def __init__(self):
def __init__(self, reactor=None):

if reactor is None:
from twisted.internet import reactor

self._reactor = reactor

# the request queue
self.requests = []
self._handlingRequest = False
Expand Down Expand Up @@ -1944,6 +1955,14 @@ def timeoutConnection(self):
policies.TimeoutMixin.timeoutConnection(self)


def callLater(self, period, func):
"""
Call something later. See:
L{reactor.callLater<twisted.internet.interfaces.IReactorTime.callLater>}
"""
return self._reactor.callLater(period, func)


def connectionLost(self, reason):
self.setTimeout(None)
for request in self.requests:
Expand Down Expand Up @@ -2294,6 +2313,15 @@ def timeOut(self, value):
self._timeOut = value
self._channel.timeOut = value

@property
def _reactor(self):
return self._channel._reactor


@_reactor.setter
def _reactor(self, value):
self._channel._reactor = value


def dataReceived(self, data):
"""
Expand Down Expand Up @@ -2409,6 +2437,8 @@ def buildProtocol(self, addr):
# timeOut needs to be on the Protocol instance cause
# TimeoutMixin expects it there
p.timeOut = self.timeOut
p._reactor = self._reactor

return p


Expand Down
73 changes: 63 additions & 10 deletions twisted/web/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
from twisted.python.reflect import prefixedMethodNames
from twisted.python.components import proxyForInterface

from twisted.internet.defer import Deferred

from twisted.web._responses import FORBIDDEN, NOT_FOUND
from twisted.web.error import UnsupportedMethod

Expand Down Expand Up @@ -108,8 +110,13 @@ class Resource:
This serves 2 main purposes; one is to provide a standard representation
for what HTTP specification calls an 'entity', and the other is to provide
an abstract directory structure for URL retrieval.
@ivar deferredTimeout: How many seconds that L{Resource.process} should
wait for L{Deferred}s returned from C{render_METHOD} before timing out.
@type deferredTimeout: L{int}, L{float}, or L{None} for no timeout.
"""
entityType = IResource
deferredTimeout = 60

server = None

Expand Down Expand Up @@ -224,19 +231,24 @@ def render(self, request):
"""
Render a given resource. See L{IResource}'s render method.
I delegate to methods of self with the form 'render_METHOD'
where METHOD is the HTTP that was used to make the
request. Examples: render_GET, render_HEAD, render_POST, and
I delegate to methods of self with the form C{render_METHOD} where
METHOD is the HTTP method (or "verb") that was used to make the
request. Examples: C{render_GET}, C{render_HEAD}, C{render_POST}, and
so on. Generally you should implement those methods instead of
overriding this one.
render_METHOD methods are expected to return a byte string which will be
the rendered page, unless the return value is C{server.NOT_DONE_YET}, in
which case it is this class's responsibility to write the results using
C{request.write(data)} and then call C{request.finish()}.
C{render_METHOD} methods are expected to return any one of::
- a byte string which will be the rendered page;
- L{NOT_DONE_YET <twisted.web.server.NOT_DONE_YET>}, in which case
it is this class's responsibility to write the results using
C{request.write(data)} and then call C{request.finish()};
- or a L{Deferred} which fires with a L{bytes} in within
L{Resource.deferredTimeout} wall-clock seconds (or unlimited if
the value is L{None}.)
Old code that overrides render() directly is likewise expected
to return a byte string or NOT_DONE_YET.
Old code that overrides C{render()} directly is likewise expected to
return a L{bytes} or L{NOT_DONE_YET <twisted.web.server.NOT_DONE_YET>}.
@see: L{IResource.render}
"""
Expand All @@ -247,7 +259,48 @@ def render(self, request):
except AttributeError:
allowedMethods = _computeAllowedMethods(self)
raise UnsupportedMethod(allowedMethods)
return m(request)

r = m(request)

if isinstance(r, Deferred):
# If it's a Deferred, we want to handle it a bit specially! We
# handle it here and not in Site as it's then possible to have
# per-resource timeouts, rather than site-level timeouts.
# Site-level timeouts are decent for user requests, but our code
# might knowingly take a very long time.

def timedOut():
"""
Called when L{Request.deferredTimeout} has passed and the
L{Deferred} has not fired.
"""
r.cancel()
request.setResponseCode(504)
request.write(b"<h1>Gateway Timeout</h1>")
request.finish()

if self.deferredTimeout:
# Only set the timeout if we have the timeout...
timeout = request.site._reactor.callLater(self.deferredTimeout,
timedOut)
else:
timeout = None

def done(res, timeout):
"""
Called when the L{Deferred} has fired.
"""
if timeout:
timeout.cancel()
request.write(res)
request.finish()

r.addCallback(done, timeout)

from twisted.web.server import NOT_DONE_YET
return NOT_DONE_YET
else:
return r


def render_HEAD(self, request):
Expand Down
2 changes: 1 addition & 1 deletion twisted/web/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def process(self):

# set various default headers
self.setHeader(b'server', version)
self.setHeader(b'date', http.datetimeToString())
self.setHeader(b'date', http.datetimeToString(self._reactor.seconds()))

# Resource Identification
self.prepath = []
Expand Down
103 changes: 101 additions & 2 deletions twisted/web/test/test_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@
Tests for various parts of L{twisted.web}.
"""

from __future__ import absolute_import, division, print_function

import os
import zlib

from zope.interface import implementer
from zope.interface.verify import verifyObject

from twisted.python import reflect, failure
from twisted.python.compat import _PY3
from twisted.python.compat import _PY3, iterbytes
from twisted.python.filepath import FilePath
from twisted.trial import unittest
from twisted.internet import reactor
from twisted.internet import reactor, defer
from twisted.internet.address import IPv4Address
from twisted.internet.task import Clock
from twisted.web import server, resource
Expand All @@ -24,6 +26,8 @@
from twisted.web.test.requesthelper import DummyChannel, DummyRequest
from twisted.web.static import Data

from twisted.test.proto_helpers import StringTransport


class ResourceTests(unittest.TestCase):
def testListEntities(self):
Expand Down Expand Up @@ -1390,3 +1394,98 @@ def test_defaultReactor(self):
from twisted.internet import reactor
factory = http.HTTPFactory()
self.assertIs(factory._reactor, reactor)



class DeferredsInResourceTests(unittest.TestCase):
"""
L{server.Site} allows L{resource.Resource} render methods to return a L{Deferred}.
"""

def test_deferredWithBytestring(self):
"""
If a L{resource.Resource} process function returns a L{defer.Deferred},
the bytestring fired from the Deferred will be written to the transport
and the transport closed.
"""
class DeferringResource(resource.Resource):
isLeaf = True

def __init__(self, clock):
self.clock = clock

def render_GET(self, request):
d = defer.Deferred()
self.clock.callLater(1, d.callback, b"whee!")
return d

clock = Clock()
baseResource = resource.Resource()
baseResource.putChild(b'', DeferringResource(clock))
site = server.Site(baseResource, reactor=clock)
site.startFactory()
self.addCleanup(site.stopFactory)

channel = site.buildProtocol(None)
transport = StringTransport()
channel.makeConnection(transport)

data = (
b"GET / HTTP/1.1\r\n"
b"Host: foo.bar\r\n"
b"\r\n")

# Wtf we can't send it all at once?
for x in iterbytes(data):
channel.dataReceived(x)

self.assertEqual(transport.value(), b"")

clock.advance(1)

self.assertIn(b"HTTP/1.1 200 OK", transport.value())
self.assertIn(b"whee!", transport.value())


def test_deferredNeverEnds(self):
"""
If a L{resource.Resource} process function returns a L{defer.Deferred},
and it does not complete in L{resource.Resource.deferredTimeout}, the
Deferred will be cancelled and a HTTP 504 returned.
"""
class NeverEndingDeferringResource(resource.Resource):
isLeaf = True
deferredTimeout = 1

def __init__(self, clock):
self.clock = clock

def render_GET(self, request):
return defer.Deferred()

clock = Clock()
baseResource = resource.Resource()
baseResource.putChild(b'', NeverEndingDeferringResource(clock))
site = server.Site(baseResource, reactor=clock)
site.startFactory()
self.addCleanup(site.stopFactory)

channel = site.buildProtocol(None)
transport = StringTransport()
channel.makeConnection(transport)

data = (
b"GET / HTTP/1.1\r\n"
b"Host: foo.bar\r\n"
b"\r\n")

# Wtf we can't send it all at once?
for x in iterbytes(data):
channel.dataReceived(x)

self.assertEqual(transport.value(), b"")

clock.advance(1)

self.assertIn(b"HTTP/1.1 504 Gateway Time-out", transport.value())
self.flushLoggedErrors(defer.CancelledError)
2 changes: 1 addition & 1 deletion twisted/web/test/test_wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -1252,7 +1252,7 @@ def _headersTest(self, appHeaders, expectedHeaders):
included in the response.
"""
# Make the Date header value deterministic
self.patch(http, 'datetimeToString', lambda: 'Tuesday')
self.patch(http, 'datetimeToString', lambda *args, **kwargs: 'Tuesday')

channel = DummyChannel()

Expand Down