Skip to content

[TwigBridge] Improve error rendering when running tests #58456

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

Closed
wants to merge 13 commits into from

Conversation

javiereguiluz
Copy link
Member

@javiereguiluz javiereguiluz commented Oct 4, 2024

Q A
Branch? 7.2
Bug fix? no
New feature? yes
Deprecations? no
Issues -
License MIT

Exception handling in Symfony is fantastic. E.g. I add a $foo = 0 / 0; statement in the Symfony Demo app and I see this nice and useful exception page in debug mode:

image


The problem is that if I run tests, that exception will output the same content in the terminal. Thousands and thousands of lines of HTML + CSS + JS 😐

image


This is easily one of the worst and most frustrating parts of Symfony. In #31514 we tried to improve this by adding an HTML comment at the beginning and the end of the long HTML exception page to quickly figure out the exception in the console.


This PR proposes to always use the CliRenderer when the exception is going to be rendered in a console/terminal context.

For example, the same exception as before now would look like this 🙌

image

@javiereguiluz javiereguiluz added this to the 7.2 milestone Oct 4, 2024
@carsonbot carsonbot changed the title Improve error rendering when running tests [TwigBridge] Improve error rendering when running tests Oct 4, 2024
Copy link
Member

@stof stof left a comment

Choose a reason for hiding this comment

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

Returning Content-type: text/html when using the CliErrorRenderer is wrong as it does not return HTML (and could lead to XSS if some of the content looks like HTML tags). The CliErrorRenderer should be updated to add a header setting the content type to text/plain in the FLattenException it returns (which won't change anything when using it for actual CLI usage, but will behave properly when using the CliErrorRenderer to return an HTTP response)

@javiereguiluz javiereguiluz requested a review from yceruto as a code owner October 7, 2024 07:11
@javiereguiluz
Copy link
Member Author

I made the changes requested in the review. Thanks!

@javiereguiluz javiereguiluz added the ❄️ Feature Freeze Important Pull Requests to finish before the next Symfony "feature freeze" label Oct 7, 2024
Copy link
Member

@alexandre-daubois alexandre-daubois left a comment

Choose a reason for hiding this comment

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

Related to #58070, so it's a 👍 for me, definitely!

Edit: approved once tests are green of course 😄

bool|callable $debug = false,
) {
$this->fallbackErrorRenderer = $fallbackErrorRenderer ?? new HtmlErrorRenderer();
$this->fallbackErrorRenderer = \in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) ? new CliErrorRenderer() : ($fallbackErrorRenderer ?? new HtmlErrorRenderer());
Copy link
Member

@nicolas-grekas nicolas-grekas Oct 9, 2024

Choose a reason for hiding this comment

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

This is breaking DI: if one dared to pass a $fallbackErrorRenderer, we have to honor it.
In order to pass a CliErrorRenderer, we should use DI instead.
If this targets tests, then we might pass a CliErrorRenderer service when framework.test is enabled?

Copy link
Member Author

Choose a reason for hiding this comment

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

What if we change that line by:

$this->fallbackErrorRenderer = $fallbackErrorRenderer ?? (\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) ? new CliErrorRenderer() : new HtmlErrorRenderer());

Copy link
Member

Choose a reason for hiding this comment

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

My suggestion in #58456 (review) was about changing the default fallback renderer, which is indeed what your new suggestion does.

Copy link
Member

Choose a reason for hiding this comment

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

would this achieve what you want?
we do define a $fallbackErrorRenderer so I guess the new code would be dead code

Copy link
Member Author

Choose a reason for hiding this comment

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

You are right. The feature no longer works after changing that line.

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Oct 9, 2024

What about improving the HtmlErrorRendered instead so that it dumps a text version after the HTML one in an HTML comment? (we'd just need to ensure the dump cannot break the HTML comment to make this XSS-safe)

@javiereguiluz
Copy link
Member Author

@nicolas-grekas that would be like iterating over the idea implemented in #31514. But, this proposal tries to fix this for real and never output HTML content in the console.

I pushed a commit a043bf7 trying to do some of the things that you said, but honestly I don't know what I'm doing 😐

@javiereguiluz
Copy link
Member Author

Nicolas gave me more clues about how to solve this so I submitted a new commit with a different approach. Thanks.

use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Workflow\Workflow;
use Symfony\Component\Yaml\Yaml;
use function Symfony\Component\DependencyInjection\Loader\Configurator\inline_service;
Copy link
Member

@stof stof Oct 9, 2024

Choose a reason for hiding this comment

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

this is unused (like many other use statements)

// in the 'test' environment, use the CLI error renderer as the default one
if ($container->hasDefinition('test.client')) {
$container->getDefinition('twig.error_renderer.html')
->setArgument(1, new Definition(CliErrorRenderer::class));
Copy link
Member

Choose a reason for hiding this comment

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

wouldn't this break if the error-handler component is not installed (which is not a dependency of the bundle right now) ?

Copy link
Member

Choose a reason for hiding this comment

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

and maybe we need a conflict with older versions of the component to ensure we don't inject a version that does not properly report a plaintext content type (and so is subject to XSS)

// in the 'test' environment, use the CLI error renderer as the default one
if ($container->hasDefinition('test.client')) {
$container->getDefinition('twig.error_renderer.html')
->setArgument(1, new Definition(CliErrorRenderer::class));
Copy link
Member

Choose a reason for hiding this comment

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

I think the decision of which fallback error renderer is the best does not depend on the environment but on the SAPI value, as you did at the beginning. What about creating a service factory that injects the proper error renderer depending on the SAPI value?

@yceruto
Copy link
Member

yceruto commented Oct 9, 2024

By the way, the issue described (PR description) also applies when the twig-bundle is not installed, so updating the TwigErrorRenderer service is not enough.

@nicolas-grekas
Copy link
Member

Solving this in a bundle looks appropriate to me. DI is the proper way to improve the DX here. Let's keep things simple.

@yceruto
Copy link
Member

yceruto commented Oct 9, 2024

My 5 cents on achieving this by using a factory to determine which renderer is most appropriate, depending on the SAPI value:

Patch (Click to open)

diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.php
index 67f28ce44d..a40fb2aa97 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.php
@@ -11,7 +11,10 @@
 
 namespace Symfony\Component\DependencyInjection\Loader\Configurator;
 
+use Symfony\Component\ErrorHandler\ErrorRenderer\CliErrorRenderer;
+use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;
 use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
+use Symfony\Component\ErrorHandler\ErrorRenderer\SapiErrorRendererFactory;
 
 return static function (ContainerConfigurator $container) {
     $container->services()
@@ -32,7 +35,18 @@ return static function (ContainerConfigurator $container) {
                 service('logger')->nullOnInvalid(),
             ])
 
+        ->set('error_handler.error_renderer.cli', CliErrorRenderer::class)
+
+        ->set('error_handler.error_renderer.default', ErrorRendererInterface::class)
+            ->factory([SapiErrorRendererFactory::class, 'create'])
+            ->args([
+                service('error_renderer.cli'),
+                service('error_renderer.html'),
+            ])
+
         ->alias('error_renderer.html', 'error_handler.error_renderer.html')
-        ->alias('error_renderer', 'error_renderer.html')
+        ->alias('error_renderer.cli', 'error_handler.error_renderer.cli')
+        ->alias('error_renderer.default', 'error_handler.error_renderer.default')
+        ->alias('error_renderer', 'error_renderer.default')
     ;
 };
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php
index d689d42995..eadc71db13 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php
@@ -213,7 +213,7 @@ return static function (ContainerConfigurator $container) {
                 inline_service()
                     ->factory([SerializerErrorRenderer::class, 'getPreferredFormat'])
                     ->args([service('request_stack')]),
-                service('error_renderer.html'),
+                service('error_renderer.default'),
                 inline_service()
                     ->factory([HtmlErrorRenderer::class, 'isDebug'])
                     ->args([service('request_stack'), param('kernel.debug')]),
diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json
index af83a9a13f..fe49a27735 100644
--- a/src/Symfony/Bundle/FrameworkBundle/composer.json
+++ b/src/Symfony/Bundle/FrameworkBundle/composer.json
@@ -23,7 +23,7 @@
         "symfony/config": "^6.4|^7.0",
         "symfony/dependency-injection": "^7.2",
         "symfony/deprecation-contracts": "^2.5|^3",
-        "symfony/error-handler": "^6.4|^7.0",
+        "symfony/error-handler": "^7.2",
         "symfony/event-dispatcher": "^6.4|^7.0",
         "symfony/http-foundation": "^6.4|^7.0",
         "symfony/http-kernel": "^6.4|^7.0",
diff --git a/src/Symfony/Component/ErrorHandler/ErrorRenderer/SapiErrorRendererFactory.php b/src/Symfony/Component/ErrorHandler/ErrorRenderer/SapiErrorRendererFactory.php
new file mode 100644
index 0000000000..89b6e5b75c
--- /dev/null
+++ b/src/Symfony/Component/ErrorHandler/ErrorRenderer/SapiErrorRendererFactory.php
@@ -0,0 +1,20 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\ErrorHandler\ErrorRenderer;
+
+final class SapiErrorRendererFactory
+{
+    public static function create(ErrorRendererInterface $cliErrorRenderer, ErrorRendererInterface $htmlErrorRenderer): ErrorRendererInterface
+    {
+        return \in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) ? $cliErrorRenderer : $htmlErrorRenderer;
+    }
+}

DI graph after the patch:

image

In fact, no change is required in TwigErrorRenderer as it's just a decorator of the html renderer.

@javiereguiluz
Copy link
Member Author

Let's close this in favor of #60033, which tries to achieve the same goals but with a different strategy. Thanks Yonel!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature ❄️ Feature Freeze Important Pull Requests to finish before the next Symfony "feature freeze" Status: Needs Work TwigBridge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants