changeset: 95129:5c3dc817ffd7 branch: 2.7 parent: 95124:4502e598fe26 user: R David Murray date: Sun Mar 22 15:15:44 2015 -0400 files: Doc/library/httplib.rst Lib/httplib.py Lib/test/test_httplib.py Misc/ACKS Misc/NEWS description: #23539: Set Content-Length to 0 for PUT, POST, and PATCH if body is None. Some http servers will reject PUT, POST, and PATCH requests if they do not have a Content-Length header. Patch by James Rutherford. diff -r 4502e598fe26 -r 5c3dc817ffd7 Doc/library/httplib.rst --- a/Doc/library/httplib.rst Sun Mar 22 10:11:54 2015 -0400 +++ b/Doc/library/httplib.rst Sun Mar 22 15:15:44 2015 -0400 @@ -433,9 +433,16 @@ and the selector *url*. If the *body* argument is present, it should be a string of data to send after the headers are finished. Alternatively, it may be an open file object, in which case the contents of the file is sent; this - file object should support ``fileno()`` and ``read()`` methods. The header - Content-Length is automatically set to the correct value. The *headers* - argument should be a mapping of extra HTTP headers to send with the request. + file object should support ``fileno()`` and ``read()`` methods. The + *headers* argument should be a mapping of extra HTTP headers to send with + the request. + + If one is not provided in *headers*, a ``Content-Length`` header is added + automatically for all methods if the length of the body can be determined, + either from the length of the ``str`` representation, or from the reported + size of the file on disk. If *body* is ``None`` the header is not set except + for methods that expect a body (``PUT``, ``POST``, and ``PATCH``) in which + case it is set to ``0``. .. versionchanged:: 2.6 *body* can be a file object. diff -r 4502e598fe26 -r 5c3dc817ffd7 Lib/httplib.py --- a/Lib/httplib.py Sun Mar 22 10:11:54 2015 -0400 +++ b/Lib/httplib.py Sun Mar 22 15:15:44 2015 -0400 @@ -247,6 +247,10 @@ _is_legal_header_name = re.compile(r'\A[^:\s][^:\r\n]*\Z').match _is_illegal_header_value = re.compile(r'\n(?![ \t])|\r(?![ \t\n])').search +# We always set the Content-Length header for these methods because some +# servers will otherwise respond with a 411 +_METHODS_EXPECTING_BODY = {'PATCH', 'POST', 'PUT'} + class HTTPMessage(mimetools.Message): @@ -1043,19 +1047,25 @@ """Send a complete request to the server.""" self._send_request(method, url, body, headers) - def _set_content_length(self, body): - # Set the content-length based on the body. + def _set_content_length(self, body, method): + # Set the content-length based on the body. If the body is "empty", we + # set Content-Length: 0 for methods that expect a body (RFC 7230, + # Section 3.3.2). If the body is set for other methods, we set the + # header provided we can figure out what the length is. thelen = None - try: - thelen = str(len(body)) - except TypeError, te: - # If this is a file-like object, try to - # fstat its file descriptor + if body is None and method.upper() in _METHODS_EXPECTING_BODY: + thelen = '0' + elif body is not None: try: - thelen = str(os.fstat(body.fileno()).st_size) - except (AttributeError, OSError): - # Don't send a length if this failed - if self.debuglevel > 0: print "Cannot stat!!" + thelen = str(len(body)) + except TypeError: + # If this is a file-like object, try to + # fstat its file descriptor + try: + thelen = str(os.fstat(body.fileno()).st_size) + except (AttributeError, OSError): + # Don't send a length if this failed + if self.debuglevel > 0: print "Cannot stat!!" if thelen is not None: self.putheader('Content-Length', thelen) @@ -1071,8 +1081,8 @@ self.putrequest(method, url, **skips) - if body is not None and 'content-length' not in header_names: - self._set_content_length(body) + if 'content-length' not in header_names: + self._set_content_length(body, method) for hdr, value in headers.iteritems(): self.putheader(hdr, value) self.endheaders(body) diff -r 4502e598fe26 -r 5c3dc817ffd7 Lib/test/test_httplib.py --- a/Lib/test/test_httplib.py Sun Mar 22 10:11:54 2015 -0400 +++ b/Lib/test/test_httplib.py Sun Mar 22 15:15:44 2015 -0400 @@ -1,4 +1,5 @@ import httplib +import itertools import array import StringIO import socket @@ -122,21 +123,59 @@ self.content_length = kv[1].strip() list.append(self, item) - # POST with empty body - conn = httplib.HTTPConnection('example.com') - conn.sock = FakeSocket(None) - conn._buffer = ContentLengthChecker() - conn.request('POST', '/', '') - self.assertEqual(conn._buffer.content_length, '0', - 'Header Content-Length not set') + # Here, we're testing that methods expecting a body get a + # content-length set to zero if the body is empty (either None or '') + bodies = (None, '') + methods_with_body = ('PUT', 'POST', 'PATCH') + for method, body in itertools.product(methods_with_body, bodies): + conn = httplib.HTTPConnection('example.com') + conn.sock = FakeSocket(None) + conn._buffer = ContentLengthChecker() + conn.request(method, '/', body) + self.assertEqual( + conn._buffer.content_length, '0', + 'Header Content-Length incorrect on {}'.format(method) + ) - # PUT request with empty body - conn = httplib.HTTPConnection('example.com') - conn.sock = FakeSocket(None) - conn._buffer = ContentLengthChecker() - conn.request('PUT', '/', '') - self.assertEqual(conn._buffer.content_length, '0', - 'Header Content-Length not set') + # For these methods, we make sure that content-length is not set when + # the body is None because it might cause unexpected behaviour on the + # server. + methods_without_body = ( + 'GET', 'CONNECT', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE', + ) + for method in methods_without_body: + conn = httplib.HTTPConnection('example.com') + conn.sock = FakeSocket(None) + conn._buffer = ContentLengthChecker() + conn.request(method, '/', None) + self.assertEqual( + conn._buffer.content_length, None, + 'Header Content-Length set for empty body on {}'.format(method) + ) + + # If the body is set to '', that's considered to be "present but + # empty" rather than "missing", so content length would be set, even + # for methods that don't expect a body. + for method in methods_without_body: + conn = httplib.HTTPConnection('example.com') + conn.sock = FakeSocket(None) + conn._buffer = ContentLengthChecker() + conn.request(method, '/', '') + self.assertEqual( + conn._buffer.content_length, '0', + 'Header Content-Length incorrect on {}'.format(method) + ) + + # If the body is set, make sure Content-Length is set. + for method in itertools.chain(methods_without_body, methods_with_body): + conn = httplib.HTTPConnection('example.com') + conn.sock = FakeSocket(None) + conn._buffer = ContentLengthChecker() + conn.request(method, '/', ' ') + self.assertEqual( + conn._buffer.content_length, '1', + 'Header Content-Length incorrect on {}'.format(method) + ) def test_putheader(self): conn = httplib.HTTPConnection('example.com') diff -r 4502e598fe26 -r 5c3dc817ffd7 Misc/ACKS --- a/Misc/ACKS Sun Mar 22 10:11:54 2015 -0400 +++ b/Misc/ACKS Sun Mar 22 15:15:44 2015 -0400 @@ -1171,6 +1171,7 @@ Mark Russell Rusty Russell Nick Russo +James Rutherford Chris Ryland Constantina S. Patrick Sabin diff -r 4502e598fe26 -r 5c3dc817ffd7 Misc/NEWS --- a/Misc/NEWS Sun Mar 22 10:11:54 2015 -0400 +++ b/Misc/NEWS Sun Mar 22 15:15:44 2015 -0400 @@ -21,6 +21,10 @@ Library ------- +- Issue #23539: If body is None, http.client.HTTPConnection.request now sets + Content-Length to 0 for PUT, POST, and PATCH headers to avoid 411 errors from + some web servers. + - Issue #23136: _strptime now uniformly handles all days in week 0, including Dec 30 of previous year. Based on patch by Jim Carroll.