Search code examples
javascripttypescriptxmlhttprequestmultipart

How to properly parse multipart/byteranges responses using typescript/javascript?


I'm new to TypeScript/JavaScript and I'm trying to do multipart range requests (RFC 7233) for different parts of a binary file, using TypeScript and XMLHttpRequest 2.

According to RFC 7233, a multipart/byteranges response has the following:

 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 see two options:

  1. Treat the body of the response as binary data array (set XMLHttpRequest.responseType = "arraybuffer"), convert the boundary string into binary, search in the binary data array for each body part delimited by the boundary-string-converted-to-binary, extract the headers for each payload and converted them to string? or,
  2. Similar to above, but treat the body as a string (set XMLHttpRequest.responseType = "text"), identify the body parts delimited by the boundary-string, and convert the payloads to binary data arrays?

What is the proper way to handle/parse such responses in javascript/typescript, since the response contains multiple body parts, each with its own headers (string) and payload (binary)?

Is there a simpler way?

Any suggestions are welcomed. Thanks!


Solution

  • I'm not even sure how you'd do it using an "arraybuffer" response type, but I've used this before to parse multipart/byteranges responses:

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

    And for using it with an XMLHttpRequest it'd probably look something like this:

    const client = new XMLHttpRequest()
    client.open('GET', 'example.pdf', true)
    client.setRequestHeader('Range', 'bytes=500-999,7000-7999')
    client.onreadystatechange = function() {
      if (client.readyState == 4 && client.status === 206) {
        const boundary = client.getResponseHeader('Content-Type').match(/boundary=(.+)$/i)[1]
        if (boundary) console.log('PARTS', parseMultipartBody(client.resposeText, boundary))
      }
    }
    client.send()
    

    The output from your example response would look like this:

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