gh-42550: Add 'Expect: 100-Continue' support to httplib#133276
gh-42550: Add 'Expect: 100-Continue' support to httplib#133276IngridMorstrad wants to merge 4 commits into
Conversation
Previously, http.client would always send content body immediately and ignore any 100 responses. This change makes HTTPClient.request() wait for a `Continue` response if the `Expect: 100-Continue` header is set, and adds a parameter to HTTPClient.getresponse() that will cause it to return 100 responses instead of eating them.
Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
|
Hi, checking for a possible review on this. @ZeroIntensity |
|
Sorry, I was only triaging. I don't have enough knowledge on the |
| sending a file-like message body. Finally, the optional *continue_timeout* | ||
| parameter controls how long the connection will wait for a ``100 Continue`` | ||
| response from the server before sending the body, if the request included | ||
| an ``Expect: 100-Continue`` header. |
There was a problem hiding this comment.
Does this need to be a constructor parameter?
There was a problem hiding this comment.
I was thinking that since it is a property of the server (how quickly it responds with 100 Continue, or whether it understands Expect at all), it should be in the constructor, not the individual request. The other per-request parameters (expect_continue, ignore_100_continue) could vary per-request. LMK if that makes sense or we need to change this and I can update the PR.
|
You have merge conflicts, please resolve them before requesting a re-review. |
|
Sorry for the delay here, I just replied to the question above. If that response makes sense, I can send an updated PR with the merge conflicts addressed. |
|
Hi, I have fixed the merge conflicts and CI is green. The PR has been open since May 2025. I am not sure who to request a review from as there is no reviewer listed for the Ashwin |
|
Hi @orsenthil , could I please get a review of this? CI is green and I keep needing to rebase over time. |
|
This PR is stale because it has been open for 30 days with no activity. |
gpshead
left a comment
There was a problem hiding this comment.
Left a pile of comments.
We've gone forever without this in our HTTP client. It should be possible to make work, but it might get weird in our stack in the modern TLS HTTPSConnection that everyone should be using instead of HTTPConnection... So maybe something to figure out there. Does this actually work properly in that scenario?
| return version, status, reason | ||
|
|
||
| def begin(self, *, _max_headers=None): | ||
| def begin(self, ignore_100_continue=True, _max_headers=None): |
There was a problem hiding this comment.
the * was important, it makes these keyword only arguments which are easier to support on public APIs like this method. (rule of thumb, when adding a new parameter it either has to be added at the end and if there are either a lot of arguments or are very infrequently passed should be added as keyword only so that it must be passed by name)
| def begin(self, ignore_100_continue=True, _max_headers=None): | |
| def begin(self, *, ignore_100_continue=True, _max_headers=None): |
|
|
||
| def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, | ||
| source_address=None, blocksize=8192, *, max_response_headers=None): | ||
| source_address=None, blocksize=8192, continue_timeout=2.5, *, |
There was a problem hiding this comment.
continue_timeout should be a keyword only argument.... but is the constructur really where we want it rather than on the request() method where headers are specified? That'd also avoid the need to plumb this through HTTPSConnection.__init__.
From an API design perspective, on the HTTPConnection.request method which takes a headers={} dict as well as some flags such as potentially this... If someone wants to use this feature, it is ergonomically nicer if we had a flag on the request_api that explicitly adds the correct Expect: 100-Continue header rather than expecting people to fill in that level of implementation detail header value directly themselves. (I think the code still needs detect if the header was added manually regardless, kind of like it does for some of the other headers)
also, assert that continue_timeout > 0.0.
| def getresponse(self, ignore_100_continue=True): | ||
| """Get the response from the server. | ||
|
|
||
| If the HTTPConnection is in the correct state, returns an | ||
| instance of HTTPResponse. | ||
| instance of HTTPResponse or of whatever object is returned by the | ||
| response_class variable. The connection will wait for a response other | ||
| than code 100 ('Continue') unless ignore_100_continue is set to False. |
There was a problem hiding this comment.
"ignore..." is a somewhat of a negative term in English for use in an API which leads to awkward descriptions of the behavior. it defaults to True and people have to turn the negative off with False leading to double negatives to reason about.
As this defaults to not supporting 100 continue behavior, why not call it support_continue=False so that enabling support for it involves setting something to True instead of setting an ignore= to False?
(apply this everywhere)
also we should make it keyword only so that it has to be spelled out when anyone wants to use it for better code readability. (blah.getresponse(True) doesn't indicate much to the reader).
I also think eliding the number from the name is a good idea. 100 is defined as continue so no need to mention that internal http detail.
| def create_connection(self, *pos, **kw): | ||
| return FakeSocket(*self.fake_socket_args) | ||
|
|
||
| def fake_select(self, rlist, wlist, xlist, timeout=None): |
There was a problem hiding this comment.
this doesn't use self, make it a @staticmethod or a top level function?
| >>> print(response.status, response.reason) | ||
| 200, OK | ||
|
|
||
| Since version 3.15, conditional transmission of the body is supported when an |
There was a problem hiding this comment.
don't mention the version here. think of docs as being read in the future, version specific changes are normally footnotes via the version added and version changed markers. (this is a feature so it won't be in 3.15)
|
|
||
| if message_body is not None: | ||
| if expect_continue and not self.__response: | ||
| read_ready, _, _ = self._select([self.sock], [], [], |
There was a problem hiding this comment.
select won't work properly on a HTTPSConnection. There's a .pending method on SSLSocket that could be used in this scenario if read_ready.
Investigate what is plausible and add some tests covering the HTTPSConnection test case.
| else: | ||
| response = self.response_class(self.sock, | ||
| method=self._method) | ||
| response.begin(ignore_100_continue=False) |
There was a problem hiding this comment.
_max_headers= should also be passed here.
| response.begin(ignore_100_continue=False) | ||
| if response.code != CONTINUE: | ||
| # Break without sending the body | ||
| self.__pending_response = response |
There was a problem hiding this comment.
wherever self.__response is cleared (= None) such as putrequest, self.__pending_response needs to cleared as well. tests should cover that this happens by running through a scenario that'd lead to this such as maybe a non-100 early response that gets ignored before another request is sent on the same connection?
| @@ -0,0 +1,2 @@ | |||
| :class:`http.client.HTTPConnection` now waits for a ``100 Continue`` response | |||
There was a problem hiding this comment.
We should highlight that this is a behavior change for any code that explicitly added the Extra: 100-Continue to their request headers as those will now wait for the timeout to elapse before sending the body if the server doesn't do 100s.
Also add an entry to the whatsnew 3.16.rst doc.
Previously, http.client would always send content body immediately and ignore any 100 responses. This change makes HTTPClient.request() wait for a
Continueresponse if theExpect: 100-Continueheader is set, and adds a parameter to HTTPClient.getresponse() that will cause it to return 100 responses instead of eating them.#42550
https://bugs.python.org/issue1346874
📚 Documentation preview 📚: https://cpython-previews--133276.org.readthedocs.build/