Laravel and Content Negotiation

Here's a little bit about content negotiation.

An HTTP client, such as your browser, or perhaps jQuery's ajax method, can set an Accept header as part of an HTTP request.

Accept Header

This header is meant to tell the server what content types it is willing to accept. From the HTTP 1.1 spec, section 14.1:

The Accept request-header field can be used to specify certain media types which are acceptable for the response.

Such a header might look something like this:

Accept: application/json

In a typical request from Chrome, we see something more like this:

Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8

As you can see, the Accept header can get complex.

The above example lists a few media types the client (Chrome) is willing to accept, and even gives them a "quality" factor (the q rating, value 0-1). This is essentially telling the server the ordered preference of content types it wants back.

I won't get any more into that, but your can read more about it in the HTTP spec.

The Server

It's up to the server to follow the rules of HTTP. When a request comes to our application, it's pretty easy to ignore these rules, as our frameworks generally let us return whatever we want.

This is the "negotiation" part. The client says what content types it's willing to accept, and its preference. The server then can decide what it's willing/able to send back. Or ignore it, if it's a rebel. Not much of a negotiation, if you ask me.

For example, if a request comes into your Laravel app with an Accept header requesting JSON, you can totally ignore it without ever realizing the HTTP client wanted something else:

Route::get('/foo', function()
{
    // Accept header? Whatever, bruh
    return view('foo.bar');
});

Checking the Accept header:

If you want to check for that header, you can do some manual stuff:

Route::get('/foo', function()
{
    $accept = request()->header('accept'); // application/json

    if( $accept === 'application/json' )
    {
        return ['foo' => 'bar']; // Returns JSON, thanks to Laravel Magicâ„¢
    }

    return view('foo.bar');
});

It gets easier, however:

Laravel provides a nice, easy way to check if a request "wants json":

Route::get('/foo', function()
{

    // Look at this nice, friendly global helper function
    // Hello, global function! We sincerely love you with all our <3
    if( request()->wantsJson() )
    {
        return ['foo' => 'bar'];
    }

    return view('foo.bar');
});

If you check out the function linked above, you can see how Laravel is using the underlying Symfony HTTP classes, which handle the dirty work of knowing that HTTP requests might send down multiple accept content types:

public function wantsJson()
{
    $acceptable = $this->getAcceptableContentTypes();
    return isset($acceptable[0]) && Str::contains($acceptable[0], ['/json', '+json']);
}

Note that this is grabbing the first of the list of acceptable content types, ordered by preference, sent in the HTTP request and testing if it is a JSON content type. It's not simply saying "Yeah, I guess JSON was one of the content types you wanted".

If you want to see if the request will accept JSON, regardless of it's set preference, use the acceptsJson() method.

Other Content Types

I suggest taking a look at some of the shortcuts the Laravel Request class has - you can check for other content types with the accepts() method, for example, or use the prefers() method to see which is the most prefered content type.

Middleware, or something

This is ripe for some sort of middleware or other functionality which automatically can decide to return JSON or HTML (or hey, even XML!). If you use RESTful routing controllers, that might be a nice addition, similar to how Rails lets you set a return type.

comments powered by Disqus