Discovering features using 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!