I’m almost done with this design. I know what data I’m serving. I know which HTTP requests clients will send when asking for the data. I know how the data will be represented as I serve it. I still have to consider the HTTP response itself. I know what my possible representations will look like, and that’s what’s going in the entity-body, but I haven’t yet considered the possible response codes or the HTTP headers I’ll send. I also need to think about possible error conditions: cases where my response signals an error instead of delivering a representation.
This step of the design is conceptually simple, but it’s the gateway to where you’re going to spend much of your implementation time: making sure that client requests are correctly turned into responses.
Most read-only resources have a pretty simple typical course of events. The user sends a GET request to a URI, and the server sends back a happy response code like 200 (“OK”), some HTTP headers, and a representation. A HEAD request works the same way, but the server omits the representation. The only main question is which HTTP headers the client should send in the request, and which ones the server should send in the response.
The HTTP response headers are a fairly large toolkit, and most
of them don’t apply to this simple service. (For descriptions of the
standard HTTP headers, see Appendix C.) In my
service, the main HTTP response header is
which tells the client the media type of the representation. My media
the map representations and search results, and
image/png for the map images. If you’ve done
any server-side web programming you already know about
Content-Type: every HTTP server and
framework uses it.
I don’t use HTTP request headers very often. I think it’s best if the client can tweak the representation by tweaking the URI to the resource, rather than tweaking the request headers. But there is one set of headers that I think ought to be built into every HTTP client, every web service, and every service hacker’s brain: the ones that make conditional GET possible.
I cover conditional GET in detail in Chapter 8, but the discussion there is somewhat detached from specific services. This discussion is tied to the map service, and covers just enough to get you thinking about conditional GET as you design your services.
Certain resources are likely to be very popular: “A road map of the United States,” “a satellite map of Earth,” or “restaurants in New York City.” A single client is likely to make a request for certain resources many times over its lifespan.
But this data is not constantly changing. Map data stays pretty constant over time. Satellite imagery is updated every few months at most. Restaurants come and go, but not on a minute-by-minute basis. Only a few resources are based on data that’s constantly changing. Most of the time, the client’s second and subsequent HTTP requests for a resource are wasted. They could have just reused the representation from their first request. But how are they supposed to know this?
This is where conditional GET comes in. Whenever a server
serves a representation, it should include a time value for the
Last-Modified HTTP header. This
is the last time the data underlying the representation was changed.
For “a road map of the United States,” the
Last-Modified is likely to be the time the
map imagery was first imported into the service. For “restaurants in
New York City,” the
may only be a few days old: whenever a restaurant was last added to
the database of places. For “container ships near San Francisco,”
the value of
Last-Modified may be
only a few minutes prior.
The client can store this value of
Last-Modified and use it later. Let’s say
the client requests “a road map of the United States” and gets a
response that says:
Last-Modified: Thu, 30 Nov 2006 20:00:51 GMT
The second time the client makes a GET request for that
resource, it can provide that time in the
GET /road/Earth HTTP/1.1 Host: maps.example.com If-Modified-Since: Thu, 30 Nov 2006 20:00:51 GMT
If the underlying data changed between the two requests, the server sends a response code of 200 (“OK”) and provides the new representation in the entity-body. That’s the same thing that happens during a normal HTTP request. But if the underlying data has not changed, the server sends a response code of 304 (“Not Modified”), and omits any entity-body. Then the client knows it’s okay to reuse its cached representation: the underlying data hasn’t changed since the first request.
There’s a little more to it than that (again, I cover this in more detail in Chapter 8). But you can see the advantages. A client that fetches detailed maps is going to be making lots of HTTP requests. If most of those HTTP requests give a status code of 304, the client will be able to reuse old images and place lists instead of downloading new ones. Everyone saves time and bandwidth.
I also need to plan for requests I can’t fulfill. When I hit an error condition I’ll send a response code in the 3xx, 4xx, or 5xx range, and I may provide supplementary data in HTTP headers. If they provide an entity-body, it’ll be a document describing an error condition, not a representation of the requested resource (which, after all, couldn’t be served).
I provide a full list of the HTTP response codes in Appendix B, along with examples where you might use each of them. Here are some likely error conditions for my map application:
The client may try to access a map that doesn’t exist, like
/road/Saturn. I understand what
the client is asking for, but I don’t have the data. The proper
response code in this situation is 404 (“Not Found”). I don’t need
to send an entity-body along with this response code, though it’s
helpful for debugging.
The client may use a place name that doesn’t exist in my database. The end user might have mistyped the name, or used a name the application doesn’t recognize. They may have described the place instead of naming it, they might have the right name but the wrong planet. Or they might just be constructing URIs with random strings in them.
I can return a 404 response code, as in the previous example, or I
can try to be helpful. If I can’t exactly match a requested place
I might run it through my search engine and see if it comes up
with a good match. If I do get a match, I can offer a redirect to
that place: say,
The response code for the helpful case here would be
303 (“See Other”), and the HTTP response header
Location would contain the URI
of the resource I think the client was “really” trying to request.
It’s the client’s responsibility to take the hint and request that
URI, or not.
If I try a search and still have no idea what place the client is talking about, I’ll return a response code of 404 (“Not Found”).
The client may use logically impossible latitudes or
degrees north latitude, 181 degrees west longitude). A 404 (“Not
Found”) is a good response here, just as it is for a place that
doesn’t exist. But a 400 (“Bad Request”) would be more precise.
What’s the difference between the two cases? Well, there’s nothing obviously wrong with a request for a nonexistent place name like “Tanhoidfog.” It just doesn’t exist right now. Someone could name a town or a business “Tanhoidfog” and then it would be a valid place name. The client doesn’t know there’s no such place: one of the nice things a client can do with my map service is check to see which places really exist.
But there is something wrong with a request for the
500,-181. The laws of geometry prevent
such a place from ever existing. A minimally knowledgeable client
could have figured that out before making the request. A 400
response code is appropriate in that case: the problem is the
client’s fault for even making the request.
A search for places on a map might return no search results. There might be no racing speedways near Sebastopol, CA. This is disappointing, but it’s not an error. I can treat this like any other search: send a 200 response code (“OK”) and a representation. The representation would include a link to the place that was searched, along with an empty list of search results.
The server may not be functioning correctly. This might be due to missing or corrupted data, a software bug, a hardware failure, or any of the other things that can go wrong with a computer program. In this case the response code is 500 (“Internal Server Error”).
This a frustrating response code (the whole 5xx series is frustrating, actually) because there’s nothing the client can do about it. Many web application frameworks automatically send this error code when an exception happens on the server side.