Skip to content

Skills don't mention that 4xx responses become HTTP 500 + BlueScreen in debug mode #6

Description

@jakubboucek

Version: 1.0.7

Bug Description

None of the skills mention that in debug mode Nette\Application\BadRequestException (and its subclasses) is not rendered by the error presenter. It propagates to Tracy, which responds with HTTP 500 and a BlueScreen. So $this->error(), ForbiddenRequestException, a violated #[Requires] constraint, an unknown presenter/action, an invalid route parameter or a failed checkRequirements() all produce a 500 locally, and 404/403 + a rendered error page only in production.

This is a recurring source of confusion for an agent working on a Nette app locally: it expects 404/403, receives 500, and concludes the application is broken. It then goes looking for a bug that does not exist, in code where the framework is behaving exactly as designed.

Beyond the plain omission — catchExceptions does not appear anywhere in this repository — four passages actively push toward the wrong mental model:

  • plugins/nette/skills/tracy-debugging/SKILL.md:79 — the section is titled "BlueScreen (exceptions & fatal errors)", framing a BlueScreen as evidence of a fault. For BadRequestException the BlueScreen is intended application behavior.
  • plugins/nette/skills/tracy-debugging/SKILL.md:13"In debug mode, Tracy displays errors in the browser; in production mode, errors are logged to the log/ directory and the user sees a generic error page." This presents the difference as purely one of presentation. It does not say that the HTTP status code differs too, nor that the whole error-presenter pipeline is disabled.
  • plugins/nette/skills/nette-architecture/references/requires.md:5"When a requirement is violated, the framework throws Nette\Application\BadRequestException, so the user sees the error page." In a dev environment the user does not see the error page; they see a BlueScreen with status 500. The same implicit assumption sits behind ?? $this->error() at line 70.
  • plugins/nette/skills/nette-configuration/SKILL.md:208 — documents application: errorPresenter: 4xx/5xx without noting that the setting has no effect in debug mode, and without mentioning catchExceptions as the switch that restores it.

Two things make this particularly hard for an agent to recover from on its own:

  • The tracy-debugging skill description says "Essential when: 500 error". So the skill an agent is explicitly routed to on seeing a 500 is the one skill that omits the only correct explanation for this class of 500.
  • The signal is indistinguishable from a real fault until the exception class is read. An agent that only observes the status code — curl -w '%{http_code}', an HTTP assertion in a test, a smoke check — has no way to tell the difference at all.

It is also negative knowledge ("this is not a bug") that cannot be derived from the application codebase. There is no trace of it in app/; it lives in a framework DI extension.

Steps To Reproduce

Minimal demo on a stock nette/web-project (reproduced with nette/application 3.2.9, tracy/tracy 2.11.4, no application: section in config, so all defaults):

// app/Presentation/Home/HomePresenter.php
public function actionMissing(): void
{
	$this->error(); // or: throw new Nette\Application\ForbiddenRequestException;
}
curl -s -o /dev/null -w '%{http_code}\n' 'http://localhost:8000/?action=missing'

Observed: 500, and the response body is a Tracy BlueScreen for Nette\Application\BadRequestException. Requesting any non-existent route gives the same result.

The chain, none of it visible in application code:

  1. Bridges/ApplicationDI/ApplicationExtension.php — error presenters are only wired onto the Application service when exceptions are caught:

    if ($config->catchExceptions || !$this->debugMode) {
        $application->addSetup('$error4xxPresenter', [...]);
        $application->addSetup('$errorPresenter', [...]);
    }

    catchExceptions defaults to false, so in debug mode neither is set.

  2. Application::run() catches the exception and calls sendHttpCode($e), which correctly sets 404/403 ($e->getHttpCode() ?: 404). Then createErrorRequest() returns null because no error presenter is configured, so the exception is rethrown.

  3. Tracy\Debugger::exceptionHandler() runs @http_response_code(500); unconditionally, overwriting the status code Nette had just set.

Expected Behavior

The skills should state plainly that an expected 4xx turns into HTTP 500 + BlueScreen in debug mode, so an agent seeing that combination recognises it as normal instead of hunting a non-existent bug — and knows how to get the real status code and error page locally.

Possible Solution

A short, explicit section in tracy-debugging/SKILL.md (that is where an agent lands after seeing a 500), along these lines:

Expected 4xx responses appear as HTTP 500 + BlueScreen in debug mode

In debug mode the error presenter is not used (application: catchExceptions defaults to false, and error presenters are only wired up when it is on). Nette\Application\BadRequestException and ForbiddenRequestException therefore reach Tracy, which always responds 500.

A BlueScreen showing Nette\Application\BadRequestException is not a bug — it is a normal 404, and it would be a 404 with a rendered error page in production. ForbiddenRequestException is likewise a normal 403. Check the exception class before investigating.

Sources: $this->error(), a violated #[Requires], an unknown presenter/action, an invalid route parameter, a failed checkRequirements().

To see the real status code and the real error page locally, set application: catchExceptions: true in config/local.neon, or test against production mode.

Plus two one-line corrections:

  • nette-architecture/references/requires.md:5 — qualify "the user sees the error page" with "(in production; in debug mode the exception reaches Tracy and you get a BlueScreen with status 500)".
  • nette-configuration/SKILL.md application: snippet — note that errorPresenter is inactive in debug mode unless catchExceptions: true.

It would also help if the tracy-debugging description mentioned "unexpected 500 on a page that should return 404/403", so the skill is reached with the right question in mind.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions