framework: [5.1] Request with 'CONTENT_TYPE'='application/json' behaves incorrectly with unit tests

Possibly related to an update in the Symphony framework.

How to reproduce:

app/Http/routes.php:

Route::post('test', 'TestController@postTest');

app/Exceptions/Handler.php

class Handler extends ExceptionHandler
{
    public function render($request, Exception $e)
    {
        /**
         * Solution found from http://stackoverflow.com/a/28947051/1172464
         */
        // If the request wants JSON (AJAX doesn't always want JSON)
        if ($request->wantsJson()) {
            // Define the response
            $response = [
                'errors' => 'Sorry, something went wrong.'
            ];

            // If the app is in debug mode
            if (config('app.debug')) {
                // Add the exception class name, message and stack trace to response
                $response['exception'] = get_class($e); // Reflection might be better here
                $response['message'] = $e->getMessage();
                $response['trace'] = $e->getTrace();
            }

            // Default response of 400
            $status = 400;

            // If this exception is an instance of HttpException
            if ($this->isHttpException($e)) {
                // Grab the HTTP status code from the Exception
                $status = $e->getStatusCode();
            }

            // Return a JSON response with the response array and status code
            return response()->json($response, $status);
        }

        // Default to the parent class' implementation of handler
        return parent::render($request, $e);
    }
}

app/Http/Controllers/TestController.php:

class TestController extends Controller
{
    public function postTest(Request $request) {
        $req = ['json' => (array) $request->json(), 'get' => $request->get('foo')];
        $this->validate($request, ['foo' => 'required']);
        return ['success' => true, 'request' => $req];
    }
}

tests/JsonTest.php:

class JsonTest extends TestCase
{
    use WithoutMiddleware;

    public function testPostJson()
    {
        $headers = [];
        $headers['CONTENT_TYPE'] = 'application/json';
        $headers['Accept'] = 'application/json';

        $this->post('/test', ['foo' => 'bar'], $headers)->seeJson(['success' => true, 'get' => 'bar']);
    }

    public function testCallJson()
    {
        $headers = [];
        $headers['CONTENT_TYPE'] = 'application/json';
        $headers['Accept'] = 'application/json';

        $server = $this->transformHeadersToServerVars($headers);

        $this->call('POST', '/test', ['foo' => 'bar'], [], [], $server);

        $this->seeJson(['success' => true, 'get' => 'bar']);
    }

    public function testCallJsonBody() {
        $headers = [];
        $headers['CONTENT_TYPE'] = 'application/json';
        $headers['Accept'] = 'application/json';

        $server = $this->transformHeadersToServerVars($headers);

        $this->call('POST', '/test', [], [], [], $server, json_encode(['foo' => 'bar']));

        $this->seeJson(['success' => true, 'get' => 'bar']);
    }

    public function testCallJsonBoth() {
        $headers = [];
        $headers['CONTENT_TYPE'] = 'application/json';
        $headers['Accept'] = 'application/json';

        $server = $this->transformHeadersToServerVars($headers);

        $this->call('POST', '/test', ['foo' => 'bar'], [], [], $server, json_encode(['foo' => 'bar']));

        $this->seeJson(['success' => true, 'get' => 'bar']);
    }
}

Expected

4 successes

Actual

$ phpunit 
PHPUnit 4.7.7 by Sebastian Bergmann and contributors.

FFF.

Time: 216 ms, Memory: 21.50Mb

There were 3 failures:

1) JsonTest::testPostJson
Unable to find JSON fragment ["get":"bar"] within [{"foo":["The foo field is required."]}].
Failed asserting that false is true.

/var/www/vhosts/laravel/testbed/vendor/laravel/framework/src/Illuminate/Foundation/Testing/CrawlerTrait.php:420
/var/www/vhosts/laravel/testbed/vendor/laravel/framework/src/Illuminate/Foundation/Testing/CrawlerTrait.php:399
/var/www/vhosts/laravel/testbed/tests/JsonTest.php:15

2) JsonTest::testCallJson
Unable to find JSON fragment ["get":"bar"] within [{"foo":["The foo field is required."]}].
Failed asserting that false is true.

/var/www/vhosts/laravel/testbed/vendor/laravel/framework/src/Illuminate/Foundation/Testing/CrawlerTrait.php:420
/var/www/vhosts/laravel/testbed/vendor/laravel/framework/src/Illuminate/Foundation/Testing/CrawlerTrait.php:399
/var/www/vhosts/laravel/testbed/tests/JsonTest.php:28

3) JsonTest::testCallJsonBody
Unable to find JSON fragment ["get":"bar"] within [{"request":{"get":null,"json":{"":{"foo":"bar"}}},"success":true}].
Failed asserting that false is true.

/var/www/vhosts/laravel/testbed/vendor/laravel/framework/src/Illuminate/Foundation/Testing/CrawlerTrait.php:420
/var/www/vhosts/laravel/testbed/vendor/laravel/framework/src/Illuminate/Foundation/Testing/CrawlerTrait.php:399
/var/www/vhosts/laravel/testbed/tests/JsonTest.php:40

FAILURES!
Tests: 4, Assertions: 5, Failures: 3.

Analysis:

  • If data is passed as request parameters, then it fails to validate
  • If data is passed as body content, validate works, but ‘get’ fails
  • If data is passed as both body content and parameters, then validate works and ‘get’ works

If data is passed as body content, then the ‘json’ available to the request is wrapped in a keyless array:

{"json":{"":{"foo":"bar"}}}

Just realised that this is because json() returns a ParameterBag and not an array.

Notes:

This doesn’t affect regular http POST requests where the content is attached as body content (as in from angularjs). This only requests posts within unit tests.

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Comments: 15 (11 by maintainers)

Most upvoted comments

I’m not entirely sure why it responds differently to actual requests than it does to testing requests. My best guess is either the server does some form of translation, or the browser claims to send one but actually sends both.

Either way, this is a definite bug in the Request class.