Multipart HTTP response

23,752

Solution 1

You can serve the response as multipart/form-data and use Response.formData() to read response at client

fetch("/path/to/server", {method:"POST", body:formData})
.then(response => response.formData())
.then(fd => {
  for (let [key, prop] of fd) {
    console.log(key, prop)
  }
})

let fd = new FormData();
fd.append("json", JSON.stringify({
  file: "image"
}));
fetch("data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH+GkNyZWF0ZWQgd2l0aCBhamF4bG9hZC5pbmZvACH5BAAKAAAAIf8LTkVUU0NBUEUyLjADAQAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQACgABACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkEAAoAAgAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkEAAoAAwAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkEAAoABAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQACgAFACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQACgAGACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAAKAAcALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==")
  .then(response => response.blob())
  .then(blob => {
    fd.append("file", blob);
    new Response(fd)
      .formData()
      .then(formData => {
        for (let [key, data] of formData) {
          console.log(key, data)
        }
      })
  })

Solution 2

If you are going for a multipart format, I don't think there's anything inherently wrong with using the exact same format both during upload (POST/PUT) and retrieval (GET).

I think there's definitely an elegance in using the same on-wire format in both directions when working with HTTP.

However, if you want to send form-data during PUT/POST and JSON back using GET, then I would start questioning whether this is the right thing to do.

multipart gets annoying for clients if they just want to display the image. Have you considered just using different endpoints; one for the image and one for it's meta-data? What reason do you have to want to combine them into a single resource?

Alternatively, you could also attempt to embed the information in the image. JPEG for instance allows custom data to be added using EXIF. At least you preserve the ability to just open the image directly.

However, I will conclude with saying that multipart/mixed is appropriate if you just want to embed an image + a json object, but bear in mind:

  1. It's probably a bit inconvenient for consumption
  2. It's also a bit unusual
  3. I'm fairly sure that multipart encoding will require you to encode your image in some 7bit encoding, which will inherently cause the request size to blow up quite a bit.
Share:
23,752

Related videos on Youtube

Seth Holladay
Author by

Seth Holladay

On a mission to make software simpler and more reliable. Most of my work is open source. In my down time, you can find me in the woods, intentionally lost. If it's winter, I'm probably drinking hot cocoa and playing Age of Empires.

Updated on July 09, 2022

Comments

  • Seth Holladay
    Seth Holladay almost 2 years

    The goal is for a Node.js / hapi API server to respond to a browser's AJAX request with two things:

    • A media file (e.g. an image)
    • A JSON object with metadata about the file

    These are two separate items only because binary data cannot easily be stored in JSON. Otherwise, this would be a single resource. Nevertheless, it is preferable that they be sent in a single response.

    We upload these in a single request with multipart/form-data. In that case, browsers provide a built-in mechanism to serialize the body and most server-side frameworks know how to parse it. But how does one do the same for a response, in the opposite direction? Namely, how should a server serialize the body to transmit it to a client?

    From what I can tell, multipart/mixed could be a useful content type. But there is very little talk of this. Most people seem to resort to providing two separate GET routes, one for each piece. I dislike that because it opens you up to race conditions, amongst other things. What am I missing?

    See also my question in hapijs/discuss#563.

    • guest271314
      guest271314 over 6 years
      "because media files cannot easily be stored in JSON" Have you tried serving the image as a data URI within JSON response?
    • Rex
      Rex over 6 years
      Would converting the image to a base64 and the json to a base64 then concatenating them to a string with a . delimiter work for you? You can send it as a string and then decode it on the front.
    • Seth Holladay
      Seth Holladay over 6 years
      That is partly what I meant by easily. I could base64 encode the media file, but not only does that add additional processing, it also bloats the file size by ~33%. I guess I'm just surprised that this is a cleanly solved problem in one direction and less so in the other.
    • guest271314
      guest271314 over 6 years
      You could serve the response as multipart/form-data and use Response.formData()
    • Seth Holladay
      Seth Holladay over 6 years
      Cool, I didn't know about response.formData(). That will be useful here. Now I have to figure out how to construct the response on the server. I basically need the inverse of pez.
  • Seth Holladay
    Seth Holladay over 6 years
    Sending a form back in a response feels strange. But eh, whatever, right? :) Know of any good libraries for constructing the response? To my knowledge, neither hapi nor any of the other frameworks know how to serialize a form, only how to parse it.
  • Seth Holladay
    Seth Holladay over 6 years
    Excellent example, thank you! That should work nicely for the client-side part. Do you know how to create the response on the server? I'll also need that in order to use this.
  • guest271314
    guest271314 over 6 years
    @SethHolladay See How to upload files in Web Workers when FormData is not defined. You should be able to use Response.formData() within a ServiceWorker as well to create multipart/form-data from arbitrary data and respond with a new Response() with the created FormData as argument, if you do not want to create the multipart/form-data by hand.
  • guest271314
    guest271314 over 6 years
  • Seth Holladay
    Seth Holladay over 6 years
    I could be mistaken, but I don't think service workers will be involved here. I need to transmit the file and metadata from a route handler in a Node.js server framework (similar to Express), that is the part I'm missing. Then I need to receive the response on the front end and parse it, which I should be able to do with your current example.
  • guest271314
    guest271314 over 6 years
    @SethHolladay Have no experience using Express or a hapi server. The ServiceWorker could be used as an intermediary to serve the response to client see Chrome extension: Block page items before access. A Question could be posed specifically as to how to create multipart/form-data manually, which should provide the possible solutions to the inquiry. The relevant specifications w3.org/Protocols/rfc1341/7_2_Multipart.html, ietf.org/rfc/rfc2388.txt
  • guest271314
    guest271314 over 6 years
  • Be Kind To New Users
    Be Kind To New Users about 4 years
    I am looking to do this so I can return a .pdf and json that contains the location of the signature boxes and other controls needed for subsequent e-signing. I would want to return those at the same time because it takes time to build the .pdf to know where to place the signature. I could do two end points: pdf and pdf&json.
  • Namrata Das
    Namrata Das about 4 years
    @MichaelPotter I guess it's been a while since this question & answer, but I would still kinda question if you can't just do 2 requests...
  • Be Kind To New Users
    Be Kind To New Users about 4 years
    @Evert I appreciate a skeptic. I am generating a pdf that can take quite some time to generate. While generating the .pdf I am also generating information that will be passed to an e-signing solution telling it where to sign and other information. Because your persistent sketicism I came up with the idea of making the call twice where the first step generates the pdf and the json. The first step will cache the json so it can be returned on the second call.
  • Namrata Das
    Namrata Das about 4 years
    @MichaelPotter nice!