41

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.

6
  • 1
    "because media files cannot easily be stored in JSON" Have you tried serving the image as a data URI within JSON response? Nov 2, 2017 at 4:02
  • 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.
    – Rex
    Nov 2, 2017 at 4:09
  • 1
    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. Nov 2, 2017 at 4:12
  • You could serve the response as multipart/form-data and use Response.formData() Nov 2, 2017 at 4:21
  • 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. Nov 2, 2017 at 4:45

2 Answers 2

15

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)
        }
      })
  })

6
  • 1
    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. Nov 2, 2017 at 15:17
  • @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. Nov 2, 2017 at 16:24
  • @SethHolladay See also github.com/whatwg/fetch/issues/392, github.com/whatwg/html/issues/3040 Nov 2, 2017 at 16:36
  • 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. Nov 2, 2017 at 16:44
  • @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 Nov 2, 2017 at 16:47
4

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.
7
  • 3
    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. Nov 2, 2017 at 4:47
  • 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. Apr 18, 2020 at 23:35
  • @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...
    – Evert
    Apr 19, 2020 at 6:50
  • @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. Apr 19, 2020 at 13:18
  • @MichaelPotter nice!
    – Evert
    Apr 19, 2020 at 17:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.