fix(mail): the image proxy refused pictures without saying so

Both refusal paths returned the 1x1 placeholder and logged nothing at all. The
reader sees the same transparent GIF whichever way it failed — deliberately,
because an error that distinguishes "refused" from "timed out" hands back the
port-scan result the fetcher's rules exist to deny — but that opacity was
extended to the operator by accident.

The cost showed up chasing an Amazon QR code that would not render. The logs
had nine outbound fetches, all 200, and nothing whatsoever about the tenth: the
signature check had refused it before a request existed, so ImageProxyFetcher
never logged either, and the only symptom was one picture in one mail that
quietly did not appear.

The response is unchanged. Only the server now says why.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul LütznerandClaude Opus 5 committed 2026-08-27 00:37:51 +02:00
1 parent d2ddd08848
commit 0215197db8
1 file changed
+16
@@ -6,6 +6,7 @@ namespace App\Controller\Mail;
use App\Service\Mail\ImageProxyFetcher;
use App\Service\Mail\ImageProxySigner;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Request;
@@ -56,6 +57,7 @@ final class ImageProxyController extends AbstractController
public function __construct(
private readonly ImageProxySigner $signer,
private readonly ImageProxyFetcher $fetcher,
private readonly LoggerInterface $logger,
) {}
#[Route('/mail/image-proxy', name: 'app_mail_image_proxy', methods: ['GET'])]
@@ -65,12 +67,26 @@ final class ImageProxyController extends AbstractController
$signature = (string) $request->query->get('s', '');
if ('' === $url || false === $this->signer->isValid($url, $signature)) {
// Without this line the refusal is completely invisible. The reader
// gets the same 1x1 GIF as every other failure (deliberately, see
// above), no fetch is attempted, so ImageProxyFetcher never logs
// either -- and the only symptom is one picture in one mail that
// quietly does not appear, with nothing anywhere to say why.
$this->logger->info('ImageProxyController: refused an unsigned or badly signed image', [
'url' => $url,
'hasSignature' => '' !== $signature,
]);
return $this->placeholder();
}
$image = $this->fetcher->fetch($url);
if (null === $image) {
// The fetcher logs the reason it gave up; this is the line that ties
// that reason to the picture the reader did not get.
$this->logger->info('ImageProxyController: no image to serve', ['url' => $url]);
return $this->placeholder();
}