Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add content_negociation extension #21

Merged
merged 10 commits into from Nov 8, 2017
Merged

Add content_negociation extension #21

merged 10 commits into from Nov 8, 2017

Conversation

davidbgk
Copy link
Contributor

@davidbgk davidbgk commented Nov 7, 2017

To reject unacceptable client requests based on the Accept header

To reject unacceptable client requests based on the `Accept` header
roll/__init__.py Outdated
if 'Content-Length' not in self.response.headers:
length = len(self.response.body)
self.response.headers['Content-Length'] = length
if 'Content-Type' not in self.response.headers:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this really serve a use case. Plus it costs a not in for each request (not a big deal but…).
I'd better let the user explicitly set it.
Or at least have a DEFAULT_CONTENT_TYPE property, so it can be changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I set a default.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a second thought, I really think this can be treacherous. IMHO Content-Type should be set explicitly (or using dedicated shortcuts like json).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any HTTP/1.1 message containing an entity-body SHOULD include a
Content-Type header field defining the media type of that body. If
and only if the media type is not given by a Content-Type field, the
recipient MAY attempt to guess the media type via inspection of its
content and/or the name extension(s) of the URI used to identify the
resource. If the media type remains unknown, the recipient SHOULD
treat it as type "application/octet-stream".

I really think as a web framework we should set a default, if it's easy to override both per view and for the whole app.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SHOULD

Means it's valid without ;)

But my point is that we are implicitly setting a value, and we have only one chance over X that this value is useful. So I think setting the correct value, per view, is the role of the user, not the one of the framework (which is blind on this topic).
If you really want to be educational, raise if the header is not found (but that would not be correct given the header is not mandatory as per the HTTP spec you quoted above) ;)

roll/__init__.py Outdated
if methods is None:
methods = ['GET']

def wrapper(func):
self.routes.add(path, **{m: func for m in methods})
handlers = {method: func for method in methods}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That way we are mixing methods and extra in the same space.
Maybe what we should do: add a has_route method to autoroute, so we can make the payload merge our-selves, and thus have a handlers key.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it may be the role of autoroutes to deal with that.

@app.listen('dispatch')
async def reject_unacceptable_requests(request, payload):
accept = request.headers.get('Accept', '*/*')
if get_best_match(accept, payload['accepts']) is None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if the return value of get_best_match should not be saved in some way?
I guess at some point the value of Accept header should be used to define which format to be used as response body?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other thought: should the route key be accepts, given at the end this is what the view can serve as content type, no?
So accepts seems to me the point of view of the request.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if the return value of get_best_match should not be saved in some way?

It's hard to guess without a use-case, I'd say let's wait for it to appear.

I guess at some point the value of Accept header should be used to define which format to be used as response body?

Not exactly, it can be a bit different so it's only for the check.

at the end this is what the view can serve as content type, no?

It can be a bit different, for instance the client asks for Accept: application/vnd.example.resource+json; version=2 and the server returns Content-Type: application/vnd.example.resource+json; version=2.1.3

protocol.on_message_begin()
protocol.on_url(path.encode())
protocol.request.body = body
protocol.request.method = method
protocol.request.headers = headers
return await self.app(protocol.request, protocol.response)
await self.app(protocol.request, protocol.response)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to extract this change to a separate commit. IMHO this should be merged right away.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that this urgent? #lazyme

requirements.txt Outdated
@@ -1,3 +1,4 @@
autoroutes==0.2.0
httptools==0.0.9
mimetype-match==1.0.4
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have a requirements_ext.txt, or should we just document the fact that this package needs be installed for the content_negotiation use?
We could easily check in the extension call that the module is installed, and raise if not.

@@ -240,3 +251,12 @@ Fired in case of error, can be at each request.
Use it to customize HTTP error formatting for instance.

Receives `request`, `response` and `error` parameters.


### dispatch
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option: attach the route payload to the request, and let the users use the request hook instead of adding a new one?

roll/__init__.py Outdated
if 'Content-Length' not in self.response.headers:
length = len(self.response.body)
self.response.headers['Content-Length'] = length
if 'Content-Type' not in self.response.headers:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a second thought, I really think this can be treacherous. IMHO Content-Type should be set explicitly (or using dedicated shortcuts like json).

roll/__init__.py Outdated
@@ -270,7 +275,7 @@ def __init__(self):
def factory(self):
return self.Protocol(self)

def route(self, path: str, methods: list=None, **extras):
def route(self, path: str, methods: list=None, **extras: dict):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can **extras be something else than a dict?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, I guess no, it's more as a matter of consistency in my experiment to annotate all parameters but it's quite useless… (for now!)

roll/__init__.py Outdated
params, handler = await self.dispatch(request)
if request.method not in request.routing:
raise HttpError(HTTPStatus.METHOD_NOT_ALLOWED)
request.kwargs.update(params)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may merge routing (the route payload) and kwargs (the route dynamic variables, if any) at this point. Maybe not in the same dict, in case of key conflicts?
Maybe a route object (perf arlert), with two properties payload and vars ?

roll/__init__.py Outdated
request.kwargs.update(params)
handler = request.routing[request.method]
await handler(request, response, **params)
request.kwargs.update(request.route.vars)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can drop request.kwargs at this point :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And renaming it to extras in slots for extra usage? 🙃

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah! :)
You really want to do request['user'] or request['session'] not request.extras['user']… ;)
I'll push something in that direction tomorrow :)

@davidbgk davidbgk merged commit 3270b80 into master Nov 8, 2017
@yohanboniface yohanboniface deleted the content-negociation branch November 20, 2017 15:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants