Uh oh!
There was an error while loading. Please reload this page.
Implement streaming multipart uploads - #857
Conversation
754cba8 to
e28bb0dCompareMost interestingly, this shields us from issues people have been having in Requests/urllib3 due to urllib3 not supporting streaming in this case, eg:
Would be interesting to validate against a local but realistic setup though, eg streaming a 500MB+ file over to a local server, and see how memory usage behaves. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
I ran a bunch of experiments against HTTPBin and a Django microserver to see how they'd behave to the presence/absence of They basically treat it as So it looks like in practice Details below: Click to expandFirst, issuing requests against Including the >>>importhttpx>>>url="https://httpbin.org/post">>>lorem=open("debug/lorem.txt")
>>>files= {"lorem": lorem}
>>>data= {"hello": "world"}
>>>r=httpx.post(url, data=data, files=files)
>>>r.status_code200>>>r.json()
{'args': {},
'data': '',
'files': {'lorem': '\n\nLorem ipsum <...Redacted for brievety...>'},
'form': {'hello': 'world'},
'headers': {'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Content-Length': '3261',
'Content-Type': 'multipart/form-data; boundary=e7fa8325d92b926b35d877268b7b8435',
'Host': 'httpbin.org',
'User-Agent': 'python-httpx/0.12.0',
'X-Amzn-Trace-Id': 'Root=1-5e69ebc0-2cde1d1a6d866d38fbb79c57'},
'json': None,
'origin': '92.169.139.224',
'url': 'https://httpbin.org/post'}If >>>importhttpx>>>url="https://httpbin.org/post">>>lorem=open("debug/lorem.txt")
>>>files= {"lorem": lorem}
>>>data= {"hello": "world"}
>>>client=httpx.Client()
>>>request=client.build_request("POST", url, data=data, files=files)
>>>request.headers.pop("content-length")
'3261'>>>r=client.send(request)
>>>r.status_code200>>>r.request.headers# NOTE: no 'content-length' here.Headers({'host': 'httpbin.org', 'user-agent': 'python-httpx/0.12.0', 'accept': '*/*', 'accept-encoding': 'gzip, deflate, br', 'connection': 'keep-alive', 'content-type': 'multipart/form-data; boundary=405abb1f7a6217c5c1bfbfbf8b96010e'})
>>>r.json() # See how this is mostly empty?
{'args': {},
'data': '',
'files': {},
'form': {},
'headers': {'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Content-Length': '0',
'Content-Type': 'multipart/form-data; boundary=405abb1f7a6217c5c1bfbfbf8b96010e',
'Host': 'httpbin.org',
'User-Agent': 'python-httpx/0.12.0',
'X-Amzn-Trace-Id': 'Root=1-5e69eb31-ef22d77230959bd05da2aac4'},
'json': None,
'origin': '92.169.139.224',
'url': 'https://httpbin.org/post'}Now with First with >>>importrequests>>>fromrequests_toolbelt.multipart.encoderimportMultipartEncoder>>>lorem=open("debug/lorem.txt", "rb")
>>>fields= {"lorem": ("lorem.txt", lorem, "text/plain"), "hello": "world"}
>>>m=MultipartEncoder(fields=fields)
>>>url="https://httpbin.org/post">>>r=requests.post(url, data=m, headers={"Content-Type": m.content_type})
>>>r.status_code200>>>r.request.headers
{'User-Agent': 'python-requests/2.23.0', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Type': 'multipart/form-data; boundary=f34a54a283644e4da0f6cc94c9f11548', 'Content-Length': '3261'}Now, dropping >>>importrequests>>>fromrequests_toolbelt.multipart.encoderimportMultipartEncoder>>>lorem=open("debug/lorem.txt", "rb")
>>>fields= {"lorem": ("lorem.txt", lorem, "text/plain"), "hello": "world"}
>>>m=MultipartEncoder(fields=fields)
>>>url="https://httpbin.org/post">>>s=requests.Session()
>>># BUG: 'MultipartEncoder' is not iterable>>>data=m.to_string()
>>># NOTE: request won't be streamed now, but not the point here.>>>req=requests.Request("POST", url, data=data, headers={"Content-Type": m.content_type})
>>>prepped=req.prepare()
>>># BUG: 'int has no len()' (because iterating over bytes yields ints)>>>prepped.body=prepped.body.splitlines()
>>>prepped.headers.pop("Content-Length")
'3261'>>>r=s.send(prepped)
>>>r.status_code200>>>r.json()
{'args': {},
'data': '',
'files': {},
'form': {},
'headers': {'Content-Type': 'multipart/form-data; boundary=9515a8b9bea94739968c0976c03d8d63',
'Host': 'httpbin.org',
'X-Amzn-Trace-Id': 'Root=1-5e69eeeb-e2950740e34e2f3045592140'},
'json': None,
'origin': '92.169.139.224',
'url': 'https://httpbin.org/post'}Now, is this specific to HTTPBin? Let's try with a Django microserver. # app.pyimportjsonfromdjango.confimportsettingsfromdjango.core.handlers.wsgiimportWSGIHandlerfromdjango.httpimportHttpResponsefromdjango.urlsimportpathsettings.configure(ROOT_URLCONF=__name__,)
defupload_file(request):
content= {
"method": request.method,
"headers": dict(request.headers),
"data": dict(request.POST),
"files": dict(request.FILES),
}
returnHttpResponse(json.dumps(content), content_type="application/json")
urlpatterns= [path("", upload_file)]
app=WSGIHandler()uvicorn app:app --interface wsgi>>>importhttpx>>>url="http://localhost:8000/">>>lorem=open("debug/lorem.txt")
>>>files= {"lorem": lorem}
>>>data= {"hello": "world"}
>>>client=httpx.Client()
>>>request=client.build_request("POST", url, data=data, files=files)
>>>request.headers.pop("content-length")
'3261'>>>r=client.send(request)
>>>r.status_code200>>>r.json()
{'method': 'POST',
'headers': {'Content-Length': '0',
'Host': 'localhost:8000',
'User-Agent': 'python-httpx/0.12.0',
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Content-Type': 'multipart/form-data; boundary=65a4b7387ae71a5324d29bbaaa9375bd'},
'data': {},
'files': {}}So, here again, Django interpreted the absence of |
florimondmanca
commented
Mar 13, 2020
Ah so, actually, it's us that default to setting httpx/httpx/_dispatch/urllib3.py Line 94 in a82adcc I'll try this against #804 and see if we get the same results. |
florimondmanca
commented
Mar 13, 2020
After experimenting some more with #863, the But the conclusion I reached is:
|
@tomchristie I think this is ready for a re-review. I've revised a bit my assumptions: it's probably okay to just load the entire contents into memory if we end up dealing with weird file-like objects that are readable but non-seekable. This means we can cover all sensible situations consistently (i.e. include |
3408988 to
0b9605dCompare
yeraydiazdiaz
left a comment
There was a problem hiding this comment.
This looks great, just a question for my own curiosity. 👏 👏 👏
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
florimondmanca
commented
Mar 24, 2020
Hey @tomchristie, how's this looking? :-) Are we ready to merge yet? |
Uh oh!
There was an error while loading. Please reload this page.
Alright, updated these on top of the HTTPCore usage that's now in |
florimondmanca
commented
Apr 10, 2020
Okay, thanks, merging now. :-) |
Fixes #114. ✨
Ensures we don't ever load any files or buffers in full into memory when uploading form data.
I've also refined tests to make sure we have full test coverage on the multipart implementation.