Merge pull request #1155 from twisted/9655-twisted-web-tempfile-parameter

Parameterized Request content file

Author: exarkun
Reviewer: hawkowl
Fixes: ticket:9655
This commit is contained in:
Jean-Paul Calderone 2019-07-22 15:55:03 -04:00 committed by GitHub
commit e79b08e11c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 130 additions and 15 deletions

View File

@ -659,6 +659,17 @@ NO_BODY_CODES = (204, 304)
_QUEUED_SENTINEL = object()
def _getContentFile(length):
"""
Get a writeable file-like object to which request content can be written.
"""
if length is not None and length < 100000:
return StringIO()
return tempfile.TemporaryFile()
@implementer(interfaces.IConsumer,
_IDeprecatedHTTPChannelToRequestInterface)
class Request:
@ -812,10 +823,7 @@ class Request:
request headers. L{None} if the request headers do not indicate a
length.
"""
if length is not None and length < 100000:
self.content = StringIO()
else:
self.content = tempfile.TemporaryFile()
self.content = _getContentFile(length)
def parseCookies(self):

View File

@ -0,0 +1 @@
twisted.web.server.Request will now use twisted.web.server.Site.getContentFile, if it exists, to get a file into which to write request content. If getContentFile is not provided by the site, it will fall back to the previous behavior of using io.BytesIO for small requests and tempfile.TemporaryFile for large ones.

View File

@ -182,6 +182,24 @@ class Request(Copyable, http.Request, components.Componentized):
return name
def gotLength(self, length):
"""
Called when HTTP channel got length of content in this request.
This method is not intended for users.
@param length: The length of the request body, as indicated by the
request headers. L{None} if the request headers do not indicate a
length.
"""
try:
getContentFile = self.channel.site.getContentFile
except AttributeError:
http.Request.gotLength(self, length)
else:
self.content = getContentFile(length)
def process(self):
"""
Process a request.

View File

@ -81,4 +81,23 @@ class FlattenTestCase(TestCase):
return d
__all__ = ["_render", "FlattenTestCase"]
def assertIsFilesystemTemporary(case, fileObj):
"""
Assert that C{fileObj} is a temporary file on the filesystem.
@param case: A C{TestCase} instance to use to make the assertion.
@raise: C{case.failureException} if C{fileObj} is not a temporary file on
the filesystem.
"""
# The tempfile API used to create content returns an instance of a
# different type depending on what platform we're running on. The point
# here is to verify that the request body is in a file that's on the
# filesystem. Having a fileno method that returns an int is a somewhat
# close approximation of this. -exarkun
case.assertIsInstance(fileObj.fileno(), int)
__all__ = ["_render", "FlattenTestCase", "assertIsFilesystemTemporary"]

View File

@ -16,7 +16,11 @@ except ImportError:
from io import BytesIO
from itertools import cycle
from zope.interface import provider
from zope.interface import (
provider,
directlyProvides,
providedBy,
)
from zope.interface.verify import verifyObject
from twisted.python.compat import (_PY3, iterbytes, long, networkString,
@ -42,9 +46,12 @@ from twisted.web.test.requesthelper import (
textLinearWhitespaceComponents,
)
from zope.interface import directlyProvides, providedBy
from twisted.logger import globalLogPublisher
from ._util import (
assertIsFilesystemTemporary,
)
class _IDeprecatedHTTPChannelToRequestInterfaceProxy(proxyForInterface(
@ -1989,10 +1996,14 @@ Hello,
content = []
decoder = []
testcase = self
class MyRequest(http.Request):
def process(self):
content.append(self.content.fileno())
content.append(self.content)
content.append(self.content.read())
# Don't let it close the original content object. We want to
# inspect it later.
self.content = BytesIO()
method.append(self.method)
path.append(self.path)
decoder.append(self.channel._transferDecoder)
@ -2000,13 +2011,12 @@ Hello,
self.finish()
self.runRequest(httpRequest, MyRequest)
# The tempfile API used to create content returns an
# instance of a different type depending on what platform
# we're running on. The point here is to verify that the
# request body is in a file that's on the filesystem.
# Having a fileno method that returns an int is a somewhat
# close approximation of this. -exarkun
self.assertIsInstance(content[0], int)
# We took responsibility for closing this when we replaced the request
# attribute, above.
self.addCleanup(content[0].close)
assertIsFilesystemTemporary(self, content[0])
self.assertEqual(content[1], b'Hello, spam,eggs spam spam')
self.assertEqual(method, [b'GET'])
self.assertEqual(path, [b'/'])

View File

@ -7,6 +7,7 @@ Tests for various parts of L{twisted.web}.
import os
import zlib
from io import BytesIO
from zope.interface import implementer
from zope.interface.verify import verifyObject
@ -26,6 +27,11 @@ from twisted.web.static import Data
from twisted.logger import globalLogPublisher, LogLevel
from twisted.test.proto_helpers import EventLoggingObserver
from ._util import (
assertIsFilesystemTemporary,
)
class ResourceTests(unittest.TestCase):
def testListEntities(self):
@ -943,6 +949,59 @@ class RequestTests(unittest.TestCase):
self.assertNotIn(b'content-type', response.lower())
def test_defaultSmallContentFile(self):
"""
L{http.Request} creates a L{BytesIO} if the content length is small and
the site doesn't offer to create one.
"""
request = server.Request(DummyChannel())
request.gotLength(100000 - 1)
self.assertIsInstance(request.content, BytesIO)
def test_defaultLargerContentFile(self):
"""
L{http.Request} creates a temporary file on the filesystem if the
content length is larger and the site doesn't offer to create one.
"""
request = server.Request(DummyChannel())
request.gotLength(100000)
assertIsFilesystemTemporary(self, request.content)
def test_defaultUnknownSizeContentFile(self):
"""
L{http.Request} creates a temporary file on the filesystem if the
content length is not known and the site doesn't offer to create one.
"""
request = server.Request(DummyChannel())
request.gotLength(None)
assertIsFilesystemTemporary(self, request.content)
def test_siteSuppliedContentFile(self):
"""
L{http.Request} uses L{Site.getContentFile}, if it exists, to get a
file-like object for the request content.
"""
lengths = []
contentFile = BytesIO()
site = server.Site(resource.Resource())
def getContentFile(length):
lengths.append(length)
return contentFile
site.getContentFile = getContentFile
channel = DummyChannel()
channel.site = site
request = server.Request(channel)
request.gotLength(12345)
self.assertEqual([12345], lengths)
self.assertIs(contentFile, request.content)
class GzipEncoderTests(unittest.TestCase):
def setUp(self):