mojolicious / mojo

:sparkles: Mojolicious - Perl real-time web framework
https://mojolicious.org
Artistic License 2.0
2.66k stars 577 forks source link

Bug in content negotiation when specifying a default format and including it in a call to respond_to() helper #2186

Closed vmmello closed 3 months ago

vmmello commented 3 months ago

I'm trying to change the default response type of an endpoint to text/plain, so that when a user runs curl / and the app receives the header Accept: */* it will reply with a txt, not html.

A simplified sample code of how I'm trying it:

get '/'  => { format => "txt" } => sub($c) {
              $c->respond_to(
                html => { },
                txt   => { },
                json  => { },
                any  => { format => "txt" },
              );
        }
        => "index";

__DATA__

@@ index.html.ep
<html></html>

@@ index.txt.ep
text

@@ index.json.ep
{"json":""}

The above looks right to me. But it completely breaks content negotiation, replying all requests with the default format. For example, when running:

$ ./app.pl get -H 'Accept: text/html' /

text

$ ./app.pl get -H 'Accept: application/json' /

text

But if I remove the txt => {} line (line 4) from respond_to(), letting text/plain be catched by the any format, it works as expected (i.e. content negotiation through Accept header works, and it correctly fallsback unknowns to txt format). If I omit the any format it replies with 204 No Content for txt and other formats (other than the ones listed in respond_to()).

So I believe this is a subtle bug in content negotiation, the fact that when you explicitly specify a default format and include it in a respond_to() method call, it breaks content negotiation through the Accept header.

This same problem happens when the default format is explicitly specified as html, e.g.: get '/' => { format => "html" } => sub { ... }; so it's not the case of changing the response format to something other than html.

kraih commented 3 months ago

I don't quite see the bug i'm afraid, the logic seems to follow the documentation in the precedence order it is described. https://docs.mojolicious.org/Mojolicious/Guides/Rendering#Content-negotiation

vmmello commented 3 months ago

From the documentation you pointed, I imagine you're referring to this phrase as the precedence order:

The best possible representation will be automatically selected from the _format GET/POST parameter, 
format stash value or Accept request header and stored in the format stash value.

If so, ok, it looks like the correct behavior.

But then, what would be the correct way to change a single endpoint to a fallback response format using content negotiation? (it seems to me that Accept: */* header doesn't match the any format).