Skip to content

Design Multipart helper to stream by default #114

Closed

Description

@sethmlarson

This is a frequent problem with urllib3 in that the default behavior loads all of the file's data into memory at once before sending resulting in a huge files getting loaded into memory.

It's a hard problem to fix in urllib3 because of how the API was designed, but we can still fix our implementation not to do this. I've got part of a patch for this currently.

Activity

  1. lovelydinosaur commented on Jul 10, 2019

    @lovelydinosaur
    Member

    Yup, defo.

    One subtlety here is that right now we either support the request content being bytes or a byte iterator. The iterator case can't support body-preserving redirects. So we've actually got a precursor to this issue of supporting a rewindable/seekable type. (To allow rewinding back to the start of the upload.)

  2. sethmlarson commented on Jul 10, 2019

    @sethmlarson
    ContributorAuthor

    Yeah there's some bits and pieces I can lift from urllib3 and some unmerged branches for that specific case. Effectively we add support for passing a seekable object as data and then we implement an file-like interface for the multipart data and internally can rewind the individual file objects when we receive a .seek(0, 0).

    Would something like this be effective?

  3. florimondmanca commented on Mar 5, 2020

    @florimondmanca
    Contributor

    Any updated context on this given where we're at now? Is this still an issue/is it easier to tackle, and how?

  4. lovelydinosaur commented on Mar 9, 2020

    @lovelydinosaur
    Member

    Ah yes. Streaming multipart would be a great feature for us to add now, in particular because it doesn't add any new API surface area at all, and the content streams are designed to allow us to support it. 😃

    Right now we generate the multipart data on __init__...

    def __init__(self, data: dict, files: dict, boundary: bytes = None) -> None:
    body = BytesIO()
    if boundary is None:
    boundary = binascii.hexlify(os.urandom(16))
    for field in self.iter_fields(data, files):
    body.write(b"--%s\r\n" % boundary)
    body.write(field.render_headers())
    body.write(field.render_data())
    body.write(b"\r\n")
    body.write(b"--%s--\r\n" % boundary)
    self.content_type = "multipart/form-data; boundary=%s" % boundary.decode(
    "ascii"
    )
    self.body = body.getvalue()

    Instead we should be moving that into __iter__...

    def __iter__(self) -> typing.Iterator[bytes]:
    yield self.body

    ...and __aiter__...

    async def __aiter__(self) -> typing.AsyncIterator[bytes]:
    yield self.body

    Additionally we'd want FileField.render_data to read and return chunks-at-a-time, and be called multiple times, rather than being called once and returning the entire file content.

  5. florimondmanca commented on Mar 9, 2020

    @florimondmanca
    Contributor

    Okay — looking into it. :-) The bits around estimating the content length without reading the entire data are interesting!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions