subscribe

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:

Web mentions