Commit beed8402 authored by R David Murray's avatar R David Murray

#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, with additional cleaning up of the
'request' documentation by me.
parent 75ed90a4
...@@ -413,23 +413,33 @@ HTTPConnection Objects ...@@ -413,23 +413,33 @@ HTTPConnection Objects
.. method:: HTTPConnection.request(method, url, body=None, headers={}) .. method:: HTTPConnection.request(method, url, body=None, headers={})
This will send a request to the server using the HTTP request This will send a request to the server using the HTTP request
method *method* and the selector *url*. If the *body* argument is method *method* and the selector *url*.
present, it should be string or bytes object of data to send after
the headers are finished. Strings are encoded as ISO-8859-1, the If *body* is specified, the specified data is sent after the headers are
default charset for HTTP. To use other encodings, pass a bytes finished. It may be a string, a :term:`bytes-like object`, an open
object. The Content-Length header is set to the length of the :term:`file object`, or an iterable of :term:`bytes-like object`\s. If
string. *body* is a string, it is encoded as ISO-8851-1, the default for HTTP. If
it is a bytes-like object the bytes are sent as is. If it is a :term:`file
The *body* may also be an open :term:`file object`, in which case the object`, the contents of the file is sent; this file object should support
contents of the file is sent; this file object should support ``fileno()`` at least the ``read()`` method. If the file object has a ``mode``
and ``read()`` methods. The header Content-Length is automatically set to attribute, the data returned by the ``read()`` method will be encoded as
the length of the file as reported by stat. The *body* argument may also be ISO-8851-1 unless the ``mode`` attribute contains the substring ``b``,
an iterable and Content-Length header should be explicitly provided when the otherwise the data returned by ``read()`` is sent as is. If *body* is an
body is an iterable. iterable, the elements of the iterable are sent as is until the iterable is
exhausted.
The *headers* argument should be a mapping of extra HTTP The *headers* argument should be a mapping of extra HTTP
headers to send with the request. headers to send with the request.
If *headers* does not contain a Content-Length item, one is added
automatically if possible. If *body* is ``None``, the Content-Length header
is set to ``0`` for methods that expect a body (``PUT``, ``POST``, and
``PATCH``). If *body* is a string or bytes object, the Content-Length
header is set to its length. If *body* is a :term:`file object` and it
works to call :func:`~os.fstat` on the result of its ``fileno()`` method,
then the Content-Length header is set to the ``st_size`` reported by the
``fstat`` call. Otherwise no Content-Length header is added.
.. versionadded:: 3.2 .. versionadded:: 3.2
*body* can now be an iterable. *body* can now be an iterable.
......
...@@ -246,6 +246,10 @@ _MAXHEADERS = 100 ...@@ -246,6 +246,10 @@ _MAXHEADERS = 100
_is_legal_header_name = re.compile(rb'[^:\s][^:\r\n]*').fullmatch _is_legal_header_name = re.compile(rb'[^:\s][^:\r\n]*').fullmatch
_is_illegal_header_value = re.compile(rb'\n(?![ \t])|\r(?![ \t\n])').search _is_illegal_header_value = re.compile(rb'\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(email.message.Message): class HTTPMessage(email.message.Message):
# XXX The only usage of this method is in # XXX The only usage of this method is in
...@@ -1126,19 +1130,26 @@ class HTTPConnection: ...@@ -1126,19 +1130,26 @@ class HTTPConnection:
"""Send a complete request to the server.""" """Send a complete request to the server."""
self._send_request(method, url, body, headers) self._send_request(method, url, body, headers)
def _set_content_length(self, body): def _set_content_length(self, body, method):
# Set the content-length based on the body. # 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 thelen = None
try: method_expects_body = method.upper() in _METHODS_EXPECTING_BODY
thelen = str(len(body)) if body is None and method_expects_body:
except TypeError as te: thelen = '0'
# If this is a file-like object, try to elif body is not None:
# fstat its file descriptor
try: try:
thelen = str(os.fstat(body.fileno()).st_size) thelen = str(len(body))
except (AttributeError, OSError): except TypeError:
# Don't send a length if this failed # If this is a file-like object, try to
if self.debuglevel > 0: print("Cannot stat!!") # 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: if thelen is not None:
self.putheader('Content-Length', thelen) self.putheader('Content-Length', thelen)
...@@ -1154,8 +1165,8 @@ class HTTPConnection: ...@@ -1154,8 +1165,8 @@ class HTTPConnection:
self.putrequest(method, url, **skips) self.putrequest(method, url, **skips)
if body is not None and ('content-length' not in header_names): if 'content-length' not in header_names:
self._set_content_length(body) self._set_content_length(body, method)
for hdr, value in headers.items(): for hdr, value in headers.items():
self.putheader(hdr, value) self.putheader(hdr, value)
if isinstance(body, str): if isinstance(body, str):
......
import errno import errno
from http import client from http import client
import io import io
import itertools
import os import os
import array import array
import socket import socket
...@@ -125,21 +126,59 @@ class HeaderTests(TestCase): ...@@ -125,21 +126,59 @@ class HeaderTests(TestCase):
self.content_length = kv[1].strip() self.content_length = kv[1].strip()
list.append(self, item) list.append(self, item)
# POST with empty body # Here, we're testing that methods expecting a body get a
conn = client.HTTPConnection('example.com') # content-length set to zero if the body is empty (either None or '')
conn.sock = FakeSocket(None) bodies = (None, '')
conn._buffer = ContentLengthChecker() methods_with_body = ('PUT', 'POST', 'PATCH')
conn.request('POST', '/', '') for method, body in itertools.product(methods_with_body, bodies):
self.assertEqual(conn._buffer.content_length, b'0', conn = client.HTTPConnection('example.com')
'Header Content-Length not set') conn.sock = FakeSocket(None)
conn._buffer = ContentLengthChecker()
# PUT request with empty body conn.request(method, '/', body)
conn = client.HTTPConnection('example.com') self.assertEqual(
conn.sock = FakeSocket(None) conn._buffer.content_length, b'0',
conn._buffer = ContentLengthChecker() 'Header Content-Length incorrect on {}'.format(method)
conn.request('PUT', '/', '') )
self.assertEqual(conn._buffer.content_length, b'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 = client.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 = client.HTTPConnection('example.com')
conn.sock = FakeSocket(None)
conn._buffer = ContentLengthChecker()
conn.request(method, '/', '')
self.assertEqual(
conn._buffer.content_length, b'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 = client.HTTPConnection('example.com')
conn.sock = FakeSocket(None)
conn._buffer = ContentLengthChecker()
conn.request(method, '/', ' ')
self.assertEqual(
conn._buffer.content_length, b'1',
'Header Content-Length incorrect on {}'.format(method)
)
def test_putheader(self): def test_putheader(self):
conn = client.HTTPConnection('example.com') conn = client.HTTPConnection('example.com')
......
...@@ -1179,6 +1179,7 @@ Sam Rushing ...@@ -1179,6 +1179,7 @@ Sam Rushing
Mark Russell Mark Russell
Rusty Russell Rusty Russell
Nick Russo Nick Russo
James Rutherford
Chris Ryland Chris Ryland
Constantina S. Constantina S.
Patrick Sabin Patrick Sabin
......
...@@ -18,6 +18,10 @@ Core and Builtins ...@@ -18,6 +18,10 @@ Core and Builtins
Library 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 #22351: The nntplib.NNTP constructor no longer leaves the connection - Issue #22351: The nntplib.NNTP constructor no longer leaves the connection
and socket open until the garbage collector cleans them up. Patch by and socket open until the garbage collector cleans them up. Patch by
Martin Panter. Martin Panter.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment