39

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.

Seth Holladay
  • 8,951
  • 3
  • 34
  • 43
  • 1
    _"because media files cannot easily be stored in JSON"_ Have you tried serving the image as a `data URI` within `JSON` response? – guest271314 Nov 02 '17 at 04: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 02 '17 at 04: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. – Seth Holladay Nov 02 '17 at 04:12
  • You could serve the response as `multipart/form-data` and use `Response.formData()` – guest271314 Nov 02 '17 at 04: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](https://github.com/hapijs/pez). – Seth Holladay Nov 02 '17 at 04:45
  • If the metadata is _very_ small, could look at putting it in custom header(s), then keep the image as immediately displayable to the unaware consumers. – dsz Mar 07 '23 at 08:43

2 Answers2

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)
        }
      })
  })
guest271314
  • 1
  • 15
  • 104
  • 177
  • 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. – Seth Holladay Nov 02 '17 at 15:17
  • @SethHolladay See [How to upload files in Web Workers when FormData is not defined](https://stackoverflow.com/q/13870853/). 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 Nov 02 '17 at 16:24
  • @SethHolladay See also https://github.com/whatwg/fetch/issues/392, https://github.com/whatwg/html/issues/3040 – guest271314 Nov 02 '17 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](https://expressjs.com/)), 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. – Seth Holladay Nov 02 '17 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](https://stackoverflow.com/q/41281291/). 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 https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html, https://www.ietf.org/rfc/rfc2388.txt – guest271314 Nov 02 '17 at 16:47
  • @SethHolladay https://stackoverflow.com/questions/47080869/how-to-manually-create-multipart-form-data – guest271314 Nov 02 '17 at 17:05
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.
Evert
  • 93,428
  • 18
  • 118
  • 189
  • 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. – Seth Holladay Nov 02 '17 at 04: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. – Be Kind To New Users Apr 18 '20 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 '20 at 06: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. – Be Kind To New Users Apr 19 '20 at 13:18
  • @MichaelPotter nice! – Evert Apr 19 '20 at 17:06
  • "multipart gets annoying for clients if they just want to display the image" then they must post process the response, rather than directly chuck in the URI into an image tag. What's the issue? – Dragas Feb 10 '23 at 09:54
  • @Dragas can you elaborate on your question? It's not really clear. – Evert Feb 10 '23 at 16:58