<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="https://ktherage.github.io/fr/xsl/atom.xsl" media="all"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="fr">
  <id>https://ktherage.github.io/fr/tags/debug/</id>
  <title>Kévin THÉRAGE | Expert Symfony Developer - Debug</title>
  <subtitle><![CDATA[Kévin THÉRAGE – Expert Symfony Developer. Technical blog on Symfony, PHP, web development with tutorials, best practices and expert advice for developers.]]></subtitle>
  <link href="https://ktherage.github.io/fr/tags/debug/atom.xml" rel="self" type="application/atom+xml" />
  <link href="https://ktherage.github.io/fr/tags/debug/" rel="alternate" type="text/html" />
  <updated>2026-06-18T15:17:31+00:00</updated>
  <author>
    <name>Kévin THÉRAGE</name>
    <uri>https://ktherage.github.io/</uri>
  </author>
  <entry xml:lang="fr">
    <id>https://ktherage.github.io/fr/blog/le-cache-qui-ne-fonctionnait-pas/</id>
    <title>Le cache qui ne fonctionnait pas : une histoire de sérialisation chez Symfony</title>
    <published>2026-06-10T00:00:00+00:00</published>
    <link href="https://ktherage.github.io/fr/blog/le-cache-qui-ne-fonctionnait-pas/" rel="alternate" type="text/html" />
    <content type="html">
      <![CDATA[<p>J'ai eu un bug où la mise en cache des réponses HTTP semblait fonctionner d'après les logs, pourtant aucun fichier n'apparaissait dans <code translate="no">var/http_cache/</code>. Pas de fichiers. Pas d'erreurs. Juste du silence.</p>
<h2 id="le-contexte">Le Contexte</h2>
<p>Je construis un serveur MCP pour exposer un RAG et éviter les appels API redondants pendant le développement. Le cache filesystem se place entre l'application et l'API externe que j'utilise pour construire mon RAG.</p>
<p>La chaîne de clients HTTP suit un pattern decorator classique (du plus haut au plus bas dans la chaine de décoration):</p>
<pre class="mermaid d-flex flex-column m-2 justify-content-center align-items-center">
flowchart TD
    A[Symfony\Component\HttpClient\HttpClient\ScopingHttpClient] --&gt; B
    B["CachedHttpClient (stratégie de cache perso)"] --&gt; C
    C["LoggedHttpClient (stratégie de logging perso)"] --&gt; D
    D["Symfony\Component\HttpClient\HttpClient::create()"]
</pre>
<p><code translate="no">CachedHttpClient</code> combine <code translate="no">ScopingHttpClient</code> de Symfony (pour le scoping et l'authentification auprès de l'API) avec un <code translate="no">FilesystemAdapter</code> pour persister les réponses HTTP dans <code translate="no">var/http_cache/</code>. Une classe <code translate="no">CachedResponse</code> implémente <code translate="no">ResponseInterface</code> pour que les réponses mises en cache ressemblent aux réponses fraîches.</p>
<h2 id="le-symptome-1">Le Symptôme</h2>
<pre><code translate="no">app.DEBUG: Storing Response to cache with key c3f36f73afae200bb284436334b6647f.
app.DEBUG: Response stored to cache with key c3f36f73afae200bb284436334b6647f.</code></pre>
<p>Les logs debug confirmaient les tentatives de mise en cache. Réalité : <code translate="no">var/http_cache/</code> restait vide.</p>
<div class="d-flex flex-column m-2 justify-content-center align-items-center">
    <iframe src="https://giphy.com/embed/NTur7XlVDUdqM" width="480" height="274" frameborder="0" class="giphy-embed" allowfullscreen></iframe>
    <p><a href="https://giphy.com/gifs/trump-consequences-NTur7XlVDUdqM">via GIPHY</a>
</p></div>
<h2 id="l-analyse">L'analyse</h2>
<h3 id="1-le-filesystemadapter-la-fausse-piste-qui-m-a-aide-a-comprendre">1. Le <code translate="no">FilesystemAdapter</code> la fausse piste qui m'a aidé a comprendre</h3>
<p>Donc la question qui ce posait à ce moment était, Pourquoi ? Pourquoi ça ne sauvegarde pas mes reponses en cache ?</p>
<h3 id="1-1-y-a-t-il-un-probleme-avec-le-systeme-de-fichiers">1.1. Y-a-t-il un problème avec le système de fichiers ?</h3>
<p>J'ai d'abord pensé que ça venait de <code translate="no">Symfony\Component\Cache\Adapter\FilesystemAdapter</code> et en fouillant dans les fichiers du dossier <code translate="no">vendor</code> j'ai pu constater ce qui suit :</p>
<pre class="mermaid d-flex flex-column m-2 justify-content-center align-items-center">
flowchart TD
    A["Symfony\Component\Cache\Adapter\FilesystemAdapter::save()"] --&gt; B
    B["Symfony\Component\Cache\Traits\AbstractAdapterTrait::save()"] --&gt; C
    C["Symfony\Component\Cache\Adapter\AbstractAdapter::commit()"] --&gt; D
    D["Symfony\Component\Cache\Traits\FilesystemTrait::doSave()"] --&gt; E
    E["Symfony\Component\Cache\Marshaller\DefaultMarshaller::marshall()"] --&gt; F{"calls serialize()"}
    F["Symfony\Component\Cache\Traits\FilesystemCommonTrait::write()"]
</pre>
<p><code translate="no">Symfony\Component\Cache\Traits\FilesystemTrait::doSave()</code> appelait <code translate="no">Symfony\Component\Cache\Marshaller\DefaultMarshaller::marshall()</code> qui lui utilisait <code translate="no">serialize()</code> de PHP avant d'en fournir le retour à <code translate="no">Symfony\Component\Cache\Traits\FilesystemCommonTrait::write()</code>.</p>
<p>En ouvrant la fonction, j'ai constaté que <code translate="no">FilesystemCommonTrait::write()</code> executais la fonction <code translate="no">mkdir()</code> de PHP préfixée d'un <code translate="no">@</code> qui supprimais les erreurs lié a la création du répertoire. Donc s'il y avait un problème avec mon repertoire de cache, il serait tû tout simplement. J'ai donc essayé de lancer un <code translate="no">chmod -R 777 var/http_cache/</code> mais en vain.</p>
<h3 id="1-2-y-a-t-il-un-probleme-avec-la-serialisation">1.2. Y-a-t-il un problème avec la serialisation ?</h3>
<p>Il ne me restais plus qu'à voir si le problème pouvait venir de la serialisation. J'ai donc créé un script de reproduction minimaliste pour comprendre :</p>
<pre><code class="language-bash hljs bash" translate="no">docker compose <span class="hljs-built_in">exec</span> cli sh -c <span class="hljs-string">"php -r '
require \"/srv/vendor/autoload.php\";

use App\HTTP\CachedResponse;
use Symfony\Component\HttpClient\HttpClient;

\$client = HttpClient::create();
\$response = \$client-&gt;request(\"GET\", \"https://some.api.com/foo\", [
    \"headers\" =&gt; [
        \"User-Agent\" =&gt; \"Test\",
    ],
]);

\$cached = new CachedResponse(\$response);
try {
    \$serialized = serialize(\$cached);
    echo \"Serialization OK\\n\";
} catch (\Exception \$e) {
    echo \"Serialization FAILED: \" . \$e-&gt;getMessage() . \"\\n\";
}
' 2&gt;&amp;1
# Sortie :
# Serialization FAILED: Serialization of 'Closure' is not allowed</span></code></pre>
<p>L'échec se produit pendant <code translate="no">serialize()</code> — bien avant les opérations de fichier. L'objet <code translate="no">CachedResponse</code> contenait des données non sérialisables, dans mon cas une <code translate="no">Closure</code>.</p>
<h3 id="2-localiser-l-element-non-serialisable">2. Localiser l'élément non sérialisable</h3>
<p>La nouvelle question maintenant, où est-ce que je peut avoir une <code translate="no">Closure</code> dans ma <code translate="no">CachedResponse</code>.</p>
<h3 id="2-1-cachedresponse">2.1. CachedResponse</h3>
<p>Cette classe est plutôt simple et est construite à partir d'une instance de <code translate="no">Symfony\Contracts\HttpClient\ResponseInterface</code>.</p>
<pre><code class="language-php hljs php" translate="no"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">declare</span>(strict_types=<span class="hljs-number">1</span>);

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

<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Contracts</span>\<span class="hljs-title">HttpClient</span>\<span class="hljs-title">ResponseInterface</span>;

<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CachedResponse</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">ResponseInterface</span>
</span>{
    <span class="hljs-keyword">private</span> int $statusCode;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">array</span> $headers;
    <span class="hljs-keyword">private</span> string $content;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">array</span> $toArray;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">array</span> $info;

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span><span class="hljs-params">(ResponseInterface $response)</span>
    </span>{
        <span class="hljs-keyword">$this</span>-&gt;statusCode = $response-&gt;getStatusCode();
        <span class="hljs-keyword">$this</span>-&gt;headers = $response-&gt;getHeaders();
        <span class="hljs-keyword">$this</span>-&gt;content = $response-&gt;getContent();
        <span class="hljs-keyword">$this</span>-&gt;toArray = $response-&gt;toArray();
        <span class="hljs-keyword">$this</span>-&gt;info = $response-&gt;getInfo();
    }

    <span class="hljs-comment">// ...</span>
}</code></pre>
<h3 id="2-2-procedons-par-elimination">2.2. Procédons par élimination</h3>
<p>En procédant par élimination, il ne nous reste que <code translate="no">getInfo()</code> qui peut avoir ce genre de choses à l'interieur puisque :</p>
<ul>
<li><code translate="no">statusCode</code>: retourne un <code translate="no">int</code> qui corresponds au code de statut HTTP qui est aussi un entier.</li>
<li><code translate="no">headers</code>: retourne les entêtes HTTP qui ne sont basiquement que des tableaux d'<code translate="no">int</code> ou <code translate="no">string</code>.</li>
<li><code translate="no">content</code>: retourne le corps de la réponse qui n'est qu'une <code translate="no">string</code> donc aucune <code translate="no">Closure</code> là-dedans.</li>
<li><code translate="no">toArray</code>: aurait renvoyé une exception si le <code translate="no">content</code> n'avait pas été encodé en JSON.</li>
</ul>
<p>Donc il doit y avoir quelque chose d'étrange que je n'avais pas anticipé dans <code translate="no">getInfo()</code> et inspecter <code translate="no">getInfo()</code> a révélé le coupable ce que j'ai fais via ce script :</p>
<pre><code class="language-bash hljs bash" translate="no">docker compose <span class="hljs-built_in">exec</span> cli php -r <span class="hljs-string">'
require "/srv/vendor/autoload.php";
use Symfony\Component\HttpClient\HttpClient;
\$client = HttpClient::create();
\$response = \$client-&gt;request("GET", "https://api.github.com/repos/symfony/symfony/pulls/64552", [
    "headers" =&gt; ["Accept" =&gt; "application/vnd.github+json", "User-Agent" =&gt; "Test"],
]);
foreach (\$response-&gt;getInfo() as \$k =&gt; \$v) {
    if (\$v instanceof \\Closure) echo "\$k =&gt; Closure\\n";
}
'</span>
<span class="hljs-comment"># Sortie :</span>
pause_handler =&gt; Closure</code></pre>
<p>Le client HTTP de Symfony inclut une clé <code translate="no">pause_handler</code> dans <code translate="no">getInfo()</code> contenant une <code translate="no">Closure</code> utilisée en interne pour la logique de retry (gestion des <code translate="no">429 Too Many Requests</code> avec <code translate="no">Retry-After</code>) or PHP ne peut pas sérialiser les <code translate="no">Closure</code>.</p>
<h3 id="3-pourquoi-l-echec-etait-silencieux">3. Pourquoi l'échec était silencieux</h3>
<p>Trois couches ont occulté la rééle cause :</p>
<p><strong>Couche 1 — Valeur de retour ignorée / Mon erreure</strong></p>
<p>Mon erreure a été de ne pas vérifier le retour de la fonction <code translate="no">save()</code> qui, je ne l'avais pas noté à ce moment là, retourne un booléen indiquant si l'enregistrement a bien été effectué.</p>
<p>Je suis donc passé de :</p>
<pre><code class="language-php hljs php" translate="no"><span class="hljs-keyword">$this</span>-&gt;cache-&gt;save($cacheItem); <span class="hljs-comment">// returns false, ignored</span>
<span class="hljs-keyword">$this</span>-&gt;logger-&gt;debug(<span class="hljs-string">"Response stored to cache with key {$key}."</span>);</code></pre>
<p>à:</p>
<pre><code class="language-php hljs php" translate="no">$saved = <span class="hljs-keyword">$this</span>-&gt;cache-&gt;save($cacheItem);
<span class="hljs-keyword">$this</span>-&gt;logger-&gt;debug(<span class="hljs-string">"Cache save: {result}"</span>, [<span class="hljs-string">'result'</span> =&gt; $saved ? <span class="hljs-string">'success'</span> : <span class="hljs-string">'FAILED'</span>]);</code></pre>
<p>Ce qui m'a permis d'avoir des logs plus pertinent avec un message <code translate="no">Cache save: FAILED</code> qui était affiché à chaque tentatives.
La méthodes <code translate="no">save()</code> ne fonctionnait pas et donc mon cache n'avais jamais fonctionné.</p>
<p><strong>Couche 2 — Gestion d'exception silencieuse dans le marshallage</strong></p>
<p><code translate="no">serialize()</code> plantais silencieusement parce que dans <code translate="no">Symfony\Component\Cache\Marshaller\DefaultMarshaller::marshall()</code> en interne et par défaut Symfony attrape les exceptions de sérialisation et peuple un tableau d'id avec les serialisations échouées.</p>
<p>Voici une version simplifiée de la fonction :</p>
<pre><code class="language-php hljs php" translate="no"><span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">marshall</span><span class="hljs-params">(array $values, ?array &amp;$failed)</span>: <span class="hljs-title">array</span>
</span>{
    $serialized = $failed = [];

    <span class="hljs-keyword">foreach</span> ($values <span class="hljs-keyword">as</span> $id =&gt; $value) {
        <span class="hljs-keyword">try</span> {
            $serialized[$id] = serialize($value);
        } <span class="hljs-keyword">catch</span> (\<span class="hljs-keyword">Exception</span> $e) {
            <span class="hljs-keyword">if</span> (<span class="hljs-keyword">$this</span>-&gt;throwOnSerializationFailure) {
                <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> \ValueError($e-&gt;getMessage(), <span class="hljs-number">0</span>, $e);
            }
            $failed[] = $id;
        }
    }

    <span class="hljs-keyword">return</span> $serialized;
}</code></pre>
<p>Avec <code translate="no">throwOnSerializationFailure</code> par défaut à <code translate="no">false</code>, les exceptions sont avalées. La clé de cache échouée va dans <code translate="no">$failed</code>, mais aucun avertissement n'est émis.</p>
<p><strong>Couche 3 — Sérialisation complète d'un tableau de donneés mixte</strong></p>
<p>Stocker complètement un tableau de données mixte dans le cache a été une autre de mes erreurs, <em>quoique à moitié la mienne je n'avais pas anticipé la <code translate="no">Closure</code> dans <code translate="no">getInfo()</code></em>, avec le recule tout n'est pas peut-être pas bon a conserver.</p>
<h2 id="la-correction">La Correction</h2>
<p><strong>Valider les résultats de l'opération de cache:</strong></p>
<pre><code class="language-php hljs php" translate="no"><span class="hljs-keyword">if</span> (!\<span class="hljs-keyword">$this</span>-&gt;cache-&gt;save(\$cacheItem)) {
    \<span class="hljs-keyword">$this</span>-&gt;logger-&gt;warning(<span class="hljs-string">'Failed to save response to cache'</span>, [<span class="hljs-string">'key'</span> =&gt; \$key]);
    <span class="hljs-keyword">return</span>;
}
\<span class="hljs-keyword">$this</span>-&gt;logger-&gt;debug(<span class="hljs-string">"Response stored to cache with key {\$key}."</span>);</code></pre>
<p><strong>Filtrer les <code translate="no">\Closure</code> de <code translate="no">getInfo()</code>:</strong></p>
<pre><code class="language-php hljs php" translate="no">\<span class="hljs-keyword">$this</span>-&gt;info = array_filter(
    \$response-&gt;getInfo(),
    <span class="hljs-keyword">static</span> fn (\$v) =&gt; !\$v <span class="hljs-keyword">instanceof</span> \Closure
);</code></pre>
<h2 id="prevention">Prévention</h2>
<p>Ce bug n'était pas un problème avec le cache de Symfony — il fonctionnait comme conçu. L'échec venait de trois négligences alignées :</p>
<ol>
<li>
<p><strong>Oublier de vérifier les valeurs de retour</strong><br>
Les méthodes retournent des valeurs pour une raison. Traite les méthodes avec <code translate="no">bool</code> comme <code translate="no">save()</code> comme des contrats.</p>
</li>
<li>
<p><strong>Négliger la gestion d'exception silencieuse</strong><br>
Les frameworks priorisent parfois le silence sur la visibilité. Saisis où basculer la verbosité (<code translate="no">throwOnSerializationFailure: true</code> en dev).</p>
</li>
<li>
<p><strong>Supposer que <code translate="no">getInfo()</code> ne contient que des données scalaires</strong><br>
Des internals comme <code translate="no">pause_handler</code> peuvent fuir dans les métadonnées. Valide toujours ce que tu caches.</p>
</li>
</ol>]]>
    </content>
  </entry>
  <entry xml:lang="fr">
    <id>https://ktherage.github.io/fr/blog/ubuntu-25-10-docker-java-cgroupv2-crash/</id>
    <title>Ubuntu 25.10 : La mise à jour qui a brické mes conteneurs Docker Java</title>
    <published>2026-04-28T00:00:00+00:00</published>
    <updated>2026-06-18T00:00:00+00:00</updated>
    <link href="https://ktherage.github.io/fr/blog/ubuntu-25-10-docker-java-cgroupv2-crash/" rel="alternate" type="text/html" />
    <content type="html">
      <![CDATA[<h2 id="la-mise-a-jour-tranquille-qui-tourne-au-cauchemar">La mise à jour tranquille qui tourne au cauchemar</h2>
<p>C'était un vendredi soir, comme les autres. Je décide enfin de faire ce que tout bon développeur évite : <strong>mettre à jour son OS</strong>. Ubuntu 24.04 LTS → 25.10, la petite mise à jour de routine. <em>"Ça ne peut que s'améliorer"</em>, me dis-je avec ce pessimisme propre à ceux qui ont vu trop de mises à jour mal se passer (Ubuntu 24.10 je te vois !).</p>
<p>Je lance la mise à jour en confiance. Tout se passe bien. Redémarrage. Tout fonctionne. Nickel.</p>
<p>Sauf que le lundi suivant, mes tests E2E ne passent plus. Le conteneur Docker <code translate="no">selenium/standalone-chrome:4.5.3</code> qui fonctionnait parfaitement la veille refuse désormais de démarrer. Il plante en boucle avec ce magnifique message d'erreur :</p>
<pre><code translate="no">chrome-1  | java.lang.reflect.InvocationTargetException
chrome-1  |     at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
chrome-1  |     at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
chrome-1  |     at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
chrome-1  |     at java.base/java.lang.reflect.Method.invoke(Method.java:566)
chrome-1  |     at org.openqa.selenium.grid.Bootstrap.runMain(Bootstrap.java:77)
chrome-1  |     at org.openqa.selenium.grid.Bootstrap.main(Bootstrap.java:70)
chrome-1  | Caused by: java.lang.NullPointerException
chrome-1  |     at java.base/jdk.internal.platform.cgroupv2.CgroupV2Subsystem.getInstance(CgroupV2Subsystem.java:81)
chrome-1  |     at java.base/jdk.internal.platform.CgroupSubsystemFactory.create(CgroupSubsystemFactory.java:113)
chrome-1  |     at java.base/jdk.internal.platform.CgroupMetrics.getInstance(CgroupMetrics.java:167)
chrome-1  |     at java.base/jdk.internal.platform.SystemMetrics.instance(SystemMetrics.java:29)
chrome-1  |     at java.base/jdk.internal.platform.Metrics.systemMetrics(Metrics.java:58)
chrome-1  |     at java.base/jdk.internal.platform.Container.metrics(Container.java:43)
chrome-1  |     at jdk.management/com.sun.management.internal.OperatingSystemImpl.&lt;init&gt;(OperatingSystemImpl.java:182)
chrome-1  |     at jdk.management/com.sun.management.internal.PlatformMBeanProviderImpl.getOperatingSystemMXBean(PlatformMBeanProviderImpl.java:281)
chrome-1  |     at jdk.management/com.sun.management.internal.PlatformMBeanProviderImpl$3.nameToMBeanMap(PlatformMBeanProviderImpl.java:198)
chrome-1  |     at java.management/java.lang.management.ManagementFactory.lambda$getPlatformMBeanServer$0(ManagementFactory.java:487)
chrome-1  |     at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:271)
chrome-1  |     at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:177)
chrome-1  |     at java.base/java.util.HashMap$ValueSpliterator.forEachRemaining(HashMap.java:1693)
chrome-1  |     at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
chrome-1  |     at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
chrome-1  |     at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
chrome-1  |     at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
chrome-1  |     at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
chrome-1  |     at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:497)
chrome-1  |     at java.management/java.lang.management.ManagementFactory.getPlatformMBeanServer(ManagementFactory.java:488)
chrome-1  |     at org.openqa.selenium.grid.jmx.JMXHelper.register(JMXHelper.java:29)
chrome-1  |     at org.openqa.selenium.grid.server.BaseServerOptions.&lt;init&gt;(BaseServerOptions.java:48)
chrome-1  |     at org.openqa.selenium.grid.commands.Standalone.createHandlers(Standalone.java:124)
chrome-1  |     at org.openqa.selenium.grid.TemplateGridServerCommand.asServer(TemplateGridServerCommand.java:41)
chrome-1  |     at org.openqa.selenium.grid.commands.Standalone.execute(Standalone.java:245)
chrome-1  |     at org.openqa.selenium.grid.TemplateGridCommand.lambda$configure$4(TemplateGridCommand.java:129)
chrome-1  |     at org.openqa.selenium.grid.Main.launch(Main.java:83)
chrome-1  |     at org.openqa.selenium.grid.Main.go(Main.java:57)
chrome-1  |     at org.openqa.selenium.grid.Main.main(Main.java:42)
chrome-1  |     ... 6 more</code></pre>
<p><strong>Ma première pensée :</strong></p>
<div class="d-flex flex-row m-2 justify-content-center">
  <iframe src="https://giphy.com/embed/4ZxicT7ZQYcLShHOiz" width="480" height="274" style="" frameborder="0" class="giphy-embed" allowfullscreen></iframe>
</div>
<blockquote>
<p>Je suis développeur PHP, pas Java.</p>
</blockquote>
<p><strong>Mon reflexe:</strong></p>
<div class="d-flex flex-row m-2 justify-content-center">
  <iframe src="https://giphy.com/embed/pUVOeIagS1rrqsYQJe" width="480" height="288" style="" frameborder="0" class="giphy-embed" allowfullscreen></iframe>
</div>
<blockquote>
<p>Demandons à plus fort que soi. Gemini cricket (🤖🦗) 🥲.</p>
</blockquote>
<h2 id="l-enquete">L'Enquête</h2>
<p>Mon ami le cricket met le doigt sur <strong>cgroupv2</strong> dans la ligne <code translate="no">CgroupV2Subsystem.java:81</code> et fait le lien avec la mise à jour de mon système. Mais <strong>cgroupv2</strong>, c'est quoi ?</p>
<p>🤖🦗:</p>
<blockquote>
<p>Pour faire court, c'est ce qui permet à Docker 🐋 de limiter le CPU ou la RAM d'un container.</p>
</blockquote>
<p>Concrètement :</p>
<ul>
<li>Docker dit à la JVM : <em>"Tu as le droit à 2Go de RAM"</em>.</li>
<li>Java lit ces infos dans les <strong>cgroups</strong> (fichiers de gestion des ressources).</li>
<li>Java ajuste son comportement (mémoire Heap, etc.) en conséquence.</li>
</ul>
<p><strong>C'est censé être une bonne chose.</strong> Cela évite que Java ne se fasse abattre par le <em>OOM Killer</em> du système hôte. Mais Java doit parser ces fichiers, et c'est là que le bât blesse.</p>
<blockquote>
<p><strong>Pourquoi un crash et pas juste une erreur ?</strong>
Dans le code source des anciennes JVM, si le chemin retourné par l'interface système n'est pas exactement celui attendu, la variable <code translate="no">mountPoint</code> reste à <code translate="no">null</code>. La JVM tente ensuite d'appeler une méthode sur cet objet inexistant. C'est l'arroseur arrosé : la fonction censée protéger votre application devient la cause de son exécution sommaire.</p>
</blockquote>
<h2 id="le-plot-twist-la-difference-subtile-entre-ubuntu-24-04-et-25-10">Le Plot Twist : La différence subtile entre Ubuntu 24.04 et 25.10</h2>
<p>C'est ici que l'affaire devient fascinante.</p>
<p>Le véritable coupable, c'est l'évolution de systemd (passé en version 258 sur la version d'Ubuntu 25.10). Depuis la v256, systemd impose un 'durcissement' de la hiérarchie cgroup v2. Il ne se contente plus d'exposer les contrôleurs ; il les organise de façon beaucoup plus granulaire pour isoler les services. Les anciennes versions de Java, conçues à une époque où la hiérarchie était plus prévisible et moins protégée, se retrouvent littéralement 'aveugles' face à cette nouvelle structure.</p>
<p><strong>Et devinez quoi ?</strong> La vieille logique d'initialisation de Java (avant Java 17) est beaucoup trop rigide pour comprendre ce nouveau format.</p>
<p>Au démarrage, le Java de notre conteneur fouille dans le système, ne trouve pas le contrôleur "memory" exactement là où il l'attendait, et assigne silencieusement null à sa variable interne. À la ligne suivante, le code tente d'appeler la méthode .getMountPoint() sur cet objet vide.</p>
<p><strong>BOOM</strong>. NullPointerException. Mort instantanée du processus.</p>
<p>Le coupable n'était ni notre code, ni notre configuration Docker, mais une ancienne JVM incapable de s'adapter à la nouvelle hiérarchie d'un noyau Linux moderne. Normale aussi vous me direz.</p>
<h2 id="la-solution-2">LA Solution</h2>
<p>La solution propre, celle qu'on devrait toujours utiliser en production :</p>
<pre><code class="language-bash hljs bash" translate="no"><span class="hljs-comment"># Mettre à jour vers une version récente de l'image</span>
docker pull selenium/standalone-chrome:4.20.0

<span class="hljs-comment"># OU utiliser une image avec Java 17+</span>
docker pull selenium/standalone-chrome:latest</code></pre>
<h2 id="le-hack-de-survie-desactiver-usecontainersupport">Le Hack de survie — Désactiver UseContainerSupport</h2>
<p>Si vous ne pouvez pas mettre à jour l'image (contraintes legacy, validation QA, etc.), vous pouvez désactiver la détection de conteneur :</p>
<pre><code class="language-bash hljs bash" translate="no"><span class="hljs-comment"># Option 1 : Via variable d'environnement Docker</span>
docker run -d \
  -e JAVA_OPTS=<span class="hljs-string">"-XX:-UseContainerSupport"</span> \
  selenium/standalone-chrome:4.5.3

<span class="hljs-comment"># Option 2 : Via docker-compose.yml</span>
services:
  chrome:
    image: selenium/standalone-chrome:4.5.3
    environment:
      - JAVA_OPTS=-XX:-UseContainerSupport</code></pre>
<aside class="note note-warning"><p><strong>Attention:</strong> Si vous cummulez plusieurs options, pensez a les séparer par des espaces. Exemple : <code translate="no">JAVA_OPTS="SOME_EXISTING_OPTIONS -XX:-UseContainerSupport</code></p></aside>
<aside class="note note-caution"><p><strong>⚠️ Avertissement important :</strong></p>
<ul>
<li><strong>NE FAITES PAS ÇA EN PRODUCTION.</strong>, dans mon cas il s'agit d'un container de <strong>developpement</strong> en locale.</li>
<li>Sans <code translate="no">UseContainerSupport</code>, Java ne connaît pas ses limites et peut se faire killer par le OOM Killer du système hôte.</li>
<li>Cette solution est un <strong>pansement temporaire</strong> en attendant la mise à jour.</li>
</ul></aside>
<h2 id="la-morale-de-cette-histoire">La morale de cette histoire</h2>
<p>Nos écosystèmes logiciels sont <strong>fragiles</strong>. Une simple mise à jour du noyau Linux — via une mise à jour d'Ubuntu dans mon cas — peut casser des conteneurs qui fonctionnaient parfaitement d'une version à l'autre.</p>
<p><strong>Les conseils philosophiques :</strong></p>
<blockquote>
<p><em>Penser à nettoyer sa chambre régulièrement (je suis sûr que vous comprenez l'image) peut faire gagner beaucoup de temps.</em></p>
</blockquote>
<blockquote>
<p><em>Ne faites jamais de mise à jour d'OS un vendredi.</em></p>
</blockquote>
<blockquote>
<p><em>Prenez soin de toujours tester vos conteneurs Docker sur un environnement de staging après une mise à jour système.</em></p>
</blockquote>
<blockquote>
<p><em>"Works on my machine" — jusqu'à la mise à jour du kernel.</em></p>
</blockquote>
<h2 id="sources-et-references">Sources et Références</h2>
<ul>
<li><a href="https://docs.oracle.com/en/java/javase/17+containers/" rel="noopener noreferrer">Oracle Docs : Java Container Support</a> </li>
<li><a href="https://ubuntu.com/blog/ubuntu-25-10" rel="noopener noreferrer">Ubuntu 25.10 Release Notes</a></li>
<li><a href="https://docker-java.readthedocs.io/" rel="noopener noreferrer">Docker &amp; Java : Best Practices</a></li>
<li><a href="https://www.kernel.org/doc/Documentation/cgroup-v2.txt" rel="noopener noreferrer">cgroup v2 kernel documentation</a></li>
<li><a href="https://systemd.io/" rel="noopener noreferrer">Systemd News : Changes in unified cgroup hierarchy handling (v256+)</a></li>
</ul>]]>
    </content>
  </entry>
  <entry xml:lang="fr">
    <id>https://ktherage.github.io/fr/blog/my-eventsubscriber-silenced-errors/</id>
    <title>Mon EventSubscriber masquait les erreurs, voici pourquoi</title>
    <published>2026-04-13T00:00:00+00:00</published>
    <link href="https://ktherage.github.io/fr/blog/my-eventsubscriber-silenced-errors/" rel="alternate" type="text/html" />
    <content type="html">
      <![CDATA[<p>Un ticket Jira est apparu : <em>« Il y a un bug étrange qui empêche les utilisateurs d'accéder à une page à ce moment-là de la journée. »</em>
Les logs indiquaient plusieurs fois : <em>« [ce jour T cette heure] request.ERROR: Uncaught PHP Exception Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException: "Access denied to that resource." at WhitelistSubscriber.php line 99 »</em></p>
<p>Je n'avais aucune idée au début… 😅 Voici comment j'ai compris.</p>
<hr>
<h2 id="la-configuration-1">La configuration</h2>
<p>J'avais un EventSubscriber qui vérifiait l'accès aux pages basé sur une liste blanche de routes. C'était du code legacy — le refactoriser n'était pas à l'ordre du jour à ce moment-là.</p>
<pre><code class="language-php hljs php" translate="no"><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>Objectif : Bloquer toutes les routes sauf la liste blanche. Simple, non ?</p>
<hr>
<h2 id="le-probleme-1">Le problème</h2>
<p>Les logs montraient des <code translate="no">AccessDeniedHttpException</code> sur des routes que je savais être dans la liste blanche. Premier geste classique : mettre un <code translate="no">dump()</code> dans le subscriber pour voir ce qui arrivait.</p>
<pre><code class="language-php hljs php" translate="no"><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">// 🔍 Voyons ce qui se passe</span>
    <span class="hljs-comment">// ...</span>
}</code></pre>
<p>Première découverte surprenante : <strong>le subscriber était appelé deux fois</strong> pour une seule requête. Le premier appel avait la route attendue, le second avait <code translate="no">$route = null</code>.</p>
<p>Question évidente : <em>pourquoi <code translate="no">_route</code> est-il null ?</em></p>
<p>J'ai creusé plus loin avec <code translate="no">dump($request-&gt;getPathInfo())</code> pour voir quelle URL était traitée lors du second appel :</p>
<pre><code translate="no">// 1er appel
dump($request-&gt;getPathInfo()); // "/foo"

// 2e appel
dump($request-&gt;getPathInfo()); // "/foo" ← identique. Attends, quoi ?</code></pre>
<p>Même URL, appelée deux fois. Cela n'avait aucun sens — si c'était la même requête, pourquoi <code translate="no">_route</code> était-il null la deuxième fois ? Je tournais en rond.</p>
<p>J'ai donc dumpé l'objet <code translate="no">$event</code> complet pour avoir plus de contexte, et j'ai réduit le champ à <code translate="no">_controller</code> dans les attributs de la requête :</p>
<pre><code class="language-php hljs php" translate="no">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>Voilà. <code translate="no">_controller</code> ne pointait pas vers mon code du tout. Symfony avait forgé une toute nouvelle requête vers son propre <code translate="no">ErrorController</code>, réutilisant l'URL d'origine — ce qui explique pourquoi <code translate="no">getPathInfo()</code> était si trompeur — mais en contournant complètement le routeur. C'est pourquoi <code translate="no">_route</code> était null.</p>
<hr>
<h2 id="cause-racine-1">Cause racine</h2>
<p>Le flux réel était :</p>
<pre><code translate="no">Requête → /foo
  └── WhitelistSubscriber (1er appel) → _route = 'app_foo' ✅ Accès accordé
      └── Controller → lève RealException 💥
          └── Symfony l'attrape
              └── Sous-requête → ErrorController (contourne le routeur, pas de _route)
                  └── WhitelistSubscriber (2e appel) → _route = null ❌ AccessDenied levé
                      └── RealException est maintenant silencieuse 🔇</code></pre>
<p>Le piège : <strong>l'<code translate="no">AccessDeniedHttpException</code> du subscriber masquait complètement l'exception originale</strong> — celle qui contenait réellement les informations de débogage utiles.</p>
<p>Lorsqu'une exception est levée, le <code translate="no">HttpKernel</code> de Symfony distribue un événement <code translate="no">KernelEvents::EXCEPTION</code>, puis délègue le rendu de l'erreur à <code translate="no">ErrorController</code> via une sous-requête interne. Cette sous-requête réutilise l'URL d'origine — ce qui explique pourquoi <code translate="no">getPathInfo()</code> était trompeur — mais elle contourne complètement la couche de routage, laissant <code translate="no">_route</code> à <code translate="no">null</code>.</p>
<hr>
<h2 id="la-solution-1">La solution</h2>
<p>Vérifiez si la requête est la requête principale (pas une sous-requête) :</p>
<pre><code class="language-php hljs php" translate="no"><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 translate="no">isMainRequest()</code> retourne <code translate="no">false</code> pour toute sous-requête interne — gestion d'erreurs, fragments ESI, <code translate="no">hinclude</code> — donc votre logique ne s'exécute que sur les vraies requêtes distribuées par le routeur.</p>
<blockquote>
<p><strong>Note :</strong> <code translate="no">isMainRequest()</code> a remplacé l'ancien <code translate="no">isMasterRequest()</code> dans Symfony 5.3. Si vous êtes sur une version plus ancienne, utilisez <code translate="no">isMasterRequest()</code> à la place.</p>
</blockquote>
<hr>
<h2 id="conclusion-1">Conclusion</h2>
<p>Chaque fois que votre subscriber fait quelque chose de destructeur — lever une exception, rediriger, définir une réponse — demandez-vous : <em>que se passe-t-il quand Symfony appelle ceci sur une sous-requête ?</em></p>
<p>Les sous-requêtes sont omniprésentes dans Symfony : gestion d'erreurs, ESI, fragments. Elles n'ont pas le même contexte qu'une requête principale, et votre subscriber ne connaît pas la différence à moins que vous ne lui disiez.</p>
<p><code translate="no">isMainRequest()</code> est cette vérification. Faites-en un réflexe. 🎉</p>]]>
    </content>
  </entry>
</feed>
