8000 [HttpFoundation] [Request] Throw dedicated exceptions. by SpacePossum · Pull Request #20389 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[HttpFoundation] [Request] Throw dedicated exceptions. #20389

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

Conversation

SpacePossum
Copy link
Contributor
Q A
Branch? master (but the lower the better ;) )
Bug fix? no
New feature? yes
BC breaks? not really
Deprecations? no
Tests pass? yes
License MIT

I'm trying to deal with invalid requests being sent to one of my sites.
While this is a Silex site I think this PR will help dealing with such requests and will be useful for SF developers as well.

The problem is that sometimes a request is send with host data like "my.test.com/".
The trailing slash will cause the Request class to throw an exception when asking for the host (for example through getHost()).

In order to deal with these kind of errors I setup something like:

// bootstrap.php

$app = new Application(); // this is a Silex app

// more bootstrap stuff

$app->register(new MonologServiceProvider(), [/**/]);
$app['monolog.listener'] = function () use ($app) {
    return new class($app['logger'], $app['folders.log']) extends LogListener {
        protected function logException(\Exception $e)
        {
            if ($e instanceof HttpExceptionInterface) {
                if ($e->getStatusCode() >= 500) { // don't log 400 errors
                    $this->logger->critical($message, ['exception' => $e]);
                }
            } else {
                // FIXME deal with host problem here
                $this->logger->critical($message, ['exception' => $e]);
            }		
        }
    }
};

$app->error(function (\Exception $e, Request $request, $code = null) use ($app) {
    // FIXME deal with host problem here
}

// more bootstrap stuff

$app->run();

Dealing with the exception at these points is hard because the Request class throws an SPL \UnexpectedValueException exception.
At the point of handling it is hard to see this exception comes from the Request class and not from somewhere else in the system (for example a controller dealing with the Request object).

It is not possible to deal with exception within the controllers and/or other part of the application on user level.
This is because the exception is thrown before the Request is given to a controller.

Example trace:

#0 /var/www/digital-shop-intermediate-site/vendor/symfony/http-foundation/Request.php(1032): Symfony\Component\HttpFoundation\Request->getHost()
#1 /var/www/digital-shop-intermediate-site/vendor/symfony/http-foundation/Request.php(1062): Symfony\Component\HttpFoundation\Request->getHttpHost()
#2 /var/www/digital-shop-intermediate-site/vendor/symfony/http-foundation/Request.php(1716): Symfony\Component\HttpFoundation\Request->getSchemeAndHttpHost()
#3 /var/www/digital-shop-intermediate-site/vendor/symfony/http-foundation/Request.php(1046): Symfony\Component\HttpFoundation\Request->prepareRequestUri()
#4 /var/www/digital-shop-intermediate-site/vendor/symfony/http-foundation/Request.php(1768): Symfony\Component\HttpFoundation\Request->getRequestUri()
#5 /var/www/digital-shop-intermediate-site/vendor/symfony/http-foundation/Request.php(924): Symfony\Component\HttpFoundation\Request->prepareBaseUrl()
#6 /var/www/digital-shop-intermediate-site/vendor/symfony/routing/RequestContext.php(73): Symfony\Component\HttpFoundation\Request->getBaseUrl()
#7 /var/www/digital-shop-intermediate-site/vendor/symfony/http-kernel/EventListener/RouterListener.php(71): Symfony\Component\Routing\RequestContext->fromRequest(Object(Symfony\Component\HttpFoundation\Request))
#8 /var/www/digital-shop-intermediate-site/vendor/symfony/http-kernel/EventListener/RouterListener.php(90): Symfony\Component\HttpKernel\EventListener\RouterListener->setCurrentRequest(Object(Symfony\Component\HttpFoundation\Request))
#9 [internal function]: Symfony\Component\HttpKernel\EventListener\RouterListener->onKernelRequest(Object(Symfony\Component\HttpKernel\Event\GetResponseEvent), 'kernel.request', Object(Symfony\Component\EventDispatcher\EventDispatcher))
#10 /var/www/digital-shop-intermediate-site/vendor/symfony/event-dispatcher/EventDispatcher.php(174): call_user_func(Array, Object(Symfony\Component\HttpKernel\Event\GetResponseEvent), 'kernel.request', Object(Symfony\Component\EventDispatcher\EventDispatcher))
#11 /var/www/digital-shop-intermediate-site/vendor/symfony/event-dispatcher/EventDispatcher.php(43): Symfony\Component\EventDispatcher\EventDispatcher->doDispatch(Array, 'kernel.request', Object(Symfony\Component\HttpKernel\Event\GetResponseEvent))
#12 /var/www/digital-shop-intermediate-site/vendor/symfony/http-kernel/HttpKernel.php(129): Symfony\Component\EventDispatcher\EventDispatcher->dispatch('kernel.request', Object(Symfony\Component\HttpKernel\Event\GetResponseEvent))
#13 /var/www/digital-shop-intermediate-site/vendor/symfony/http-kernel/HttpKernel.php(68): Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object(Symfony\Component\HttpFoundation\Request), 1)
#14 /var/www/digital-shop-intermediate-site/vendor/silex/silex/src/Silex/Application.php(496): Symfony\Component\HttpKernel\HttpKernel->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
#15 /var/www/digital-shop-intermediate-site/vendor/silex/silex/src/Silex/Application.php(477): Silex\Application->handle(Object(Symfony\Component\HttpFoundation\Request))
#16 /var/www/digital-shop-intermediate-site/web/index.php(13): Silex\Application->run()

It will not only be a lot of work modifying all components dealing with the host of a Request (either by getHost or getHttpHost) before the Request is routed to a controller, it will also be hard to make sure when new components are added (or existing modified) deal with the exception correctly.

Therefor I propose to let the Request class throw exceptions of meaningful (/dedicated) classes. That way people can deal with these exception as they want.

* file that was distributed with this source code.
*/

namespace Symfony\Component\HttpFoundation\Exception;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other components, exceptions implement an interface specific to the component.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which interface would be appropriate in this case?
For example currently the class throws exceptions of the class ConflictingHeadersException but that doesn't implement any interface.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was only one exception but if the component contains more exceptions, they should implement an ExceptionInterface.

@SpacePossum SpacePossum force-pushed the master_Request_dedicated_exceptions branch from 8b34ded to 9807f06 Compare November 4, 2016 08:58
@SpacePossum
8000 Copy link
Contributor Author

reworked, @Ener-Getick please have a look :)

@@ -1220,7 +1226,7 @@ public function getHost()
// check that it does not contain forbidden characters (see RFC 952 and RFC 2181)
// use preg_replace() instead of preg_match() to prevent DoS attacks with long host names
if ($host && '' !== preg_replace('/(?:^\[)?[a-zA-Z0-9-:\]_]+\.?/', '', $host)) {
throw new \UnexpectedValueException(sprintf('Invalid Host "%s"', $host));
throw new InvalidHostException($host, sprintf('Invalid host "%s".', $host));
Copy link
Member
@chalasr chalasr Nov 4, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't we just pass the message as it contains the host, such as done for UntrustedHostException?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to get the host that triggered the exception to be thrown when I'm dealing with the exception. Relying on the message (format) of an exception is never a good idea IMHO.

*/
public static function getTrustedHeaderName($key)
{
if (!array_key_exists($key, self::$trustedHeaders)) {
throw new \InvalidArgumentException(sprintf('Unable to get the trusted header name for key "%s".', $key));
throw new InvalidTrustedHeaderException($key, null, sprintf('Unable to get the trusted header name for key "%s".', $key));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't an InvalidArgumentException localized in the component enough and more appropriate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to revert :)

@@ -1936,7 +1937,7 @@ public function testHostValidity($host, $isValid, $expectedHost = null, $expecte
$this->assertSame($expectedPort, $request->getPort());
}
} else {
$this->setExpectedException('UnexpectedValueException', 'Invalid Host');
$this->setExpectedException('Symfony\Component\HttpFoundation\Exception\InvalidHostException', 'Invalid host');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use the short syntax here Exception::class

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah SF upped requirements to PHP 5.5, good point will update

*
* @author SpacePossum
*/
final class InvalidHostException extends AbstractHostException
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this one should be internal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 agreed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought, I don't think so, people want to deal with this exception by catching it. Marking it as internal will cause IDE's to complain about using an internal class.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed sorry I didn't commented on the right class... I was thinking about AbstractHostException.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I've updated that class.

* @param string $host
* @param string $message
*/
public function __construct($host, $message)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not making $message optional (if null, default to Invalid host "'.$host.'"), so you can simply throw new InvalidHostException($host).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making it null able will break with the design of the base exception class itself, which is IIRC something people don't want in the SF project.
Besides there is no way this class can generate a useful/correct message based on the value of $host alone, the logic that figured out the $host value is incorrect and throws the exception knows what is wrong and can therefor set a meaningful message only.

* @param string|null $value
* @param string $message
*/
public function __construct($header, $value, $message)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same.. make message optional?

/**
* @var string
*/
private $value;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having getHeader() / getValue() would be nice? Right now these are basically useless.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank, indeed, however as opt here I will remove the class all together.

abstract class AbstractHostException extends \UnexpectedValueException implements ExceptionInterface
{
/**
* @var string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe unnecessary docs can be removed.. same for the getter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these do no harm and will become of use when doing SCA, refactoring and/or updating to PHP 7 when the PHPDocs can be used to generate the type hints on method signatures.

Copy link
Member
@dunglas dunglas Nov 10, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are useless, they can be inferred from constructor's docs and most tools support this.

@hhamon
Copy link
Contributor
hhamon commented Nov 10, 2016

If this PR is accepted, it will only go into Symfony 4.0 only for BC reasons I suppose. You should also trigger some deprecation errors before every new exception types is thrown.

@ogizanagi
Copy link
Contributor

@hhamon : Why ? There is no BC break, as the new exceptions extends \UnexpectedValueException which was thrown before, right ?

} catch (\UnexpectedValueException $e) {
$this->assertEquals('Untrusted Host "evil.com"', $e->getMessage());
} catch (UntrustedHostException $e) {
$this->assertEquals('Untrusted host "evil.com".', $e->getMessage());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think host was capitalized referencing the Host header. I'm not sure this has to be changed (same for other occurrences).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alright 👍 updated back to Host

@SpacePossum
Copy link
Contributor Author

If this PR is accepted, it will only go into Symfony 4.0 only for BC reasons I suppose.

I agree with @ogizanagi here, there is no real BC break. The only way this can break is people rely on the exception being an instance of \UnexpectedValueException but not inherits from it.

You should also trigger some deprecation errors before every new exception types is thrown.

no, the logic of the exception being thrown is not deprecated and so it should not trigger such warnings.

@dunglas
Copy link
Member
dunglas commented Nov 10, 2016

👍

@SpacePossum
Copy link
Contributor Author

@dunglas I've removed the PHPDoc's as requested, anything else you want me to change to get the PR into the status that is ready and marked as reviewed?

@dunglas
Copy link
Member
dunglas commented Nov 16, 2016

It's ok for me, you now need to wait that at least another core team member review it and agree to merge it.

Status: reviewed

@xabbuh
Copy link
Member
xabbuh commented Nov 16, 2016

👍 for me (but for 3.3)

@@ -1940,7 +1945,12 @@ private static function createRequestFromFactory(array $query = array(), array $
$request = call_user_func(self::$requestFactory, $query, $request, $attributes, $cookies, $files, $server, $content);

if (!$request instanceof self) {
throw new \LogicException('The Request factory must return an instance of Symfony\Component\HttpFoundation\Request.');
throw new \UnexpectedValueException(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BC break :) although this is the better exception :(

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reverted, I don't care to much about that one anyway so ;)

@SpacePossum
Copy link
Contributor Author

but for 3.3

does this mean I've to retarget this PR?

@nicolas-grekas nicolas-grekas added this to the 3.x milestone Dec 6, 2016
@fabpot fabpot mentioned this pull request Dec 16, 2016
@fabpot
Copy link
Member
fabpot commented Dec 16, 2016

Closing in favor of #20962

@fabpot fabpot closed this Dec 16, 2016
@SpacePossum SpacePossum deleted the master_Request_dedicated_exceptions branch December 16, 2016 16:30
fabpot added a commit that referenced this pull request Dec 17, 2016
This PR was merged into the 3.3-dev branch.

Discussion
----------

Request exceptions

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | yes
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #20389, #20615, #20662
| License       | MIT
| Doc PR        | n/a

Replaces #20389 and #20662. The idea is to generically manage 400 responses when an exception implements `RequestExceptionInterface`.

The "weird" caches on the request for the host and the clients IPs allows to correctly manage exceptions in an exception listener/controller (as we are duplicating the request there, but we don't want to throw an exception there).

Commits
-------

32ec288 [HttpFoundation] refactored Request exceptions
d876809 Return a 400 response for suspicious operations
@nicolas-grekas nicolas-grekas modified the milestones: 3.x, 3.3 Mar 24, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

0