sanic-org / sanic

Accelerate your web app development | Build fast. Run fast.
https://sanic.dev
MIT License
18.1k stars 1.55k forks source link

Blueprint versioning in request headers for an API #1234

Closed ncrocfer closed 5 years ago

ncrocfer commented 6 years ago

Hello,

I am migrating an API from Flask to Sanic (perf are really improved !) and I would like to take advantage of it to add a versioning feature.

Sanic allows us to add the version directly in the blueprint, so for example it's possible to do the following :

v1 = Blueprint('v1', version=1)
v2 = Blueprint('v2', version=2)

@v1.route('/items')
async def get_items(request):
    return json({"foo": "bar"})

@v2.route('/items')
async def get_items(request):
    return json([{
            'title': 'foo',
            'description': 'bar'
        }])

It works perfectly using an URL prefix :

$ curl http://127.0.0.1:8000/v1/items
{"foo":"bar"}
$ curl http://127.0.0.1:8000/v2/items
[{"title":"foo","description":"bar"}]

But what is the solution if I want to select the version directly in the request headers please (like Github does it)?

From what I seen this feature is hardcoded, so do you think if there is a solution to select a Blueprint on the fly ? In a middleware for example ?

Thanks in advance !

arnulfojr commented 6 years ago

I have some concerns regarding the injection of the blueprint’s version in the request’s header or relying routing through headers. I’m a fan of micro-frameworks such as Sanic. And the idea of them is to be as small as possible. That being said, I will argue that X-Headers are basically unofficial HTTP headers, meaning that they’re not part of the HTTP protocol, so adding them to the request would mean that Sanic would have to inspect the URI and the request header, remember that HTTP is resource/document-based, hence the URI, in the HTTP layer you’re addressing a version of the resource, hence the routing through URI makes sense, but when referring to the version of the resource in the header is much like, you have a resource (non-versioned) that accepts versioning if requested. It’s a required vs optional. This can be also an impact to sanic’s performance.

If you really need to handle the logic through the headers, I can think that injecting headers can be useful sometimes when behind a LB or proxy, but in this case I think the routing of the version is happening outside the application layer and in the LB/proxy layer. Or having different schemas for validating the request/response of the document/resource addressed.

From my point of view, versioning through headers is handled by a non-versioned route and then the responsibility falls into the handler to chose logic. A way to ensure the header (as these kind of headers are optional) I would add a middleware to ensure the version and use the default before it lands to the handler, maybe a decorator can be an alternative... From there I would use schema version validation like for example cerberus or marshmallow, to convert the payload to a standardized input to the service layer.

http://marshmallow.readthedocs.io/en/latest/ http://docs.python-cerberus.org/en/stable/

Hope it helps!

ncrocfer commented 6 years ago

First of all thank you for your answer :)

I will argue that X-Headers are basically unofficial HTTP headers

You are right about the X-Headers but here we are talking about the application/vnd header, which is recognized by IANA : https://www.iana.org/assignments/media-types/media-types.xhtml

I don't think it's a bad practice, for example the Github website itself (on which we are talking right now ^^) uses it for its API versioning : https://developer.github.com/v3/media/#request-specific-version

From my opinion (and it's just mine), adding the version in the URL breaks the RESTFUL rules described by Roy Fielding :

Moreover clients already send their expected response format when they send a request Accept : application/json, text/xml. So sending the version of the API is also acceptable in the headers.

PS: I don't need LB or proxy to inject this header, the clients can directly do it when crafting their requests ;)

In fact I don't want to remove the ability to add the version in URL, but leave the Sanic users the choice to handle it (it's currently hardcoded : https://github.com/channelcat/sanic/blob/master/sanic/router.py#L124).

If you have an idea for doing it I'll take it, if not maybe a PR is the solution.

stale[bot] commented 5 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If this is incorrect, please respond with an update. Thank you for your contributions.