You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Form-encoded data with None as the value gets passed as string "None" #1500
There are no similar issues or pull requests to fix it yet.
The bug is reproducible against the latest release and/or master.
Describe the bug
When posting form data using requests and the form value is None, nothing is passed. With httpx, the None is converted to the string "None".
To reproduce
importrequestsimporthttpxurl='https://httpbin.org/post'data=dict(foo=None)
resp_requests=requests.post(url, data=data)
pprint(resp_requests.json())
# In the response: 'form': {},resp_httpx=httpx.post(url, data=data)
pprint(resp_httpx.json())
# In the response: 'form': {'foo': 'None'},
Expected behavior
Expected behavior to match requests, since "broadly" the behavior is expected to match
Did not expect that "None" would be passed, rather e.g. an empty string might make sense?
Actual behavior
The passed form-encoded data contains the string "None"
For some reason this rings a bell. Are we sure there are no previous issues about this?
Otherwise yeah I think the "None" string is a bit surprising. I would advise validating this isn't specifically linked to HTTPBin, eg inspect what the actual request body looks like.
From what I've seen, passing empty string instead of None works fine both for requests and httpx, and round-trips with httpbin. This is a perfectly fine workaround for me. However, for compatibility with requests, converting None to empty string will not yield identical results, since then the empty string is in fact passed with the POST request.
I've verified that the empty-string workaround works for the project I am currently working on, so it is not only a httpbin issue. I used httpbin to create a reproducible minimal example.
Personally, I would be least surprised if httpx followed requests, since this is what the documentation promises. A loud error would be surprising but would not lead to silent failure, so in my view that would be acceptable.
From this, it means that any non-str/bytes value will yield unexpected results, such as passing a custom object (like above), passing True (foo=True), etc.
Actually the None case has been reported on bpo in 2013: https://bugs.python.org/issue18857 still discussed actively in 2020. Looks like the motivation/discussion there was to allow no-value querystring items, such as ?foo, as opposed to ?foo= which would happen if None was treated as ''.
Anyway, I sense head-scratching ahead, so I'd be tempted to keep things as-is for simplicity. (Plus, I believe issues around special-casing values like None have been a PITA for Requests over time.) We could just add a note in our Requests Compatibility guide about this. WDYT?
We might consider applying the same transformation in encode_urlencoded_data()…? That would still be different from Requests (and we'd have to document that — we already for for query params: https://www.python-httpx.org/compatibility/#query-parameters), but at least it would be consistent with what we already do for query params.
I see several sensible options - clearly the "None" string representation is the only one which is obviously not so great. I'd cast a vote for consistency, since being consistent makes life much easier for end users, even if a choice is not "locally optimal" it makes reasoning about "what would httpx do" easier.
Personally I think requests' behaviour here is unintuitive.
We might consider applying the same transformation in encode_urlencoded_data()…? That would still be different from Requests (and we'd have to document that — we already for for query params: https://www.python-httpx.org/compatibility/#query-parameters), but at least it would be consistent with what we already do for query params.
Activity
pletnes commentedon Mar 8, 2021
Do you consider this a bug, or just "behaves differently"?
florimondmanca commentedon Mar 8, 2021
Hi :)
For some reason this rings a bell. Are we sure there are no previous issues about this?
Otherwise yeah I think the "None" string is a bit surprising. I would advise validating this isn't specifically linked to HTTPBin, eg inspect what the actual request body looks like.
florimondmanca commentedon Mar 8, 2021
Not marking as a bug yet because we'd also need to validate if "null values" make sense in the context of HTTP forms at all.
If not, consider matching Requests by ignoring None fields?
Or raising a loud error as "this is ambiguous"?
pletnes commentedon Mar 8, 2021
From what I've seen, passing empty string instead of None works fine both for requests and httpx, and round-trips with httpbin. This is a perfectly fine workaround for me. However, for compatibility with requests, converting
Noneto empty string will not yield identical results, since then the empty string is in fact passed with thePOSTrequest.I've verified that the empty-string workaround works for the project I am currently working on, so it is not only a httpbin issue. I used httpbin to create a reproducible minimal example.
Personally, I would be least surprised if httpx followed requests, since this is what the documentation promises. A loud error would be surprising but would not lead to silent failure, so in my view that would be acceptable.
pletnes commentedon Mar 8, 2021
Relevant discussion for
request, a node.js package, which argues thatNone,nulletc are language-specific and do not make sense.request/request#1832
Scrapy appears to do the same as requests:
https://docs.scrapy.org/en/latest/topics/request-response.html#scrapy.http.FormRequest.from_response
The
aiohttppackage does the same ashttpx:yields:
florimondmanca commentedon Mar 8, 2021
@pletnes Thanks, those are very useful pointers. :-)
As to why this happens:
When
data=<dict>is passed, we send it as URL-encoded form data, built through this call:httpx/httpx/_content.py
Lines 106 to 113 in f3c2941
So our behavior essentially boils down to the stdlib doing this:
I think that's somewhat undefined behavior, since the docs read:
urlencode()officially only accepts str or bytes, but actually it accepts anything else, in which cases it just callsstr()on it:From this, it means that any non-str/bytes value will yield unexpected results, such as passing a custom object (like above), passing
True(foo=True), etc.Actually the
Nonecase has been reported on bpo in 2013: https://bugs.python.org/issue18857 still discussed actively in 2020. Looks like the motivation/discussion there was to allow no-value querystring items, such as?foo, as opposed to?foo=which would happen ifNonewas treated as''.Anyway, I sense head-scratching ahead, so I'd be tempted to keep things as-is for simplicity. (Plus, I believe issues around special-casing values like
Nonehave been a PITA for Requests over time.) We could just add a note in our Requests Compatibility guide about this. WDYT?florimondmanca commentedon Mar 8, 2021
Other relevant threads:
Nonevalue, ie the current behavior of Requests)#175 resolved #154 by treating
params={"foo": None}as?foo=. Which made me realize we have this small utility function:httpx/httpx/_utils.py
Lines 59 to 71 in c725387
Which we use in
QueryParamshere:httpx/httpx/_models.py
Line 450 in 8696405
We might consider applying the same transformation in
encode_urlencoded_data()…? That would still be different from Requests (and we'd have to document that — we already for for query params: https://www.python-httpx.org/compatibility/#query-parameters), but at least it would be consistent with what we already do for query params.pletnes commentedon Mar 9, 2021
I see several sensible options - clearly the
"None"string representation is the only one which is obviously not so great. I'd cast a vote for consistency, since being consistent makes life much easier for end users, even if a choice is not "locally optimal" it makes reasoning about "what would httpx do" easier.lovelydinosaur commentedon Mar 9, 2021
Personally I think requests' behaviour here is unintuitive.
Yes that'd be my strong preference too.