formidable icon indicating copy to clipboard operation
formidable copied to clipboard

Support parsing of multipart/byteranges

Open jcready opened this issue 8 years ago • 5 comments

Currently formidable returns an empty body and a single item in the files object which is named 'null' and only contains the last part of the multipart byte-range body.

jcready avatar Aug 20 '16 19:08 jcready

@felixge any help on that? Maybe https://github.com/jshttp/range-parser worth look?

tunnckoCore avatar Jan 16 '17 02:01 tunnckoCore

I don't understand how this is supposed to work. @jcready can you provide an example request and what you'd expect formidable to do with it?

felixge avatar Jan 16 '17 09:01 felixge

@felixge a multipart/byteranges response would look something like this (taken from RFC 7233):

HTTP/1.1 206 Partial Content
Date: Wed, 15 Nov 1995 06:25:24 GMT
Last-Modified: Wed, 15 Nov 1995 04:58:08 GMT
Content-Length: 1741
Content-Type: multipart/byteranges; boundary=THIS_STRING_SEPARATES

--THIS_STRING_SEPARATES
Content-Type: application/pdf
Content-Range: bytes 500-999/8000

...the first range...
--THIS_STRING_SEPARATES
Content-Type: application/pdf
Content-Range: bytes 7000-7999/8000

...the second range
--THIS_STRING_SEPARATES--

I imagine the parsed body would end up looking like this:

[
  {
    "headers": {
      "content-type": "application/pdf",
      "content-range": "bytes 500-999/8000"
    },
    "body": "...the first range..."
  },
  {
    "headers": {
      "content-type": "application/pdf",
      "content-range": "bytes 7000-7999/8000"
    },
    "body": "...the second range"
  }
]

Ideally this functionality would just be an additional parser like lib/byterange_parser.js where it would be used if the response's Content-Type started with multipart/byteranges. I've managed to get the desired output by overwriting formidable.IncomingForm.prototype.parse:

formidable.IncomingForm.prototype.parse = function parseByteRanges (res, done) {
  const parts = []
  let totalLength = 0
  res.on('error', done).on('data', (chunk) => {
    parts.push(chunk)
    totalLength += chunk.length
  }).on('end', () => {
    try {
      const boundary = res.headers['content-type'].match(/boundary=(.+)$/)[1]
      const body = Buffer.concat(parts, totalLength)
      done(null, parseMultipartBody(body.toString(), boundary))
    } catch (e) {
      done(e)
    }
  })
}

function parseMultipartBody (body, boundary) {
  return body.split('--' + boundary).reduce((memo, part) => {
    if (part && part !== '--') {
      const [ head, body ] = part.trim().split(/\r\n\r\n/g)
      memo.push({
        headers: head.split(/\r\n/).reduce((memo, header) => {
          const [ key, val ] = header.split(/:\s+/)
          memo[key.toLowerCase()] = val
          return memo
        }, {}),
        body: body
      })
    }
    return memo
  }, [])
}

jcready avatar Jan 16 '17 15:01 jcready

@jcready hm, looks good. Can you try to PR and add some tests when you have time? We (the new maintainers) are here to help and review :)

tunnckoCore avatar Jan 16 '17 16:01 tunnckoCore

@jcready makes sense. But I wonder if this should really be part of formidable. If yes, it should use the streaming multipart parser that's used for multipart/form-data and avoid unbound memory allocation. If nobody has time for a good implementation and tests, I'd rather not see this supported.

felixge avatar Jan 17 '17 18:01 felixge