=== modified file 'twisted/web/server.py' --- twisted/web/server.py 2009-09-14 17:25:32 +0000 +++ twisted/web/server.py 2010-09-30 18:10:15 +0000 @@ -126,58 +126,31 @@ except: self.processingFailed(failure.Failure()) - def render(self, resrc): - try: - body = resrc.render(self) - except UnsupportedMethod, e: - allowedMethods = e.allowedMethods - if (self.method == "HEAD") and ("GET" in allowedMethods): - # We must support HEAD (RFC 2616, 5.1.1). If the - # resource doesn't, fake it by giving the resource - # a 'GET' request and then return only the headers, - # not the body. - log.msg("Using GET to fake a HEAD request for %s" % - (resrc,)) - self.method = "GET" - body = resrc.render(self) - - if body is NOT_DONE_YET: - log.msg("Tried to fake a HEAD request for %s, but " - "it got away from me." % resrc) - # Oh well, I guess we won't include the content length. - else: - self.setHeader('content-length', str(len(body))) - - self.write('') - self.finish() - return - - if self.method in (supportedMethods): - # We MUST include an Allow header - # (RFC 2616, 10.4.6 and 14.7) - self.setHeader('Allow', allowedMethods) - s = ('''Your browser approached me (at %(URI)s) with''' - ''' the method "%(method)s". I only allow''' - ''' the method%(plural)s %(allowed)s here.''' % { - 'URI': self.uri, - 'method': self.method, - 'plural': ((len(allowedMethods) > 1) and 's') or '', - 'allowed': string.join(allowedMethods, ', ') - }) - epage = resource.ErrorPage(http.NOT_ALLOWED, - "Method Not Allowed", s) - body = epage.render(self) - else: - epage = resource.ErrorPage(http.NOT_IMPLEMENTED, "Huh?", - "I don't know how to treat a" - " %s request." % (self.method,)) - body = epage.render(self) - # end except UnsupportedMethod - + """ Render a request. + + This is called on the leaf resource for + a request. Render must return either a string + or a Deferred containing a string as its result. + This string will be sent to the browser as the HTML + for the request. + Render may also return NOT_DONE_YET; if NOT_DONE_YET + is returned then the resource's render method must + make any request.write calls and then request.finish. + Usually these are done in a deffered callback. This + behaviour will be deprecated in favour of returning + a Deferred. + """ + body = defer.maybeDeferred(resrc.render, self) + body.addCallbacks(self._cbRender, self._ebRender, + callbackArgs=(resrc,), errbackArgs=(resrc,)) + return + + def _cbRender(self, body, resrc): + """ Callback for render; writes the data.""" if body == NOT_DONE_YET: return - if type(body) is not types.StringType: + if not isinstance(body, str): body = resource.ErrorPage( http.INTERNAL_SERVER_ERROR, "Request did not return a string", @@ -198,6 +171,60 @@ self.setHeader('content-length', str(len(body))) self.write(body) self.finish() + + def _ebRender(self, fail, resrc): + """ Errback for render to handle UnsupportedMethod""" + fail.trap(UnsupportedMethod) + allowedMethods = fail.value.allowedMethods + if (self.method == "HEAD") and ("GET" in allowedMethods): + log.msg("Using GET to fake a HEAD request for %s" % (resrc,)) + self.method = "GET" + + body = defer.maybeDeferred(resrc.render, self) + body.addCallbacks(self._cbRenderEb, self._ebRenderEb, callbackArgs=(resrc,), errbackArgs=(resrc,)) + return body + + if self.method in (supportedMethods): + # We MUST include an Allow header + # (RFC 2616, 10.4.6 and 14.7) + self.setHeader('Allow', allowedMethods) + s = ('''Your browser approached me (at %(URI)s) with''' + ''' the method "%(method)s". I only allow''' + ''' the method%(plural)s %(allowed)s here.''' % { + 'URI': self.uri, + 'method': self.method, + 'plural': ((len(allowedMethods) > 1) and 's') or '', + 'allowed': string.join(allowedMethods, ', ') + }) + epage = resource.ErrorPage(http.NOT_ALLOWED, + "Method Not Allowed", s) + + body = defer.maybeDeferred(epage.render, self) + body.addCallback(self._cbRenderEb, resrc) + return body + else: + epage = resource.ErrorPage(http.NOT_IMPLEMENTED, "Huh?", + "I don't know how to treat a" + " %s request." % (self.method,)) + + body = defer.maybeDeferred(epage.render,self) + body.addCallback(self._cbRender, resrc) + return body + + def _cbRenderEb(self, body, resrc): + """ Callback for render's errback; tries to write a fake HEAD request + that is created from the GET method. + """ + if body == NOT_DONE_YET: + log.msg("Tried to fake a HEAD request for %s, but " + "it got away from me." % resrc) + # Oh well, I guess we won't include the content length. + else: + self.setHeader('content-length', str(len(body))) + + self.write('') + self.finish() + return def processingFailed(self, reason): log.err(reason) === modified file 'twisted/web/test/test_web.py' --- twisted/web/test/test_web.py 2009-11-22 22:46:12 +0000 +++ twisted/web/test/test_web.py 2010-09-30 18:07:08 +0000 @@ -861,3 +861,36 @@ self.assertEqual( self.site.logFile.read(), '1.2.3.4 - - [25/Oct/2004:12:31:59 +0000] "GET /dummy HTTP/1.0" 123 - "-" "Malicious Web\\" Evil"\n') + +class DeferredRenderResource(resource.Resource): + """ Leaf resource that returns a Deferred from its render method. """ + + isLeaf = True + def render(self, request): + d = task.deferLater(reactor, 1, lambda: 'deferred result') + d.addCallback(self._cbRender) + return d + + def _cbRender(self, result): + return result + + +class DeferredRenderTest(unittest.TestCase): + """ Test for handling a resource which returns a Deferred in its render method.""" + + def testDeferredRender(self): + """ Test a request that calls a resource which will return a Deferred from its render method.""" + d = DummyChannel() + d.site.resource.putChild('deferrender', DeferredRenderResource()) + d.transport.port = 81 + request = server.Request(d, 1) + request.setHost('example.com', 81) + request.gotLength(0) + request.requestReceived('GET', '/deferrender', 'HTTP/1.0') + + # check that the Deferred returned by render has returned the correct string + d = task.deferLater(reactor, 3, self._testDeferredRender, request) + return d + + def _testDeferredRender(self, req): + self.assertEquals(req.transport.getvalue().splitlines()[-1], 'deferred result')