WebDAV features that might be useful for HTTP services.
While WebDAV is no longer really used as the foundation for new HTTP services, the WebDAV standard introduced a number of features that are applicable to other types of HTTP services.
WebDAV comprises of many standards that each build on the HTTP protocol. Many of the features it adds to HTTP are not strictly tied to WebDAV and are useful in other contexts. And even though it ‘extends’ HTTP, it does so within the confines of the HTTP framework, so you can take advantage of them using standard HTTP clients and servers.
MOVE & COPY
WebDAV adds 2 HTTP methods for moving and copying resources. If you were to stick to typical HTTP/REST semantics, doing a move operation might imply you need several requests.
GET /source <-- retrieve a resource
PUT /destination <-- create the new resource
DELETE /source <-- remove the old one
One issue with this approach is that it’s not an atomic operation. In the middle of this process there is a short window where both the source and destination exist.
If an atomic move operation is required, a typical solution might be to create
a POST
request with a specific media-type for this, and this is a completely
valid solution.
A POST request like that might look like this:
POST /source HTTP/1.1
Content-Type: application/vnd.move+json
{
"destination": "/destination"
}
The WebDAV MOVE
request looks like this:
MOVE /source HTTP/1.1
Destination: /destination
Both the MOVE
and COPY
request use the Destination
header to tell the
server where to copy/move to. The server is supposed to perform this operation
as an atomicly, and it must either completely succeed or completely fail.
Using POST
for this is completely valid. However, in my mind using a HTTP
method with more specific semantic can be nice. This is not that different
from using PATCH
for partial updates. Anything that can be done with PATCH
,
could be done with POST
, yet people tend to like the more specific meaning
of a PATCH
method to further convey intent.
REPORT & SEARCH
Sometimes it’s required to do complex queries for information on a server. The
standard way for retrieving information is with a GET
request, but there’s
times where it’s infeasable to embed the entire query in the url.
There’s more than one way to solve this problem. Here’s a few common ones:
- You can use
POST
instead. This is by far the most common, and by many considered the most pragmatic. - Create a “report” resource, expose a separate “result” resource and fetch it
with
GET
. This is considered a better RESTful solution because you still get a way to reference the result by its address. - Supply a request body with your
GET
request. This is a really bad idea, and goes against many HTTP best practices, but some products like Elasticsearch do this.
If you are considering option #1 (POST
) you’re opting out of one of the most
useful features of HTTP/Rest services, which is the ability to address specific
resources.
However, you are giving up another feature of GET
. GET
is considered a
‘safe’ and ‘idempotent’ request, but POST
is neither.
If that last issue is something you care about, you might want to consider
using REPORT
or SEARCH
instead. The requests and response bodies could
identical if you were to use POST
, but these methods are both idempotent,
and safe.
Note that the rfc’s above focus on WebDAV semantics and XML requests and responses. However, I don’t believe there’s anything wrong with defining your own (json-based?) media-types and semantics.
The If header
HTTP has a bunch of headers for conditional requests. Specifically, these are:
If-Match
If-None-Match
If-Modified-Since
If-Unmodified-Since
If-Range
WebDAV defines another header for conditional requests that’s much more
powerful: If
. It can do the same as If-Match
and If-None-Match
, but
adds a number of features on top.
Here’s some examples of If
and their equivalent If-[None-]Match
headers:
If-Match: "foo-bar"
If-None-Match: "foo-bar"
If: (["foo-bar"])
If: (NOT ["foo-bar"])
This doesn’t seem much like a benefit, but If
can do more. For instance,
it’s possible to make a request conditional on the etag state of a second
resource.
The following PUT request only succeeds if the etag on /resourceB
equals
"foo-bar"
.
PUT /resourceA HTTP/1.1
If: </resourceB> (["foo-bar"])
A specific example where this is useful might actually be the earlier
mentioned COPY
and MOVE
operations. Because those methods affect
resources not specified in the request-URI, you might want to check the etag
of both the source and destinations to avoid conflicts.
COPY /source HTTP/1.1
Destination: /destination
If: </source> (["etag1"]) </destination> (["etag2"]).
Another possibility is to validate against 2 etags with “OR”.
PUT /foo
If: (["etag1"]) (["etag2"]).
Another possibility is to use AND
, although in this example this would
always fail:
PUT /foo
If: (["etag1"] ["etag2"]).
Conditions on custom flags
There’s another feature that’s very powerful. In every previous example we
always made our requests conditional on ETags. HTTP itself also allows
conditions based on the Last-Modified
header, but that’s really it. The If
header allows for conditions on your own custom flags & extensions.
The following is a purely fictional example, but imagine that we have a “user” resource, and that user has a flag indicating whether or not they paid for a service.
This flag could be expressed as a uri. For example,
https://example.org/flags/user-has-paid
. Note that this uri doesn’t actually
need to ‘work’. You could use a urn:uuid:
or even gopher://
if you wish.
The following PUT
request would only succeed if the user has paid:
PUT /foo
If: </user/234> (<https://example.org/flags/user-has-paid>).
This example is a bit contrived, but I tried to find a simple one. The underlying idea is that conditional requests can sometimes be useful, and ‘ETag’ is not always the best way to express certain states on the server.
Conclusion
I’m not sure yet it’s a good idea to use these features for HTTP or RESTish services, because it kind of breaks the principe of least surprise.
However, they provide useful semantics that the base specs don’t have a perfect answer for.
I’m not sure yet how to weight these trade-offs, but at least it’s interesting.
What didn’t make the list
- The MKREDIRECT method to create new redirects on the server.
- The LOCK and UNLOCK method to lock resources.
- BIND and UNBIND for implementing a ‘hard-link’ type of feature.