Discovering features and information via HTTP OPTIONS
Say you have an API, and you want to communicate what sort of things a user can do on a specific endpoint. You can use external description formats like OpenAPI or JSON Schema, but sometimes itβs nice to also dynamically communicate this on the API itself.
`OPTIONS` is the method used for that. You may know this HTTP method from CORS, but itβs general purpose is for clients to passively find out βWhat can I do here?β.
All HTTP clients typically support making `OPTIONS` request. For example with `fetch()`:
const response = await fetch( 'https://example.org', {method: 'OPTIONS'} );
A basic `OPTIONS` response might might look like this:
HTTP/1.1 204 No Content Date: Mon, 23 Sep 2024 02:57:38 GMT Server: KKachel/1.2 Allow: GET, PUT, POST, DELETE, OPTIONS
Based on the `Allow` header you can quickly tell which HTTP methods are available at a given endpoint. Many web frameworks emit this automatically and generate the list of methods dynamically per route, so chances are that you get this one for free.
To find out if your server does, try running the command below (with your URL!):
curl -X OPTIONS http://localhost:3000/some/endpoint/
One nice thing you could do with the `Allow` header, is that you could also communicate access-control information on a very basic level. For example, you could only include `DELETE` and `PUT` if a user has write access to a resource.
## Accept and Accept-Encoding
Thereβs server other standard headers for discovery. Hereβs an example showing a few at once:
HTTP/1.1 204 No Content Date: Mon, 23 Sep 2024 02:57:38 GMT Server: KKachel/1.2 Allow: GET, PUT, POST, DELETE, OPTIONS Accept: application/vnd.my-company-api+json, application/json, text/html Accept-Encoding: gzip,brotli,identity
You may already be familiar with `Accept` and `Accept-Encoding` from HTTP requests, but they can also appear in responses. `Accept` in a response lets you tell the client which kind of mimetypes are available at an endpoint. I like adding `text/html` to every JSON api endpoint and making sure that API urls can be opened in browsers and shared between devs for easy debugging.
The `Accept-Encoding` lets a client know in this case that they can compress their request bodies with either `gzip` or `brotli` (`identity` means no compression).
## Patching, posting and querying
3 other headers that can be used are `Accept-Patch`, `Accept-Post` and `Accept-Query`. These three headers are used to tell a client what content-types are available for the `PATCH`, `POST` and `QUERY` http methods respectively.
For all of these headers, their values effectively dictate what valid values are for the `Content-Type` header when making the request.
HTTP/1.1 204 No Content Date: Mon, 23 Sep 2024 02:57:38 GMT Server: KKachel/1.2 Allow: OPTIONS, QUERY, POST, PATCH Accept-Patch: application/json-patch+json, application/merge-patch+json Accept-Query: application/graphql Accept-Post: multipart/form-data, application/vnd.custom.rpc+json
In the above response, the server indicates it supports both JSON Patch and JSON Merge Patch content-types in `PATCH` requests. It also suggests that GraphQL can be used via the `QUERY` method, and for `POST` it supports both standard file uploads and some custom JSON-based format.
Typically you wouldnβt find all of these at the same endpoint, but I wanted to show a few examples together.
## Whereβs PUT?
Oddly, thereβs no specific header for `PUT` requests. Arguably you could say that `GET` and `PUT` are symmetrical, so perhaps the `Accept` header kind of extends to both. But the spec is not clear on this.
I think the actual reality is that `Accept-Patch` was the first header in this category that really clearly defined this as a means of feature discovery on `OPTIONS`. `Accept-Post` and `Accept-Query` followed suit. I think `Accept-Patch` in `OPTIONS` was modelled after in-the-wild usage of `Accept` in `OPTIONS`, even though the HTTP specific doesnβt super clearly define this.
If Iβm wrong with my interpretation here, I would love to know!
_Aside: If youβre wondering about`DELETE`, `DELETE` should never have a body, so all a user would need to know is _can_ they delete, which you can see in the `Allow` header. If this is new to you to, read my other article about `GET` request bodies. Most of the information there is applicable to `DELETE` as well. _
## Linking to documentation
The `OPTIONS` response is also a great place to tell users where to find additional documentation. In the below example, I included both a machine-readable link to a documentation site, a link to an OpenAPI definition, and a message intended for humans in the response body:
HTTP/1.1 200 OK Date: Mon, 23 Sep 2024 04:45:38 GMT Allow: GET, QUERY, OPTIONS Link: <https://docs.example.org/api/some-endpoint>; rel="service-doc" Link: <https://api.example.org/openapi.yml>; rel="service-desc" type="application/openapi+yaml" Content-Type: text/plain Hey there! Thanks for checking out this API. You can find the docs for this specific endpoint at: https://docs.example.org/api/some-endpoint Cheers, The dev team
I recommend keeping the response body as mostly informal and minimal any real information should probably just live on its own URL and be linked to.
I used the `service-doc` and `service-desc` link relationships here, but you can of course use any of the IANA link relationship types here or a custom one. Also see the Web linking spec for more info.
## Obscure uses
### WebDAV usage
WebDAV, CalDAV and CardDAV also use OPTIONS for feature discovery. For example:
HTTP/1.1 204 No Content Date: Mon, 23 Sep 2024 05:01:50 GMT Allow: GET, PROPFIND, ACL, PROPPATCH, MKCOL, LOCK, UNLOCK DAV: 1, 2, 3, access-control, addressbook, calendar-access
### The server-wide asterisk request
Normally HTTP requests are made to a path on the server, and the first line looks a bit like the following in HTTP/1.1:
GET /path HTTP/1.1
But, there are a few other βrequest lineβ formats that are rarely used. One of them lets you discover features available on an entire server, using the asterisk:
OPTIONS * HTTP/1.1
The asterisk here is not a path. Normally asterisks arenβt even allowed in URIs. Many HTTP clients (including `fetch()`) donβt even support this request.
Classic webservers like Apache and Nginx should support this. To try it out, use CURL
curl -vX OPTIONS --request-target '*' http://example.org
## Final notes
If you have a reason to allow clients to discover features on an endpoint, consider using `OPTIONS` instead of a proprietary approach! As you can see in many of these examples, itβs especially useful if you use mimetypes well.
If you have questions, other novel uses of `OPTIONS` or other ideas around feature discovery, you can respond via:
* This post on Mastodon
* This post on Bluesky
* Via the Webmention protocol!