<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="https://ktherage.github.io/xsl/atom.xsl" media="all"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <id>https://ktherage.github.io/tags/security/</id>
  <title>Kévin THÉRAGE | Expert Symfony Developer - Security</title>
  <subtitle><![CDATA[Technical blog over web development, Symfony, PHP and best practices. Find tutorials and advices for developers.]]></subtitle>
  <link href="https://ktherage.github.io/tags/security/atom.xml" rel="self" type="application/atom+xml" />
  <link href="https://ktherage.github.io/tags/security/" rel="alternate" type="text/html" />
  <updated>2026-04-15T12:09:11+00:00</updated>
  <author>
    <name>Kévin THÉRAGE</name>
    <uri>https://ktherage.github.io/</uri>
  </author>
  <entry xml:lang="en">
    <id>https://ktherage.github.io/blog/my-eventsubscriber-silenced-errors/</id>
    <title>My EventSubscriber silenced errors, here&#039;s why</title>
    <published>2026-04-13T00:00:00+00:00</published>
    <link href="https://ktherage.github.io/blog/my-eventsubscriber-silenced-errors/" rel="alternate" type="text/html" />
    <content type="html">
      <![CDATA[<p>A Jira ticket came out with : <em>"There's a strange bug disallowing users to access a page at that time that day."</em>
Logs said multiple times : <em>"[that day T that time] request.ERROR: Uncaught PHP Exception Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException: "Access denied to that resource." at WhitelistSubscriber.php line 99"</em></p>
<p>I had no idea at first... 😅 Here's how I figured it out.</p>
<hr>
<h2 id="the-setup">The Setup</h2>
<p>I had an EventSubscriber checking page access based on a whitelist of routes. This was legacy code — refactoring it wasn't on the table at the time.</p>
<pre><code class="language-php hljs php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">EventSubscriber</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">EventDispatcher</span>\<span class="hljs-title">EventSubscriberInterface</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">HttpKernel</span>\<span class="hljs-title">Event</span>\<span class="hljs-title">RequestEvent</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">HttpKernel</span>\<span class="hljs-title">KernelEvents</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WhitelistRouteSubscriber</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">EventSubscriberInterface</span>
</span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">const</span> WHITELISTED_ROUTES = [
        <span class="hljs-string">'app_login'</span>,
        <span class="hljs-string">'app_homepage'</span>,
        <span class="hljs-string">'app_healthcheck'</span>,
    ];

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSubscribedEvents</span><span class="hljs-params">()</span>: <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [
            KernelEvents::REQUEST =&gt; [<span class="hljs-string">'onKernelRequest'</span>, <span class="hljs-number">0</span>],
        ];
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onKernelRequest</span><span class="hljs-params">(RequestEvent $event)</span>: <span class="hljs-title">void</span>
    </span>{
        $request = $event-&gt;getRequest();
        $route = $request-&gt;attributes-&gt;get(<span class="hljs-string">'_route'</span>);

        <span class="hljs-comment">// Allow whitelisted routes</span>
        <span class="hljs-keyword">if</span> (in_array($route, <span class="hljs-keyword">self</span>::WHITELISTED_ROUTES, <span class="hljs-keyword">true</span>)) {
            <span class="hljs-keyword">return</span>;
        }

        <span class="hljs-comment">// Deny access for non-whitelisted routes</span>
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> AccessDeniedHttpException(<span class="hljs-string">'Route not whitelisted'</span>);
    }
}</code></pre>
<p>Goal: Block all routes except the whitelist. Simple, right?</p>
<hr>
<h2 id="the-problem">The Problem</h2>
<p>Logs were showing <code>AccessDeniedHttpException</code> on routes I knew were whitelisted. Classic first move: throw a <code>dump()</code> inside the subscriber to see what was coming in.</p>
<pre><code class="language-php hljs php"><span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onKernelRequest</span><span class="hljs-params">(RequestEvent $event)</span>: <span class="hljs-title">void</span>
</span>{
    $request = $event-&gt;getRequest();
    $route = $request-&gt;attributes-&gt;get(<span class="hljs-string">'_route'</span>);

    dump($route); <span class="hljs-comment">// 🔍 Let's see what's happening</span>
    <span class="hljs-comment">// ...</span>
}</code></pre>
<p>First surprising finding: <strong>the subscriber was being called twice</strong> for a single request. The first call had the expected route, the second had <code>$route = null</code>.</p>
<p>Obvious question: <em>why is <code>_route</code> null?</em></p>
<p>I dug further with <code>dump($request-&gt;getPathInfo())</code> to see what URL was being processed on the second call:</p>
<pre><code>// 1st call
dump($request-&gt;getPathInfo()); // "/foo"

// 2nd call
dump($request-&gt;getPathInfo()); // "/foo" ← same. Wait, what?</code></pre>
<p>Same URL, called twice. That made no sense — if it was the same request, why was <code>_route</code> null the second time? I was going in circles.</p>
<p>So I dumped the full <code>$event</code> object to get more context, and narrowed it down to <code>_controller</code> in the request attributes:</p>
<pre><code class="language-php hljs php">dump($request-&gt;attributes-&gt;get(<span class="hljs-string">'_controller'</span>));
<span class="hljs-comment">// "Symfony\Component\HttpKernel\Controller\ErrorController"</span></code></pre>
<p>There it was. <code>_controller</code> wasn't pointing to my code at all. Symfony had forged a brand new request to its own <code>ErrorController</code>, reusing the original URL — which is why <code>getPathInfo()</code> was so misleading — but bypassing the router entirely. That's why <code>_route</code> was null.</p>
<hr>
<h2 id="root-cause">Root Cause</h2>
<p>The actual flow was:</p>
<pre><code>Request → /foo
  └── WhitelistSubscriber (1st call) → _route = 'app_foo' ✅ Access granted
      └── Controller → throws RealException 💥
          └── Symfony catches it
              └── Sub-request → ErrorController (bypasses router, no _route)
                  └── WhitelistSubscriber (2nd call) → _route = null ❌ AccessDenied thrown
                      └── RealException is now silenced 🔇</code></pre>
<p>The trap: <strong>the subscriber's <code>AccessDeniedHttpException</code> was completely masking the original exception</strong> — the one that actually contained the useful debug information.</p>
<p>When an exception is thrown, Symfony's <code>HttpKernel</code> dispatches a <code>KernelEvents::EXCEPTION</code> event, then delegates the error rendering to <code>ErrorController</code> via an internal sub-request. That sub-request reuses the original URL — which is why <code>getPathInfo()</code> was misleading — but it completely bypasses the routing layer, leaving <code>_route</code> as <code>null</code>.</p>
<hr>
<h2 id="the-solution">The Solution</h2>
<p>Check if the request is the main request (not a sub-request):</p>
<pre><code class="language-php hljs php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">EventSubscriber</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">EventDispatcher</span>\<span class="hljs-title">EventSubscriberInterface</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">HttpKernel</span>\<span class="hljs-title">Event</span>\<span class="hljs-title">RequestEvent</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">HttpKernel</span>\<span class="hljs-title">KernelEvents</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WhitelistRouteSubscriber</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">EventSubscriberInterface</span>
</span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">const</span> WHITELISTED_ROUTES = [
        <span class="hljs-string">'app_login'</span>,
        <span class="hljs-string">'app_homepage'</span>,
        <span class="hljs-string">'app_healthcheck'</span>,
    ];

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSubscribedEvents</span><span class="hljs-params">()</span>: <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [
            KernelEvents::REQUEST =&gt; [<span class="hljs-string">'onKernelRequest'</span>, <span class="hljs-number">0</span>],
        ];
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onKernelRequest</span><span class="hljs-params">(RequestEvent $event)</span>: <span class="hljs-title">void</span>
    </span>{
        <span class="hljs-comment">// Skip sub-requests (like error handling)</span>
        <span class="hljs-keyword">if</span> (!$event-&gt;isMainRequest()) {
            <span class="hljs-keyword">return</span>;
        }

        $request = $event-&gt;getRequest();
        $route = $request-&gt;attributes-&gt;get(<span class="hljs-string">'_route'</span>);

        <span class="hljs-comment">// Allow whitelisted routes</span>
        <span class="hljs-keyword">if</span> (in_array($route, <span class="hljs-keyword">self</span>::WHITELISTED_ROUTES, <span class="hljs-keyword">true</span>)) {
            <span class="hljs-keyword">return</span>;
        }

        <span class="hljs-comment">// Deny access for non-whitelisted routes</span>
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> AccessDeniedHttpException(<span class="hljs-string">'Route not whitelisted'</span>);
    }
}</code></pre>
<p><code>isMainRequest()</code> returns <code>false</code> for any internal sub-request — error handling, ESI fragments, <code>hinclude</code> — so your logic only runs on real, router-dispatched requests.</p>
<blockquote>
<p><strong>Note:</strong> <code>isMainRequest()</code> replaced the deprecated <code>isMasterRequest()</code> in Symfony 5.3. If you're on an older version, use <code>isMasterRequest()</code> instead.</p>
</blockquote>
<hr>
<h2 id="takeaway">Takeaway</h2>
<p>Anytime your subscriber does something destructive — throw, redirect, set a response — ask yourself: <em>what happens when Symfony calls this on a sub-request?</em></p>
<p>Sub-requests are everywhere in Symfony: error handling, ESI, fragments. They don't carry the same context as a main request, and your subscriber doesn't know the difference unless you tell it to.</p>
<p><code>isMainRequest()</code> is that check. Make it a reflex. 🎉</p>]]>
    </content>
  </entry>
</feed>
