diff --git a/app/Http/Request.php b/app/Http/Request.php
index 2b192d2b8d3f805c6e0f788b96cd5199f7f03dcf..fbaa48d637b52245e27f269753377c3025bf5db6 100644
--- a/app/Http/Request.php
+++ b/app/Http/Request.php
@@ -77,6 +77,18 @@ class Request extends ServerRequest
         return $currentProtocol;
     }
 
+    /**
+     * Was the request an ssl secured request
+     *
+     * You might want to return an error response when the request was not secured via ssl.
+     *
+     * @return bool
+     */
+    public function isSslSecured(): bool
+    {
+        return $this->getProtocol() === 'https';
+    }
+
     /**
      * Check if the proxy is a trusted proxy.
      *
@@ -120,9 +132,9 @@ class Request extends ServerRequest
      */
     public function accepts(string $mimeType): bool
     {
-        $re = '/(^|,)' . preg_quote($mimeType, '/') . '(;|,|$)/i';
-        return $this->hasHeader('Accept') &&
-            preg_match($re, $this->getHeader('Accept')[0]);
+        list($type, $subType) = explode('/', $mimeType);
+        $re = '/(^|, ?)' . preg_quote($type, '/') . '\/(\*|' . preg_quote($subType, '/') . ')(;|,|$)/i';
+        return $this->hasHeader('Accept') && preg_match($re, $this->getHeader('Accept')[0]);
     }
 
     public function get(string $key = null, $default = null)
@@ -184,7 +196,9 @@ class Request extends ServerRequest
      */
     public function getJson(bool $assoc = true, int $depth = 512, int $options = 0)
     {
-        if (strtolower($this->getHeader('Content-Type')[0]) !== 'application/json') {
+        if ($this->hasHeader('Content-Type') &&
+            strtolower($this->getHeader('Content-Type')[0]) !== 'application/json'
+        ) {
             return null;
         }
 
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 0872a016aedd5d19cf37e41c1bd3ec935e0b897f..085ad61b06c0bee8d3cfc940744c4e7d13a9b46e 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -8,6 +8,8 @@ use App\Environment;
 use Hugga\Console;
 use Mockery\Adapter\Phpunit\MockeryTestCase;
 use Mockery as m;
+use Monolog\Handler\TestHandler;
+use Monolog\Logger;
 use Whoops;
 
 abstract class TestCase extends MockeryTestCase
@@ -62,6 +64,10 @@ abstract class TestCase extends MockeryTestCase
         $this->app->instance('whoops', $whoops);
         $whoops->unregister();
         $whoops->shouldReceive('register')->andReturnSelf()->byDefault();
+
+        $logger = $this->mocks['logger'] = m::mock(Logger::class)->makePartial();
+        $logger->__construct('app', [new TestHandler()]);
+        $this->app->instance('logger', $logger);
         
         /** @var Console|m\Mock $console */
         $console = $this->mocks['console'] = m::mock(Console::class)->makePartial();
diff --git a/tests/Unit/ApplicationTest.php b/tests/Unit/ApplicationTest.php
index cab77c8542be4a0db3279106be2834b800a2333c..d7b33816798f45de06dd30131954aebe15bc8b87 100644
--- a/tests/Unit/ApplicationTest.php
+++ b/tests/Unit/ApplicationTest.php
@@ -21,12 +21,9 @@ class ApplicationTest extends TestCase
     /** @test */
     public function definesAnErrorHandlerForLogging()
     {
-        $handler = new PlainTextHandler($this->app->logger);
-        $handler->loggerOnly(true);
-
         $this->app->initWhoops();
 
-        self::assertEquals($handler, $this->app->get('whoops')->popHandler());
+        self::assertInstanceOf(PlainTextHandler::class, $this->app->get('whoops')->popHandler());
     }
 
     /** @test */
diff --git a/tests/Unit/Http/RequestTest.php b/tests/Unit/Http/RequestTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..fe0253c18baf42f11258ef07019f6ef6f4fe5879
--- /dev/null
+++ b/tests/Unit/Http/RequestTest.php
@@ -0,0 +1,382 @@
+<?php
+
+namespace Test\Unit\Http;
+
+use App\Http\Request;
+use GuzzleHttp\Psr7\Utils;
+use InvalidArgumentException;
+use Mockery as m;
+use Test\TestCase;
+use function GuzzleHttp\Psr7\stream_for;
+
+class RequestTest extends TestCase
+{
+    /** @test */
+    public function getQueryReturnsTheCompleteQuery()
+    {
+        $request = (new Request('GET', '/any/path'))
+            ->withQueryParams(['foo' => 42, 'bar' => 23]);
+
+        $query = $request->getQuery();
+
+        self::assertSame(['foo' => 42, 'bar' => 23], $query);
+    }
+
+    /** @test */
+    public function getQueryReturnsSpecificParameter()
+    {
+        $request = (new Request('GET', '/any/path'))
+            ->withQueryParams(['foo' => 42, 'bar' => 23]);
+
+        $query = $request->getQuery('foo');
+
+        self::assertSame(42, $query);
+    }
+
+    /** @test */
+    public function getQueryReturnsTheDefaultValue()
+    {
+        $request = new Request('GET', '/any/path');
+
+        $query = $request->getQuery('foo', 42);
+
+        self::assertSame(42, $query);
+    }
+
+    /** @test */
+    public function getPostReturnsTheCompletePost()
+    {
+        $request = (new Request('POST', '/any/path'))
+            ->withParsedBody(['foo' => 42, 'bar' => 23]);
+
+        $post = $request->getPost();
+
+        self::assertSame(['foo' => 42, 'bar' => 23], $post);
+    }
+
+    /** @test */
+    public function getPostReturnsSpecificParameter()
+    {
+        $request = (new Request('POST', '/any/path'))
+            ->withParsedBody(['foo' => 42, 'bar' => 23]);
+
+        $post = $request->getPost('foo');
+
+        self::assertSame(42, $post);
+    }
+
+    /** @test */
+    public function getPostReturnsTheDefaultValue()
+    {
+        $request = new Request('POST', '/any/path');
+
+        $post = $request->getPost('foo', 42);
+
+        self::assertSame(42, $post);
+    }
+
+    /** @test */
+    public function getJsonReturnsTheRequestBodyJsonDecoded()
+    {
+        $data = [
+            'foo' => 42,
+            'bar' => 23,
+            'baz' => null,
+        ];
+        $request = (new Request('POST', '/any/path'))
+            ->withBody(Utils::streamFor(json_encode($data)));
+
+        self::assertSame($data, $request->getJson());
+    }
+
+    /** @test */
+    public function getJsonRetrunsNullWhenTheContentTypeHeaderIsNotJson()
+    {
+        $request = (new Request('POST', '/any/path', [
+            'Content-Type' => 'text/plain',
+        ]))->withBody(Utils::streamFor(json_encode(['a' => 'b'])));
+
+        self::assertNull($request->getJson());
+    }
+
+    /** @test */
+    public function getJsonLogsNoticeWhenBodyIsInvalid()
+    {
+        $request = (new Request('POST', '/any/path'))
+            ->withBody(Utils::streamFor("{foo:'bar'}")); // this is not json but javascript
+
+        $this->mocks['logger']->shouldReceive('notice')->once();
+
+        $request->getJson();
+    }
+
+    /** @test */
+    public function getJsonThrowsNotWhenTheJsonIsNull()
+    {
+        $request = (new Request('POST', '/any/path'))
+            ->withBody(Utils::streamFor(json_encode(null)));
+
+        self::assertNull($request->getJson());
+    }
+
+    /** @test */
+    public function getIpReturnsTheRemoteAddr()
+    {
+        $request = (new Request('GET', '/any/path', [], null, '1.1', [
+            'REMOTE_ADDR' => '172.19.0.9',
+        ]));
+
+        $ip = $request->getIp();
+
+        self::assertSame('172.19.0.9', $ip);
+    }
+
+    /** @dataProvider provideIpHeaders
+     * @test */
+    public function getIpReturnsTheRealIpWhenProxyIsTrusted(array $header, string $remoteAddr, string $expected)
+    {
+        $config = $this->app->config;
+        $config->trustedProxies = [$remoteAddr];
+        $request = (new Request('GET', '/any/path', $header, null, '1.1', [
+            'REMOTE_ADDR' => $remoteAddr,
+        ]));
+
+        $ip = $request->getIp();
+
+        self::assertSame($expected, $ip);
+    }
+
+    public function provideIpHeaders()
+    {
+        return [
+            'X-Real-Ip' => [['X-Real-Ip' => '8.8.8.8'], '10.0.0.1', '8.8.8.8'],
+            'X-Forwarded-For' => [['X-Forwarded-For' => '8.8.8.8'], '10.0.0.1', '8.8.8.8'],
+            'precedence' => [[ // prefers x-real-ip
+                'X-Forwarded-For' => '23.0.4.2',
+                'X-Real-Ip' => '8.8.8.8',
+            ], '10.0.0.1', '8.8.8.8'],
+            'last-entry' => [[ // uses the last entry
+                'X-Forwarded-For' => '23.0.4.2, 8.8.8.8',
+            ], '10.0.0.1', '8.8.8.8'],
+        ];
+    }
+
+    /** @test */
+    public function getIpReturnsTheRemoteAddrIfNoRealIpHeaderGiven()
+    {
+        $config = $this->app->config;
+        $config->trustedProxies = ['10.0.0.1'];
+        $request = (new Request('GET', '/any/path', [], null, '1.1', [
+            'REMOTE_ADDR' => '10.0.0.1',
+        ]));
+
+        $ip = $request->getIp();
+
+        self::assertSame('10.0.0.1', $ip);
+    }
+
+    /** @test */
+    public function getIpReturnsTheRemoteAddrWhenProxyIsUntrusted()
+    {
+        /** @var Request|m\MockInterface $request */
+        $request = m::mock(Request::class)->makePartial();
+        $request->__construct('GET', '/any/path', [
+            'X-Forwarded-For' => '23.0.4.2'
+        ], null, '1.1', [
+            'REMOTE_ADDR' => '8.8.8.8',
+        ]);
+        $request->shouldReceive('isTrustedForward')->once()->andReturnFalse();
+
+        $ip = $request->getIp();
+
+        self::assertSame('8.8.8.8', $ip);
+    }
+
+    /** @test */
+    public function getReturnsTheValueFromQuery()
+    {
+        $request = (new Request('GET', '/any/path'))
+            ->withQueryParams(['foo' => 'bar']);
+
+        self::assertSame('bar', $request->get('foo'));
+    }
+
+    /** @test */
+    public function getReturnsTheValueFromPost()
+    {
+        $request = (new Request('GET', '/any/path'))
+            ->withQueryParams(['foo' => 'bar'])
+            ->withParsedBody(['foo' => 'baz']);
+
+        self::assertSame('baz', $request->get('foo'));
+    }
+
+    /** @test */
+    public function getReturnsTheValueFromJson()
+    {
+        $request = (new Request('GET', '/any/path'))
+            ->withQueryParams(['foo' => 'bar'])
+            ->withBody(Utils::streamFor(json_encode(['foo' => 'baz'])));
+
+        self::assertSame('baz', $request->get('foo'));
+    }
+
+    /** @test */
+    public function getReturnsTheMergedArray()
+    {
+        $request = (new Request('GET', '/any/path'))
+            ->withQueryParams(['foo' => 'bar'])
+            ->withBody(Utils::streamFor(json_encode(['answer' => 42])));
+
+        self::assertSame([
+            'foo' => 'bar',
+            'answer' => 42,
+        ], $request->get());
+    }
+
+    /** @test */
+    public function getProtocolReturnsTheProtocolTheClientUsed()
+    {
+        // by default (on cli) it is http
+        $request = new Request('GET', '/any/path');
+
+        $protocol = $request->getProtocol();
+
+        self::assertSame('http', $protocol);
+    }
+
+    /** @test */
+    public function getProtocolReadsServerParamHttps()
+    {
+        $request = new Request('GET', '/any/path', [], null, '1.1', ['HTTPS' => 'on']);
+
+        $protocol = $request->getProtocol();
+
+        self::assertSame('https', $protocol);
+    }
+
+    /** @test */
+    public function getProtocolAcceptsXForwardedProtoForTrustedProxies()
+    {
+        /** @var m\MockInterface|Request $request */
+        $request = m::mock(Request::class)->makePartial();
+        $request->__construct('GET', '/any/path', ['X-Forwarded-Proto' => 'foobar']);
+        $request->shouldReceive('isTrustedForward')->once()->andReturn(true);
+
+        $protocol = $request->getProtocol();
+
+        self::assertSame('foobar', $protocol);
+    }
+
+    /** @test */
+    public function getProtocolReturnsCurrentProtocolIfNoForwardedProtocolGiven()
+    {
+        /** @var m\MockInterface|Request $request */
+        $request = m::mock(Request::class)->makePartial();
+        $request->__construct('GET', '/any/path', []);
+        $request->shouldReceive('isTrustedForward')->once()->andReturn(true);
+
+        $protocol = $request->getProtocol();
+
+        self::assertSame('http', $protocol);
+    }
+
+    /** @test */
+    public function getProtocolOffMeansNoSsl()
+    {
+        $request = new Request('GET', '/any/path', [], null, '1.1', ['HTTPS' => 'off']);
+
+        $protocol = $request->getProtocol();
+
+        self::assertSame('http', $protocol);
+    }
+
+    /** @test */
+    public function isTrustedForwardIsFalseWhenNoProxiesAreTrusted()
+    {
+        $this->app->config->trustedProxies = [];
+        $request = new Request('GET', '/any/path');
+
+        $trusted = $request->isTrustedForward();
+
+        self::assertFalse($trusted);
+    }
+
+    /** @test */
+    public function isTrustedForwardIsTrueWhenTheRemoteAddrMatches()
+    {
+        $this->app->config->trustedProxies = ['127.0.0.1'];
+        $request = new Request('GET', '/any/path', [], null, '1.1', ['REMOTE_ADDR' => '127.0.0.1']);
+
+        $trusted = $request->isTrustedForward();
+
+        self::assertTrue($trusted);
+    }
+
+    /** @test */
+    public function isTrustedForwardIsTrueWhenTheRemoteAddrMatchesAnyRange()
+    {
+        $this->app->config->trustedProxies = ['127.0.0.1', '10.23.42.0/24'];
+        $request = new Request('GET', '/any/path', [], null, '1.1', ['REMOTE_ADDR' => '10.23.42.1']);
+
+        $trusted = $request->isTrustedForward();
+
+        self::assertTrue($trusted);
+    }
+
+    /** @test */
+    public function isTrustedForwardIsFalseWhenTheProxyIsUntrusted()
+    {
+        $this->app->config->trustedProxies = ['127.0.0.1', '10.23.42.0/24'];
+        $request = new Request('GET', '/any/path', [], null, '1.1', ['REMOTE_ADDR' => '192.168.0.42']);
+
+        $trusted = $request->isTrustedForward();
+
+        self::assertFalse($trusted);
+    }
+
+    /** @test */
+    public function isSslSecuredIsTrueWhenTheProtocolIsHttps()
+    {
+        /** @var m\MockInterface|Request $request */
+        $request = m::mock(Request::class)->makePartial();
+        $request->__construct('GET', '/any/path');
+        $request->shouldReceive('getProtocol')->once()->andReturn('https');
+
+        $secured = $request->isSslSecured();
+
+        self::assertTrue($secured);
+    }
+
+    /** @test
+     * @dataProvider provideAcceptHeaders */
+    public function acceptsReturnsIfTheMimeTypeIsAccepted($header, $type, $accepted)
+    {
+        $request = new Request('GET', '/any/pah', [
+            'accept' => $header,
+        ]);
+
+        self::assertSame($accepted, $request->accepts($type));
+    }
+
+    public function provideAcceptHeaders()
+    {
+        return [
+            ['text/html', 'text/html', true],
+            ['text/html', 'application/json', false],
+            ['text/html,application/json;q=0.8', 'application/json', true],
+            ['image/png,image/*;q=0.8', 'image/jpeg', true],
+            ['text/html, application/xhtml+xml', 'application/xhtml+xml', true],
+        ];
+    }
+
+    /** @test */
+    public function acceptsIgnoresAsteriskSlashAsterisk()
+    {
+        $request = new Request('GET', '/any/pah', [
+            'accept' => 'text/html,*/*;q=0.4',
+        ]);
+
+        self::assertFalse($request->accepts('application/json'));
+    }
+}