Skip to content

date: 2026-06-08 tags: [csrf, webhooks, laravel, testing, todoist] status: active graduated_to:

A webhook POST in routes/web.php 419s in production — and the testing env hides it

Symptom — The Todoist webhook (POST /webhooks/todoist) would be rejected with 419 (CSRF token mismatch) on a real server, before the controller ran — yet every Pest feature test posting to it was green.

Root cause — Routes in routes/web.php go through Laravel's CSRF middleware. A server-to-server caller (Todoist) sends no CSRF token, so the POST is refused. Tests never catch this: ValidateCsrfToken short-circuits when app()->runningUnitTests() is true (i.e. the testing env), so postJson sails through regardless of whether an exemption exists.

Fix — Exempt the path in bootstrap/app.php (PR #368, SHA 1798c25): $middleware->validateCsrfTokens(except: ['webhooks/todoist']). The HMAC check in TodoistWebhookController::isValidSignature() stays the real authenticity gate — this only drops the session-CSRF check that can't apply to a webhook.

Guardtests/Feature/TodoistWebhookTest.php"exempts the webhook route from CSRF verification" forces a non-testing env (app()->detectEnvironment(fn (): string => 'production')) so CSRF actually runs: it 419s without the exemption, 204s with it. Any new public / server-to-server POST route needs both the validateCsrfTokens(except:) entry and this forced-env regression test — a plain postJson test will pass either way and prove nothing.