Unnatural API Calls and HTTP Follies

  • Adam DuVander
    Adam DuVander
    July 19 2021

There are some API rules you don’t break. For example: return 200-level status codes with successful requests. Breaking a basic rule makes it hard or even impossible for others to use your API. Yet, API designs remain remarkably flexible even when abiding the many conventions of REST, HTTP standards, and oodles of best practices. The unusual examples in this post are bound to raise an eyebrow—if not an error message.

While fun to read about, you may also find yourself tempted to shirk convention. The first example, an HTTP GET request with a message body, comes up frequently (just look at StackOverflow). There are API design patterns for a reason, but you may convince yourself yours is the counter use case. As you’ll see, it almost always makes sense to adopt the common practice instead—and there are tools that can help you and your team do the right thing.

We’ll look at a few API outliers and when it makes sense to break with convention like they do. Yes, you may find an opportunity to employ some of these API rarities, but you’ll find others are 406 Not Acceptable.

🔗 HTTP GET with Request Body

While several HTTP methods make use of request bodies, the most common HTTP method—the GET request—does not typically include any request data. Yet, the HTTP standard leaves the possibility to include a body, albeit with some confusing requirements.

A GET request’s parameters are usually sent through the path or query string. Think of a search request, where search terms, filters, sorting, and other information is all sent in the URL: /search?query=api+design&limit=10&sort=desc

This search call has everything the server needs to return results. Now, consider an alternate design, where the query parameters are sent in the request body instead of the path / query string. Here’s a snippet of the OpenAPI to describe such an API design:

   /search:
    get:
      summary: Retrieve search results
      description: Pass a search query in the request body
      operationId: get-search
      responses:
        '200':
          description: OK
      requestBody:
        description: This GET operation has a request body
        content:
          application/json:
            schema:
              type: object
              properties:
                query:
                  type: string

Rather than the query string method, the query is passed within the body as a JSON object, { "query": "api design" }. While you might find this baffling, it’s not hard to find developers arguing for this use case. Indeed, it’s technically possible based on the HTTP standard, though definitely discouraged. In fact, the OpenAPI 3.0 spec clearly states a GET request should not include a request body:

The requestBody is only supported in HTTP methods where the HTTP 1.1 specification RFC7231 has explicitly defined semantics for request bodies.

That RFC citation says that a “payload within a GET request message [body] has no defined semantics.” While there’s certainly room for someone to argue for sending a data object through a GET request, the bottom line is: it’s highly unusual, against conventions, and very likely to cause more issues than it will solve. If you’re considering an implementation, it’s best to switch up your HTTP method instead.

🔗 401 vs 403 vs … 404?

Let’s switch gears from HTTP request methods to what they return—responses, notably response codes (or status codes). The HTTP standard declares all of these, including 100, 200, 300, 400, and 500-level statuses. In this section, we’ll be looking at the status codes returned when there’s a client-side error. Specifically, 401 (Unauthorized), 403 (Forbidden), and the classic 404 (Not Found).

It’s common to mix up 401 and 403 error codes, which are both related to API security. As an API provider, you only want to provide access to users you have identified and who have permission to use your API (or particular endpoints). In fact, the difference between 401 and 403 comes down to these two requirements:

  • 401 is returned when a user cannot be identified. A repeat request could succeed if it includes a proper auth token or API key following your security scheme.
  • 403 is returned when the identified user cannot access a particular resource. The server identified the user, but denied the request. No adjustments to the request will change the outcome.

The fact that 401 returns the word “Unauthorized” probably adds to the confusion, since it sounds like it describes the 403 scenario. However, the distinction is important, and goes a long way toward being able to infer potential solutions based on the errors.

Recently, the API community debated the distinction between 401 and 403 statuses. A third 400-level option entered the discussion for a specific scenario involving inaccessible API resources:

Given that resource with id 123 actually exists in the underlying database, what should be the response to GET /resources/123 when consumer is not allowed to access it #apidesign #http #choosehttpstatuscode

The addition of 404 as a valid answer here opens a new security discussion: if an identified user does not have access to a specific resource, is it safe to let them know it exists? Returning the more accurate 403 status code provides a potential threat with additional knowledge. On the other hand, if they don’t have proper permissions, they won’t have access to see the 403 response code anyway.

As with many issues of API design, the context of your situation is required to make the right choice between 400-level responses for each case. However, one thing is certain: if you’ve identified the user, you should not use the 401 status code. Use 403 or 404 instead.

🔗 Custom Status Codes to “Enhance Your Calm”

You think it’s tough to distinguish between two or three status codes? What if you had to account for hundreds of potential responses? There are only 41 codes listed in the HTTP standard, but developers and projects have added others when existing codes didn’t quite fit. There’s nothing to stop you from selecting any three digit status code as long as it’s supported by your server or framework —other than: you’ll completely confuse consumers of your API.

Good usage of custom status codes in the wild are related to proxies, where standard responses might not convey the granularity or source of error. In other cases, search harder through the 41 existing choices before adopting a custom code. Whenever possible, search for the most accurate status code in the official HTTP standard before creating your own.

Twitter might have saved some of its early developers a headache if they had heeded this advice. In some of its first APIs, the microcontent pioneers returned a custom status code 420 Enhance Your Calm when too many requests were received from the same consumer. Some Twitter docs still list it, though it’s unlikely to be in current use.

Enhance Your Calm

There was already a much better status code available to Twitter when they created the API. Most APIs then, and the latest iterations of Twitter APIs now, use a standards-compliant 429 Too Many Requests status code to indicate the consumer is being rate limited. Coming from Twitter in the era of the Fail Whale, perhaps the 420 code was meant to bring levity.

Pot humor is at the heart of another custom status code, 418 I’m a teapot, part of a 1998 April Fool’s joke Hyper Text Coffee Pot Control Protocol (HTCPCP):

Any attempt to brew coffee with a teapot should result in the error code "418 I'm a teapot". The resulting entity body MAY be short and stout.

While the standard was never adopted, several languages and frameworks have adopted the status code.

Google Teapot

Google has implemented the 418 response in a single page Easter egg on its site.

Outside of silly examples, it makes much more sense to choose from existing status codes. In fact, the OpenAPI spec points to an HTTP standard that lists a finite group of official statuses. The introduction of new codes has a high bar for applicability and a specific approach to take:

Proposals for new status codes that are not yet widely deployed ought to avoid allocating a specific number for the code until there is clear consensus that it will be registered; instead, early drafts can use a notation such as “4NN”, or “3N0” … “3N9”, to indicate the class of the proposed status code(s) without consuming a number prematurely.

The answer here is even clearer than GET containing a request body: don’t create custom status codes.

🔗 Declare Your API Design Guidelines

Your APIs are unlikely to have a real use for unnatural requests and responses like the ones we’ve shown here (unless, perhaps, you’re building a connected teapot). Applying the most common convention is the natural choice for the majority of use cases. You can explicitly support or restrict your preferred conventions with a programmatic style guide. Stoplight’s open source linter Spectral has ready-made rulesets for common uses and can be extended to include custom checks of your OpenAPI design documents.

Peek-a-Boo (1)

Subscribe for the latest in API Design

By submitting this you will be receiving our latest updates on post.

Take a Listen to API Intersection

The podcast on the intersection between API design & digital transformation
Listen

Related Posts