requests
requests copied to clipboard
Pass response_kw to HTTPConnectionPool through HTTPAdapter.send
Summary
This PR adds kwargs arguments to HTTPAdapter.send, which it passes to HTTPConnectionPool.urlopen in urllib3.
Description
As discussed in #4956, urrllib3 recently changed the default value of enforce_content_length from False to True. The new default seems like a sane choice, but in some use cases the Content-Length header should not be enforced. To change the default behavior, urllib3 allows application code to set the enforce_content_length argument. As far as I know, requests does not have a way to pass this argument to urllib3.
The HTTPConnectionPool.urlopen method in urllib3 has the **response_kw kwargs argument to pass extra arguments down to the response parser. This PR adds a similar to argument to HTTPAdapter.send.
With this PR, users can override the HTTPAdapter.send method to pass extra arguments to HTTPConnectionPool.urlopen. For example, this enables users to explicitly set the enforce_content_length.
Example
import requests
from requests.adapters import HTTPAdapter
class EnforceContentLengthAdapter(HTTPAdapter):
def send(self, *args, **kwargs):
kwargs["enforce_content_length"] = False
return super().send(*args, **kwargs)
s = requests.Session()
s.mount("http://", EnforceContentLengthAdapter())
s.mount("https://", EnforceContentLengthAdapter())
r = s.get("http://localhost:8080/")
print(r.raw.enforce_content_length) # Returns False
Hi @joren485 ,
Requests is under a feature freeze. Further, even though this may seem like a backwards compatible change, it in fact will likely cause issues for a good percentage of users if they are unware. Many people implement customer HTTPAdapters for various reasons and I'd estimate at least 50% copy the exact signature out of requests when they write the code and don't expect to need to update things after the fact. This effectively expands the signature in a way that isn't backwards compatible as now, someone may start using a library that has too permissive of a lower bound on requests with an adapter that requires this and it will cause confusing and unexpected errors in passing too many arguments. This will cost countless people an unnecessary amount of time trying to debug things.
With that in mind, I could see a potential benefit to adding a separate attribute on the HTTPAdapter that is not added to any function signature (i.e., not to __init__, send, etc.) and is pickled properly that is a dictionary of urllib3_response_options, e.g.,
self.urllib3_response_options = {}
As the default that could then be updated with self.urllib3_response_options.setdefault("enforce_content_length", True). This could then be used in https://github.com/psf/requests/blob/881281250f74549f560408e5546d95a8cd73ce28/src/requests/adapters.py#L485-L497 where I would create a copy of the dictionary and then override it with what we have in the kwargs to that call, and the explode (**) into the urlopen call.
Does that make sense to you?
@sigmavirus24
That definitely makes sense to me! I pushed a first version of your idea. Is this what you had in mind?
It would work like this:
import requests
from requests.adapters import HTTPAdapter
class EnforceContentLengthAdapter(HTTPAdapter):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.urllib3_response_options.setdefault("enforce_content_length", False)
s = requests.Session()
s.mount("http://", EnforceContentLengthAdapter())
s.mount("https://", EnforceContentLengthAdapter())
r = s.get("http://localhost:8080/")
print(r.raw.enforce_content_length)
I am aware of the feature freeze, but I dot not consider this change a feature, but rather backwards compatibility as the default behavior of requests has changed. Is this PR allowed?
@sigmavirus24 I implemented your suggestions.
I removed the need for self.urllib3_response_options.copy() by providing **self.urllib3_response_options directly to urlopen, because it minimizes the code changes and looks cleaner imo. This way, user code is still not able to override the given arguments (and only provide new arguments like enforce_content_length), as providing duplicate arguments throws a TypeError.
However, if you prefer the use of a separate dictionary with self.urllib3_response_options.copy(), I will happily change it back.
@nateprewitt Thank you very much for your suggestions, I am happy to implement them. @sigmavirus24 do you agree with the suggested changes?
I understand the use case, although it's fairly straightforward to work around.
Could you expand on this? The only way to work around the new enforce_content_length behavior, that I know of, is to monkey patch urllib3. Do you have a better workaround?
The only way to work around the new enforce_content_length behavior, that I know of, is to monkey patch urllib3
You can inherit from the adapter and override the send method that you are modifying here.
You could also create a pool manager that sets this parameter and set it on the adapter. Specifically, a Pool manager accepts ConnectionPool kwargs and the Connection Pool accepts response kwargs.
Honestly, it's probably best to update the adapter to pass this to how we create the pool manager now that we're talking about it. It will be far fewer copies and updates here and will look like how folks can already use this today
Sorry for the twists here @joren485. I've been reviewing this entirely from my phone so much harder to review docs etc at the same time and make a better decision
You can inherit from the adapter and override the send method that you are modifying here.
I wanted to prevent the need for such an override with this PR, as HTTPAdapter.send is quite a large method and overriding it to change a single call in the middle of the method would require manually copying any upstream changes.
You could also create a pool manager that sets this parameter and set it on the adapter. Specifically, a Pool manager accepts ConnectionPool kwargs and the Connection Pool accepts response kwargs.
I do not see how this would work, as the connection_pool_kw kwargs set by PoolManager._new_pool are not used in HTTPConnectionPool.urlopen (or HTTPConnectionPool._make_request). PoolManager does not seem to have control over the arguments passed to HTTPConnectionPool.urlopen. Please correct me if I am wrong.