Wednesday, May 13, 2015

MIME Type is Important for REST

Today's comic makes fun of MIME (or Internet Media) types, but they can a very useful part of web applications that make use of REST.

MIME started out as a way to tell email clients what type of file you'd attached to a message, but have become much more useful. Every time your web browser makes a request for content, it sends along a header called "Accept" which is a list of data types it can process. Typically, this includes HTML, JavaScript, and images. The server, as part of its response, includes a header called "Content-Type" which specifies what is actually being returned. The two sides usually find some common ground.

When making REST web services, being a little more particular about your MIME types can open up new possibilities. REST stands for Representational State Transfer. When a human being is driving a web browser and asks for a resource (a.k.a. a web page or URL), they expect the server to transfer a representation of the current state of that resource. That representation is often HTML or an image because that's what humans can understand (well, assuming the browser turns the HTML into something pretty first). But the real power of REST web services is how it opens things up to the possibilities of machine-to-machine communication. Let's say a computer program requests the same resource as the human. The machine would have an easier time dealing with it if it could be in a different representation other than HTML, such as JSON. Two representations, one resource.

And that's where the MIME type comes in. When making a web service call, specifying exactly what you can deal with (your desired representation of that resource) lets the server know if it should give you HTML, an image, JSON, or something else. When serving requests, explicitly setting the content type of the response lets the client know if it should even try to process it.

To help visualize it, I've created a demo. The following Node/Express server has a single route, /pizza. Depending on what the client wants, it will return either a picture of a pizza, a JSON string describing a pizza, or an HTML snippet describing a pizza.

var express = require("express");
var fs = require("fs");
var app = express();

app.get("/pizza", function(req, res, next) {

    if (req.accepts("image/jpg") === "image/jpg") {

        fs.readFile("pizza.jpg", function(err, data) {

            if (err) {
                res.sendStatus(500).send(err);
            } else {
                res.setHeader("Content-Type", "image/jpg");
                res.send(data);
            }

        });

    } else if (req.accepts("application/json") === "application/json") {

        res.setHeader("Content-Type", "application/json");
        res.send({
            size: "large",
            toppings: ["pepperoni", "mushrooms"]
        });

    } else if (req.accepts("text/html") === "text/html") {

        res.setHeader("Content-Type", "text/html");
        res.send("<p>pizza with<ul><li>pepperoni</li><li>mushrooms</li></ul></p>");

    } else {
        res.sendStatus(406);
    }

});

//------------ static content
app.use(express.static("public"));

// ------------ start listening
var server = app.listen(3000, function() {
    console.log("listening on port %d", server.address().port);
});

To make this happen, the route has to check what the client can accept, which is sent as part of the "accept" header. Express gives us the method req.accepts() to check that header. Unfortunately, I found it a little awkward to use. You pass a string or an array of strings to req.accepts() and it returns either a string of the best match or undefined if there is no match. This is because browsers typically send a whole list of things as part of the accept header - after all, a browser can deal with HTML, plain text, JavaScript, and a variety of images to say the least. Express tries to help you by telling you what the browser prefers for each request if there is more than one option. That's why I did things like put the image check first (browsers accept image/* first when requesting the src of an img tag but also accept anything else!) and always check to see if the best choice exactly equals my test string.

Look at this jQuery client code:

$.ajax("/pizza", {
    headers: { Accept: "text/html" },
    success: function(data) {
        $("#d1").html(data);
    }
});

$.ajax("/pizza", {
    headers: { Accept: "application/json" },
    success: function(data) {

        var h = "<h3>got a " + data.size + " pizza with ";
        for (var i = 0; i < data.toppings.length; i++) {
            if (i > 0) h += " and ";
            h += data.toppings[i];
        }
        h += "</h3>";
        $("#d2").html(h);

    }
});

$.ajax("/pizza", {
    headers: { Accept: "application/xml" },
    success: function(data) {
        console.log("success!");
    },
    error: function() {
        console.log("error!");
    }
});

$("#d3").html("<img src=\"/pizza\" />");

Using jQuery to make Ajax calls to a single URL three different times, I specifically set the "accept" header to just one thing in each of the requests. The first request says it can accept only text/html, so the server gives me exactly that. The second request wants JSON, so the server responds accordingly and the JSON is turned into HTML on the client side. The third request wants XML, but the server doesn't support that and responds with a 406 - Not Acceptable. That response sounds backwards, since the server is telling the client that it doesn't have a way of giving it something that it can accept not that the server got something unacceptable from the client. Finally, an image tag is generated which specifies that same URL as the source and it does in fact get the JPG from the server.

Running that JavaScript code on this web page

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">
  <title>MIME Types</title>
</head>

<body>

  <div id="d1"></div>
  
  <div id="d2"></div>
  
  <div id="d3"></div>

</body>

<script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>

</html>

produces results that look like this (supply your own pizza image):



And that's how you can help usher in the future of machine-to-machine communications via the Internet. Make web services that serve machines as well as people. Build the Internet of Things. Now go get some REST.

Amphibian.com comic for 13 May 2015

No comments:

Post a Comment