Index: Misc/NEWS =================================================================== --- Misc/NEWS (revision 65550) +++ Misc/NEWS (working copy) @@ -41,6 +41,8 @@ Library ------- +- Issue#1346874: Added 100-continue support to httplib + - Changed code in the following modules/packages to remove warnings raised while running under the ``-3`` flag: aifc, asyncore, bdb, bsddb, ConfigParser, cookielib, DocXMLRPCServer, email, filecmp, fileinput, inspect, Index: Doc/library/httplib.rst =================================================================== --- Doc/library/httplib.rst (revision 65550) +++ Doc/library/httplib.rst (working copy) @@ -84,6 +84,8 @@ .. versionadded:: 2.0 + .. versionchanged:: 2.6 + *ignore_continue* was added. The following exceptions are raised as appropriate: @@ -95,6 +97,13 @@ .. versionadded:: 2.0 +.. exception:: ExpectationFailed + + A subclass of :exc:`HTTPException`, raised if a server returns a 417 + (Expectation Failed) after sending an Expect: 100-continue header + + .. versionadded:: 2.6 + .. exception:: NotConnected A subclass of :exc:`HTTPException`. @@ -415,6 +424,9 @@ Should be called after a request is sent to get the response from the server. Returns an :class:`HTTPResponse` instance. + .. versionchanged:: 2.6 + *ignore_continue* was added. + .. note:: Note that you must have read the whole response before you can send a new Index: Lib/httplib.py =================================================================== --- Lib/httplib.py (revision 65550) +++ Lib/httplib.py (working copy) @@ -320,11 +320,13 @@ # See RFC 2616 sec 19.6 and RFC 1945 sec 6 for details. - def __init__(self, sock, debuglevel=0, strict=0, method=None): + def __init__(self, sock, debuglevel=0, strict=0, method=None, + ignore_continue=True): self.fp = sock.makefile('rb', 0) self.debuglevel = debuglevel self.strict = strict self._method = method + self.ignore_continue = ignore_continue self.msg = None @@ -380,19 +382,21 @@ # we've already started reading the response return - # read until we get a non-100 response - while True: - version, status, reason = self._read_status() - if status != CONTINUE: - break - # skip the header from the 100 response + if self.ignore_continue: + # read until we get a non-100 response while True: - skip = self.fp.readline().strip() - if not skip: + version, status, reason = self._read_status() + if status != CONTINUE: break - if self.debuglevel > 0: - print "header:", skip - + # skip the header from the 100 response + while True: + skip = self.fp.readline().strip() + if not skip: + break + if self.debuglevel > 0: + print "header:", skip + else: + version, status, reason = self._read_status() self.status = status self.reason = reason.strip() if version == 'HTTP/1.0': @@ -883,6 +887,11 @@ if 'accept-encoding' in header_names: skips['skip_accept_encoding'] = 1 + expect_continue = False + for hdr, val in headers.iteritems(): + if 'expect' == hdr.lower() and '100-continue' in val.lower(): + expect_continue = True + self.putrequest(method, url, **skips) if body and ('content-length' not in header_names): @@ -905,10 +914,23 @@ self.putheader(hdr, value) self.endheaders() + # If an Expect: 100-continue was sent, we need to check for a 417 + # Expectation Failed to avoid unecessarily sending the body + # See RFC 2616 8.2.3 + if expect_continue: + if not body: + raise HTTPException("A body is required when expecting " + "100-continue") + resp = self.getresponse(ignore_continue=False) + resp.read() + self.__state = _CS_REQ_SENT + if resp.status == EXPECTATION_FAILED: + raise ExpectationFailed + if body: self.send(body) - def getresponse(self): + def getresponse(self, ignore_continue=True): "Get the response from the server." # if a prior response has been completed, then forget about it. @@ -937,10 +959,12 @@ if self.debuglevel > 0: response = self.response_class(self.sock, self.debuglevel, strict=self.strict, - method=self._method) + method=self._method, + ignore_continue=ignore_continue) else: response = self.response_class(self.sock, strict=self.strict, - method=self._method) + method=self._method, + ignore_continue=ignore_continue) response.begin() assert response.will_close != _UNKNOWN @@ -1107,6 +1131,9 @@ # or define self.args. Otherwise, str() will fail. pass +class ExpectationFailed(HTTPException): + pass + class NotConnected(HTTPException): pass Index: Lib/test/test_httplib.py =================================================================== --- Lib/test/test_httplib.py (revision 65550) +++ Lib/test/test_httplib.py (working copy) @@ -22,6 +22,9 @@ raise httplib.UnimplementedFileMode() return self.fileclass(self.text) + def close(self): + pass + class NoEOFStringIO(StringIO.StringIO): """Like StringIO, but raises AssertionError on EOF. @@ -73,6 +76,32 @@ conn.request('POST', '/', body, headers) self.assertEqual(conn._buffer.count[header.lower()], 1) + +class OneHundredContinueTests(TestCase): + def test_continue_sent(self): + body = "HTTP/1.1 100 Continue\r\n\r\n" + conn = httplib.HTTPConnection('example.com') + sock = FakeSocket(body) + conn.sock = sock + + body = 'testdata' + headers = {'Expect': '100-continue'} + conn.request(method='GET', url='/foo', body=body, headers=headers) + self.assertTrue(body in sock.data) + + def test_expectation_failed(self): + body = "HTTP/1.1 417 Expectation Failed\r\n\r\n" + conn = httplib.HTTPConnection('example.com') + sock = FakeSocket(body) + conn.sock = sock + + body = 'testdata' + headers = {'Expect': '100-continue'} + self.assertRaises(httplib.ExpectationFailed, conn.request, + method='GET', url='/foo', body=body, headers=headers) + self.assertTrue(body not in sock.data) + + class BasicTest(TestCase): def test_status_lines(self): # Test HTTP status lines @@ -90,7 +119,7 @@ self.assertRaises(httplib.BadStatusLine, resp.begin) def test_partial_reads(self): - # if we have a lenght, the system knows when to close itself + # if we have a length, the system knows when to close itself # same behaviour than when we read the whole thing with read() body = "HTTP/1.1 200 Ok\r\nContent-Length: 4\r\n\r\nText" sock = FakeSocket(body) @@ -255,7 +284,7 @@ def test_main(verbose=None): test_support.run_unittest(HeaderTests, OfflineTest, BasicTest, TimeoutTest, - HTTPSTimeoutTest) + HTTPSTimeoutTest, OneHundredContinueTests) if __name__ == '__main__': test_main()