-
-
Notifications
You must be signed in to change notification settings - Fork 111
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
Resolve Content-Type from custom and common MIME types via Accept header #179
Resolve Content-Type from custom and common MIME types via Accept header #179
Conversation
This is meant to clarify the intent and overall create a more focused and useful method. Realistically, this is a private API for the class, and it's only used in one place - but nonetheless the intent and purpose should be clear. This is now (clearly) responsible for looking at the current Accept header value (default or not) and identifying (with weight compensation) if the value is a known (non-custom) MIME type. If it is, the type is returned, otherwise nil is returned.
This is really meant to be another one of those "clearly demonstrated intent" methods. When we're looking to the Accept header for a potential Content-Type value, we need to first check for normal header types, then consider custom types. This method is exclusively meant for the latter.
This method is simply replacing the logic that used to exist in the methods that look to the header for a certain MIME type (either common or custom). Now, instead of checking for the header to exist, then looking at type, they are simpler and only look at type. The check for the Accept headers presence is now taken care of external to those methods (since they shouldn't be aware of that state anyway).
The method was not taking into account when we have a custom MIME type registered for a potential controller format - instead it was overlooking these values and simply skipping to the default content type. Now, we honor the ordered preference of choosing which content type to use: 1. Explicit set value (from #format=) 2. Weighted value from Accept header (common MIME types) 3. Value from Accept header (custom registered MIME types) 4. Configured default content type 5. Hard-coded default content type
2 similar comments
I would add more coverage, but there isn't really any isolated/unit tests as is - I'm not sure where they'd be incorporated. Also, the integration tests I've added are more than enough for the scenario - I suppose I could add more? Not sure what the requirements are, please advise. |
First off let me say this awesome. I see this being extremely valuable. |
@@ -207,7 +211,14 @@ def format | |||
# end | |||
# end | |||
def content_type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking about this purely conceptually the Content-Type
and response body format should be set based on a couple things according to the Accepts
header section in the HTTP RFC.
Scenarios
- If the service supports one of the mime types specified in
Accepts
header it should respond with the most preferred based on the q scoring that also happens to be supported. See RFC 2616 Section 14.1 for details. - If the client specifies one or more mime types and the service does not support any of them then the service should respond with a 406 status code.
- If the client does not specify any accept mime types then the service should default to its ideal mime type.
If I remember correctly the 406 status code is already handled for scenario 2 above, or at least partially handled. Maybe we should revisit that to make sure it is completely handled.
I am a little confused by the priority order of get_common_mime_type_from_accept_header
and get_custom_mime_type_from_accept_header
.
It seems in my mind based on the RFC if an accept header is provided with one or more acceptable mime types you have two possible scenarios.
- the service doesn't support any of them and a 406 status code is responded. This already happens.
- the service has been identified as supporting one or more of the mime types specified in the
Accepts
header and therefore needs to identified the out of the supported ones which is the most preferred based on the q score stuff and set the content type type to that.
I am not sure why we need get_common_mime_type_from_accept_header
used at all when we know an accept header has been specified so we have to be in one of the above two states. Am I missing something?
It also seems instead of the get_custom_mime_type_from_accept_header
, or maybe not instead, but maybe the inside of that method needs to be different. It seems it would need to be something like best_q_match(accept, get_acceptable_mime_types(accept))
. That way you would be first getting the matching mime types specified by the client in the Accept header and then taking that resulting list and picking the most preferred one using the q score matching.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great feedback @cyphactor, I'm going to make some modifications based on this today.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the changes you made. It looks like it should be covering the RFC now.
In my earlier commits the preference for Content-Type resolution was focused on the types that are common (from Rack), THEN on the types that were registered through the controller configuration. Now we are going to look at both of those with the `q` weighting that is part of the RFC (and currently part of how Hanami Controller works).
We need to be able to know what MIME types have been registered with a custom controller configuration, so we can appropriately get the correct Content-Type to respond with (given an Accept header). There was no accessor method other than the protected `#formats` which only returns a hash including both MIME type and format key (we only need the former).
@cyphactor I've made some adjustments, and added a few more tests to prove the changes work. Can you please take another look? |
@configuration.format custom: 'custom/format' | ||
end | ||
|
||
it 'returns all known format MIME types' do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This description should probably read "return all configured format MIME types".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good thought, I've added this change in 9bc4e2e
Technically we aren't dealing with all known types, it's only the ones known to the controller configuration, so this makes a lot more sense.
1 similar comment
Everything looks good to me at this point. I give it a 👍 . |
# | ||
# @api private | ||
def get_mime_type_from_accept_header | ||
all_types = (MIME_TYPES + configuration.format_mime_types).uniq |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if it's worth it, but rather than running uniq
on a new Array, would it be worth trying out Set
(probably SortedSet
in this case) and seeing how that performs in comparison?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did a comparison with a few different ways to make a list unique, though there's a difference it's relatively minor.
Running benchmark with array1(10000 elements, 3675 dupes) and array2(10000 elements, 3642 dupes)
user system total real
Add Arrays 0.000000 0.000000 0.000000 (0.006018)
New Set (merge) 0.010000 0.000000 0.010000 (0.004714)
New Set with add 0.000000 0.000000 0.000000 (0.005160)
Hash, keys only 0.010000 0.000000 0.010000 (0.004712)
With that said, I don't mind switching if it's a real concern.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cllns what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The difference looks pretty insignificant, especially considering that there will usually be around 10 elements max in the Accept
header, never 1000.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My bad I spoke too fast - there are indeed around 500 mime types registered in rack. The difference is still pretty small.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@beauby I agree, especially considering the impact of making the code less intuitive (Set with merged values vs a simple Array).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My vote is for simple understandable code in this case rather than a small performance increase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One other thing to keep in mind is that the configuration.format_mime_types
may typically contain very few values, in which case it is probably less performant to ensure uniqueness than to simply concatenate the arrays (with duplicates) and search in that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also (not related to this PR), is there a size limit on the Accept
header at some level, or can we simply make the CPU spin by supplying a very long list of random values?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for looking into this. benchmark-ips
is a better way of comparing, IMO since it digests the data for you.
I'm generally in favor of understandability over minor performance gains, but this code would affect every request (as I understand it), where even small gains add up.
I'll let @jodosha or @joneslee85 determine whether they think this is OK :)
return @content_type if @content_type | ||
|
||
if accept_header? | ||
type = get_mime_type_from_accept_header |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The get
in the name seems superfluous and not in line with the rest of this file. How about mime_type_from_accept_header
instead?
It's honestly going to rarely be the case when there are many custom MIME types specified in the Accept header - so it is probably best to just always combine and search through them - since it's not really an issue if there are duplicates anyway.
2 similar comments
# @since 0.1.0 | ||
# Checks if there is an Accept header for the current request. | ||
# | ||
# @return [Boolean] true if there's an Accept header to look at |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpicking:
tTrue ifthere'sthere is an Accept headerto look atin the current request.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough, I don't mind making these changes - just a note though, I did it this way based on other documentation in the same file (https://github.com/Acornsgrow/controller/blame/resolve_content_type_from_custom_formats/lib/hanami/action/mime.rb#L409).
Should I clean up all of the documentation in the file to match?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's worth the effort but let's see what the maintainers think.
# or the custom registered ones (see Hanami::Controller::Configuration#format). | ||
# | ||
# @return [Nil] when the Accept header does not match any known MIME | ||
# Types. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpicking
wWhen the Accept header does not match any known MIMETypestype.
# | ||
# @return [Nil] when the Accept header does not match any known MIME | ||
# Types. | ||
# @return [String] the matched MIME type for the given Accept header. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpicking
tThe matched MIME type for the given Accept header.
end | ||
|
||
it 'sets "Content-Type" header according to "Accept"' do | ||
response = @app.get('/custom_from_accept', 'HTTP_ACCEPT' => 'application/custom;q=0.9, application/json;q=0.5') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've noticed that there does not seem to be any enforced constraint on line size in Hanami but it does improve readability when lines do not exceed 80 columns (or 90, or 100 – 80 just seems to be the inherited standard from the physical terminal days).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was actually wondering about this, since it seemed like the lines generally are shorter, but definitely not always.
Based on what you said, it seems like the standard is to just make sure the lines are as readable as possible... I can run through all my changes and see where they can be shortened to make it better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it makes more sense to have one big separate rubocop cleanup PR that does not change behavior.
@beauby thank you again for the review - I've made all of the changes you suggested. Would you please take another look and see if there's anything else? |
2 similar comments
3 similar comments
As far as I'm concerned it looks good 👍 |
@cllns There have been some changes added since you last looked - would you mind giving it a once-over? |
Seems fine to me, but I can't merge on this repo 😃 @jodosha is busy with |
@cllns and @joneslee85 - any chance we could move forward with this PR? I'm going to be using it either way, and I prefer that my company does not need to use only our fork of Thanks in advance! p.s. Please let me know if there's anything I can do to move this forward. |
Deferring to @jodosha :) |
@jodosha or @joneslee85: Is there no way to get some motion on this PR? 😢 |
Well, bummer. I'm going to close this PR since I can't get any momentum toward merging or changing it. It's a shame, I do wish there were more active core members that could review it. In any case, the branch will persist, and it can be looked up if needed. For now we'll just switch our company to our own fork and use that moving forward. Thanks to all who helped improve this code! |
IMO this should be merged (whether in its current form or not is up to the maintainers, but the underlying issue is a bug). |
@russCloak I understand you're frustrated at the lack of attention, but the core team members are all volunteers working in their spare time. I expect this will be merged and you can get back over to using regular ole' I do recommend, if you want this PR to get merged, that you should leave it open. I imagine it'll get overlooked if it's closed. 🌸 |
@cllns Fair enough - I'll reopen. I just figured a stale PR is just as bad as a closed one. Ideally it will get in eventually, and we'll head back over to this remote - thanks for the reply! |
@russCloak I'm sorry if we're merging only today. All of you @russCloak, @cyphactor, @beauby, and @cllns and it would be a shame that good collaboration like this would be lost. 💯 Please understand that we all do voluntary work and other stuff can take higher priority. For instance other parts of Hanami, our day jobs, our families.. It takes time the right to digest a month of discussions, to check the code, tweak it if needed. That's the reasons for the waiting. 🌸 That being said, thank you for this contribution. This is a great fix. 💚 |
The
#content_type
method was not taking into account when we have a custom MIME type registered for a potential controller format - instead it was overlooking these values and simply skipping to the default content type.Now, we honor the ordered preference of choosing which content type to use:
Examples:
Given the following controller:
Before this PR
When a request was made with
Accept: application/vnd.custom.v2+json
it would respond withContent-Type: application/json
.With this PR
When a request is made with
Accept: application/vnd.custom.v2+json
, it responds withContent-Type: application/vnd.custom.v2+json
. Likewise,v1
returnsv1
, andapplication/json
returnsapplication/json
.Weighting also works now for custom types:
Before this PR
When a request was made with
Accept: application/json; q=0.5, application/vnd.custom.v1+json; q=0.9
, it would respond withContent-Type: application/json
.With this PR
When a request is made with
Accept: application/json; q=0.5, application/vnd.custom.v1+json; q=0.9
, it responds withContent-Type: application/vnd.custom.v1+json
. Likewise, a request made withAccept: application/json; q=0.8, application/vnd.custom.v1+json; q=0.4
responds withContent-Type: application/json
.