2

In an HTTP application, I think about routing requests based not only on the path, but also based on the request body. For an example, think about the following two different body schemas for a PUT request against an /orders/{id} endpoint:

A

{
    adminPassword: String,
    status: String,
}

B

{
    clientSecret: String,
    deliveryAdress: Object,
}

The requests should be routed to a different controller.

Of course, the business logic could be expressed by differentiating the two requests by assigning each an individual path. But I think there is some semantic benefit in having exactly one path for all updates of a entity, in different ways.

The idea is inspired by how Erlang and Elixir allow multiple function clauses with pattern-matching.

If I understand it correctly, it would be not native and probably hard, maybe even impossible, to reflect this API structure in OpenAPI. That in itself is a strong argument against this idea.

What other arguments speak against this approach? Has this been tried before?

4
  • you can do this with a software router like haproxy or nginx stackoverflow.com/questions/23259843/…
    – Ewan
    Commented Apr 29 at 8:37
  • 7
    "I think there is some semantic benefit in having exactly one path for all updates of a entity, in different ways" - almost everyone else thinks the exact opposite, which is why this is hard to represent in OpenAPI. Fairly easy to implement though, you just have a meta controller that forwards to one of the two implementations.
    – pjc50
    Commented Apr 29 at 9:42
  • 4
    This would be a lot more likely to use a PATCH rather than PUT. But I'm far from convinced this should use different controllers. It should probably a single controller which checks all data in the update and calls separate functions/methods/whatever to update the relevant data.
    – jcaron
    Commented Apr 29 at 10:12
  • Note also that routing usually occurs way before the body is read: the body could contain a large file upload, and you don't want the router to start reading all that into memory before finally deciding (possibly without even needing the body at all) where to route the request. And controllers may handle the body via very different APIs depending on the context (read the whole body at once in some cases, use streaming in others, and the latter may be get confused if the router has already read the body or part of it).
    – jcaron
    Commented Apr 29 at 10:26

5 Answers 5

27

What you're asking is essentially having the mailman deliver the packages differently based on what's inside the package. The mailman should not be looking in there.

Other information, such as the URL, query params and headers are intended for the figurative mailman to inspect and rely on to deliver the package to the correct location.

POST bodies are not meant to be inspected while still in transit. That's not to say it would be impossible to do so, but I'm not going to advocate for you doing so.

But I think there is some semantic benefit in having exactly one path for all updates of a entity, in different ways.

Here, we strike on the X of your XY problem. You're not looking for a way to route POST requests to different endpoints based on their body, you're looking for a way to have the same POST endpoint accept different bodies.

Like you said yourself, it makes semantic sense to have exactly one path. Somehow, you've not applied the same conclusion to having exactly one endpoint (which represents said path), which is where I think you've gone off the rails when looking for a solution.

Instead of trying to get the router to inspect the bodies and somehow figure out which body should go to which endpoint, have your (single) endpoint receive the JSON and figure out which type (of several options) to deserialize it into.

Any argument you make pro-one-route, I can argue is also a pro-one-endpoint argument. Any argument you make anti-one-endpoint, I can argue is also an anti-one-route argument.

It's the same logic in either case, but going with one endpoint means you're not needlessly involving the router in a way that it was never meant to be operated.

5

This answer is a frame challenge.

But I think there is some semantic benefit in having exactly one path for all updates of a entity, in different ways.

Perhaps you are not aware that what you are describing is a core design element of SOAP/WSDL based services which was a large part of the problems with the web services built upon them. When you do something like this, you are ignoring the features of HTTP designed to solve this problem, hence your struggle.

Routing requests using HTTP features designed for that purpose will make your goals much easier to achieve and your solution more easily understood and supported. Unless you can identify some specific benefit of this that cannot be achieved using HTTP features as intended, I don't see any good reason to do this. The one caveat would be if someone had decided to do this in the past and you were required to make a backwards compatible solution. But in this case, you would be the person potentially creating that problem for someone in the future, if I understand correctly.

One of the more serious issues with this is that you may make your solution significantly more difficult to secure. For example, if you used a separate path for your admin operations, firewall rules can easily be setup to restrict access to that endpoint from only approved IP addresses/ranges. If you use a single endpoint that solution doesn't work, and you will need something that can inspect the contents of the message and write custom rules. The risks of an error resulting in a vulnerability are much higher when you customize your security solutions.

7
  • 3
    @Flater's answer and your second paragraph are a bull's eye for this question. Commented Apr 29 at 15:31
  • Also GraphQL. Or basically any query language exposed over HTTP. This also includes Elasticsearch (the engine that powers this website), Jira's custom Jira Query Language etc.
    – slebetman
    Commented Apr 30 at 2:46
  • @sbebetman I'm not sure I follow. Are you suggesting that these query protocols 'direct' requests to different (internal) services based on the content of the document?
    – JimmyJames
    Commented Apr 30 at 14:03
  • @slebetman It seems that GraphQL does work in a SOAP way (I think this might be why I decided to mostly ignore it a while ago) but I'm not so sure about elasticsearch.
    – JimmyJames
    Commented Apr 30 at 14:14
  • The OP was merely wanting the request body to route the request to different models (tables, collections etc. whatever your db calls it's data set). And elasticsearch as well as JQL certainly fits the bill. With GraphQL it's even more extreme because GraphQL does not have a database at all, its 100% a proxying service with a query language that determines how to proxy the request to the appropriate data sources. Heck, I've even seen someone implement a GraphQL resolver to query the DOM in a browser.
    – slebetman
    Commented Apr 30 at 14:37
3

Essentially this is just method overloading over HTTP.

However, given the limitations of json you would have a problem distinguishing types from each other. The obvious solution would be to add a _type field somewhere that's easy to read.. like the path

1
  • Thanks for providing the name. That provides me with some more context to reason about this. Commented Apr 29 at 11:55
2

Client secrets and admin passwords refer to authentication and authorization. The HTTP protocol already represents this as challenge-response authentication and the authorization header. Now, this doesn't stop you from doing routing based on the request body. You can write code to parse the JSON payload and dispatch to a different controller, but this is outside the bounds of the HTTP standard.

To be honest, dispatching to different controllers means changes in application behavior. This is a matter of business logic, and business logic is not something that HTTP is concerned with.

As for OpenAPI? Client secrets and admin passwords become part of the description of the parameters passed to an API endpoint. OpenAPI doesn't describe the controllers in your web application. It describes the messages you can send to the system, so your proposed routing technique could fit, but probably would not be relevant to the API spec.

But I think there is some semantic benefit in having exactly one path for all updates of a entity, in different ways.

You can have one path and different ways of updating an entity. What a user is allowed to do is authorization. Based on what the user is authorized to do, the server side application might execute different logic. The conceptual resource identified by /orders/{id} shouldn't change based on the user, but what the user can change within that resource is not captured by HTTP. This is a separate concern specific to your application, and not generalized to how computers identify resources across a network.

From an HTTP client perspective, the controller that handles a request is entirely hidden, and irrelevant. This only becomes relevant to the maintainers of the server side application processing requests. Those people should have some knowledge of HTTP, and being that routing based on the request body is not typically supported by most web frameworks, it would violate the Principal of Least Astonishment. It may surprise people that routing works this way in your application, but the world wouldn't end.

Choosing a solution like this should be done only if other standard approaches won't work, and you clearly document why it doesn't work and how to use this feature of your routing code. Otherwise, if it works better than the alternatives, there is nothing wrong with doing this.

2
  • 2
    The clientSecret and adminPassword were meant as examples only. Your answer makes me believe it was a bad example. Commented Apr 29 at 6:01
  • @JonathanScholbach: so, change those things to different information, and my answer remains unchanged. You need to accept different data in the request. HTTP and the routing feature of most web frameworks just aren't built to handle this use case. That means you need to restructure the application to match how HTTP and web frameworks are meant to be used, or admit you've run into an edge case - document it and implement as needed. Commented Apr 29 at 15:30
0

What other arguments speak against this approach? Has this been tried before?

In your question, you are somewhat conflating message contents with the target of the endpoint, and give an example of two payloads. But if there are thousands? This is not a stretch, as a focused site as StackExchange is in the range of two hundred.

In both cases, the routing API will be in charge of first decoding payloads, then in running a recursive matching algorithm with no know bounds, to find the best object that best matches the payload, or throws an error, but only at the end of explosive, exhaustive search.

This screams DDoS. A very obvious, very inevitable DDoS target, in the very first steps of an API designed like this.

1
  • 1
    Thanks! Good point. I never meant to have only one endpoint instead of 200. I just thought about "method overloading", as Ewan calls it in their answer - so one endpoint for doing the same thing (semantically spoken), but with different inputs. Commented Apr 29 at 14:38

Not the answer you're looking for? Browse other questions tagged or ask your own question.