The firefly/kernel package provides LaraFly's product-agnostic error model. It has no Laravel or
external dependencies, so every other package (and your application) can throw and describe errors
consistently.
All framework exceptions extend Firefly\Kernel\Exception\FireflyException, which carries four pieces of
metadata beyond the message:
| Accessor | Meaning |
|---|---|
errorCode(): string |
A stable, machine-readable code (e.g. RESOURCE_NOT_FOUND). |
httpStatus(): int |
The HTTP status the web layer should emit. |
category(): ErrorCategory |
Business / Validation / Security / Infrastructure / External / Framework / Plugin / Internal. |
severity(): ErrorSeverity |
Info / Warning / Error / Critical. |
Typed subclasses fix these values. Selected examples:
| Exception | Code | Status | Category |
|---|---|---|---|
Business\ResourceNotFoundException |
RESOURCE_NOT_FOUND |
404 | Business |
Business\ConflictException |
CONFLICT |
409 | Business |
Business\PaymentRequiredException |
PAYMENT_REQUIRED |
402 | Business |
Business\ValidationException |
VALIDATION_ERROR |
422 | Validation |
Security\AuthenticationException |
AUTHENTICATION_FAILED |
401 | Security |
Security\AuthorizationException |
ACCESS_DENIED |
403 | Security |
Infrastructure\ServiceUnavailableException |
SERVICE_UNAVAILABLE |
503 | Infrastructure |
Infrastructure\RateLimitExceededException |
RATE_LIMIT_EXCEEDED |
429 | Infrastructure |
Infrastructure\DataAccessException |
DATA_ACCESS_ERROR |
500 | Infrastructure |
Infrastructure\DataIntegrityViolationException |
DATA_INTEGRITY_VIOLATION |
409 | Infrastructure |
Infrastructure\DuplicateKeyException |
DUPLICATE_KEY |
409 | Infrastructure |
Infrastructure\CannotAcquireLockException |
LOCK_NOT_ACQUIRED |
409 | Infrastructure |
Infrastructure\DeadlockLoserDataAccessException |
DEADLOCK |
409 | Infrastructure |
Infrastructure\OptimisticLockingFailureException |
OPTIMISTIC_LOCKING_FAILURE |
409 | Infrastructure |
Infrastructure\EmptyResultDataAccessException |
EMPTY_RESULT |
404 | Infrastructure |
Infrastructure\IncorrectResultSizeDataAccessException |
INCORRECT_RESULT_SIZE |
500 | Infrastructure |
Infrastructure\BadSqlGrammarException |
BAD_SQL_GRAMMAR |
500 | Infrastructure |
Infrastructure\QueryTimeoutException |
QUERY_TIMEOUT |
504 | Infrastructure |
Infrastructure\TransactionTimedOutException |
TRANSACTION_TIMED_OUT |
504 | Infrastructure |
Infrastructure\DataAccessResourceFailureException |
DATASOURCE_UNAVAILABLE |
503 | Infrastructure |
Infrastructure\TransientDataAccessResourceException |
TRANSIENT_DATA_ACCESS_FAILURE |
503 | Infrastructure |
External\ExternalServiceException |
EXTERNAL_SERVICE_ERROR |
502 | External |
The data-access rows are what firefly/data throws — its exception translation for the driver failures, and
getById(), findOneByExample(), the versioning trait and the transaction deadline for
EmptyResultDataAccessException, IncorrectResultSizeDataAccessException, OptimisticLockingFailureException
and TransactionTimedOutException respectively (see Data & Repositories). They
nest — DuplicateKeyException under DataIntegrityViolationException, DeadlockLoserDataAccessException under
CannotAcquireLockException, everything under DataAccessException except TransactionTimedOutException,
which sits under Infrastructure\TimeoutException like every other timeout (Spring's TransactionException
side, not its DataAccessException side) — so a handler catches at the granularity it needs: catch (DataAccessException $e) does not see a transaction that ran past its deadline; catch (TimeoutException $e)
does. Their messages are fixed sentences; the driver's message, with the statement in it, stays on previous.
use Firefly\Kernel\Exception\Business\ResourceNotFoundException;
throw new ResourceNotFoundException("Order {$id} not found");RFC 9457 lets a problem document carry extension members beside the standard ones, and lets title be
the problem type's own phrase rather than the status's reason phrase. Both live on FireflyException, so
any exception in the taxonomy — or any subclass you write — can carry them without a renderer of its own:
use Firefly\Kernel\Exception\Business\PaymentRequiredException;
throw (new PaymentRequiredException('The Team edition includes up to five workers.', 'EDITION_LIMIT'))
->withExtensions(['field' => 'workers', 'limit' => 5])
->withTitle('Your plan does not include this');{
"status": 402,
"title": "Your plan does not include this",
"code": "EDITION_LIMIT",
"category": "business",
"severity": "warning",
"detail": "The Team edition includes up to five workers.",
"type": "about:blank",
"field": "workers",
"limit": 5
}withExtensions()/withTitle() mutate the instance and return it, so a throw site stays one expression and
no subclass has to widen its constructor; both are also constructor arguments on FireflyException itself
for a subclass that wants to fix them. An extension can never override a standard member: ErrorResponse
writes status, title, code and the rest over the extensions, so ['status' => 999] is harmless.
The generated OpenAPI problem schema declares additionalProperties: true for the same reason.
ValidationException additionally carries Firefly\Kernel\Error\FieldError instances — the shape of Spring's
FieldError: the field path exactly as the client sent it (shipTo.postcode, lines[1].sku), a message
that describes the constraint, an optional application code, the constraint that failed by its attribute
name, and the rejectedValue:
use Firefly\Kernel\Error\FieldError;
use Firefly\Kernel\Exception\Business\ValidationException;
throw new ValidationException('Validation failed', [
new FieldError('email', 'must not be blank', constraint: 'NotBlank', rejectedValue: ''),
new FieldError('age', 'must be >= 0', 'min', -1),
]);Rendered inside the problem document's errors array as field, message, then code, constraint and
rejectedValue when present:
"errors": [
{ "field": "email", "message": "must not be blank", "constraint": "NotBlank", "rejectedValue": "" },
{ "field": "age", "message": "must be >= 0", "code": "min", "rejectedValue": -1 }
]firefly/validation's #[Valid] produces these for every declared constraint — the constraint's own sentence
(must not be blank, size must be between 1 and 50, must match "^[A-Z0-9]…") rather than Laravel's
humanised attribute (The ship to.street field is required.), unless firefly.validation.messages is
laravel; see Validation § Field errors. The HTML error
page renders the status, code and detail of a 422 and no field list.
Firefly\Kernel\Error\ErrorResponse turns any FireflyException into a problem+json payload. The web
layer (a later package) renders it; here is the shape:
use Firefly\Kernel\Error\ErrorResponse;
$response = ErrorResponse::fromException(
$exception,
instance: '/orders/42',
traceId: $traceId, // the id a person quotes: see "Web rendering" for which id that is
correlationId: $correlationId, // always the correlation id, so the two are never conflated
);
$payload = $response->toArray(); // omits null/empty optionals{
"status": 404,
"title": "Not Found",
"code": "RESOURCE_NOT_FOUND",
"category": "business",
"severity": "warning",
"detail": "Order 42 not found",
"instance": "/orders/42",
"traceId": "...",
"correlationId": "..."
}firefly/web (Web Layer) is the concrete renderer this document promised: every
FireflyException thrown while handling a request is turned into an application/problem+json response
by Firefly\Web\Exception\ProblemDetailsRenderer, via ErrorResponse::fromException(...), at the
exception's own httpStatus() — the same kernel-level ErrorResponse this page documents, built by the
same call.
The rendered document carries more than the sample above, and every extra member is one only a request can
supply. That listing is a kernel call with nothing in hand but an exception and an instance; the renderer
has the request, so it passes traceId, correlationId, a timestamp and RFC 9457 §3.1.1's type as well
— and type is on every published document (about:blank unless firefly.web.problem.type-uri says
otherwise, below), which is the one member the kernel sample above cannot show you, because nothing in it
sets one. instance is the member with a rule of its own: the renderer builds it through
Firefly\Web\Error\ProblemMapper::instanceFor(), which answers a root-relative URI reference
(/orders/42, never orders/42) — §3.1.5 makes the member a URI reference, and a relative one resolves
against the document's own base URI, so orders/42 served from /orders/42 would identify
/orders/orders/42. The four characters that would let the leading slash open an authority instead
(\, and the tab, LF and CR a URL parser deletes before it reads anything) are percent-encoded there, so
the member can never name a different origin however the request target was spelled.
Firefly\Web\Error\ProblemMapper owns the rule for turning any throwable into that shape, in one place,
because the HTML page below needs the same answer and two copies of it would eventually tell a browser and a
client different things about one failure. It has three cases, and only the third is a disclosure:
| Throwable | Status | Whose message is it? |
|---|---|---|
A FireflyException |
its own httpStatus() |
the application's, written for the client |
An HttpExceptionInterface (the router's own 404, abort(409, '…')) |
its real status | the author's, via abort() |
| Anything else | 500 INTERNAL_ERROR |
an accident, and withheld — see below |
!!! danger "A generic throwable's message is not for the client"
A QueryException stringifies the failing SQL and its bindings; a TypeError names an absolute path on
the server; a PDOException names the host it could not reach. All three were copied verbatim into
detail and published as problem+json. The problem document now has a gate of its own,
firefly.web.problem.disclose, which defaults to false and follows nothing — not app.debug, not the
HTML page's trace. For one release the JSON path shared trace, and that was the wrong gate for a machine
surface: every local and compose environment sets APP_DEBUG, and a console fed by problem+json rendered
a duplicate-key insert as the DSN, the tenant id and the full statement in a red banner while the HTML page
beside it withheld everything. With the gate off an unhandled throwable answers
An unexpected error occurred. It has been logged; quote reference <traceId> if you report it. and its
real message stays on the exception, where the log has it beside the same id. When no settings object is
bound at all — a JSON-only deployment that never constructed one — the default is the safe one; an
absent gate must not mean an open one.
firefly.web.problem.type-uri decides what RFC 9457's type says, and it has exactly three positions.
§3.1.1 makes an absent type identical to about:blank, which makes emitting it a presentation choice
rather than a conformance one — and LaraFly makes Spring ProblemDetail's choice, writing about:blank out
by default so a client reading the member always finds a string instead of having to encode the RFC's
equivalence rule. Point the key at an absolute http(s):// base and every document instead carries a
dereferenceable type derived from the stable code — https://api.example.test/problems/resource-not-found
— which is the thing LaraFly can do here that Spring cannot, because that identifier is already on every
error, in the log line and in the support ticket. Set the key to '' and the member is omitted entirely:
the pre-9457 document, byte for byte.
Anything else falls back to about:blank rather than to silence, because '' is a position an operator
takes deliberately and a typo must not be able to take it for them. The vocabulary is
ErrorPageSettings::typeUri()'s — the two sentinels and an absolute http(s):// base, edges trimmed the
way a URL parser trims them, with a relative base, a javascript:/data:/file: scheme, a
protocol-relative //host and an interior tab, LF or CR all refused. type is the one operator-supplied
URI on this surface that never reaches an href on the error page, which is exactly why it needed a guard
of its own: every API console, IDE HTTP client and documentation viewer that renders a problem document
turns the member into a link.
Two more things every problem document carries. The ids — two of them, related and never conflated.
traceId is the id a person quotes: the request's W3C trace id when tracing gave it a valid span,
and the correlation id when it did not — so pasting it into a trace search finds the request, which was the
one thing it could never do while the member named after a trace held a uuid no trace backend had heard of.
correlationId is always the correlation id, the one CorrelationIdFilter reads or mints at order
-100 and stamps on every log line, and it is on the response as X-Correlation-Id exactly as before; a
request that arrived with no id is given one rather than left unreferenced. The trace id gets its own
response header (X-Trace-Id, see firefly.web.trace-id.* below) and is written only when there is one, so
the two ids never overwrite each other and a deployment with tracing off sees no new header at all. With
tracing off, traceId and correlationId are the same string — byte for byte what the document carried
before the trace id existed.
And the headers an HttpExceptionInterface carries are copied through: a 405 keeps its Allow header,
and is rendered with the reason phrase as its title, a sentence written for a person (This address only accepts POST.) in place of
the router's, and the permitted verbs in an allowed extension member. A 404 the router raised for a URL that
matches nothing says There is nothing at this address.; an abort(404, '…') message the author wrote is
kept verbatim. A 503 carries Retry-After, and PHP's own execution-time limit (Maximum execution time of N seconds exceeded) is answered as 503 EXECUTION_TIME_EXCEEDED rather than a 500 quoting the engine: the
request was not wrong, the server stopped it.
Before that generic rendering happens, LaraFly gives the application a chance to handle the exception itself:
#[ExceptionHandler(SomeException::class)]marks a method — on the throwing#[RestController]itself, or on a#[ControllerAdvice]bean — as the renderer for a specific exception class (or any of its subclasses).- A controller-local handler (declared directly on the controller that threw) always beats a
global
#[ControllerAdvice]handler for the same exception. - Within whichever scope wins, the most-specific matching handler by class hierarchy is chosen — a handler for a more-derived exception class outranks one for an ancestor class.
- A matched handler's return value is content-negotiated like any other controller return, but rendered
at the exception's
httpStatus()rather than the route's default status. - If no handler matches at any scope, the exception propagates to the renderers described above — so an
unhandled 404/422/500 comes back as
application/problem+json, or as the LaraFly error page when the caller asked for HTML (see the next section), and never as an uncaught framework error page.
The same failure is rendered two ways, and the choice is not "is this a FireflyException". It used to be,
which meant a person clicking a stale link to /orders/999999 in a browser was shown a raw JSON blob: the
exception taxonomy that makes LaraFly's errors consistent for clients was the very thing that made them
unreadable for people.
| The caller | What it gets |
|---|---|
Named text/html (or application/xhtml+xml) in Accept |
The HTML error page |
Asked for JSON, or is an XMLHttpRequest |
application/problem+json |
Sent only a wildcard Accept — a bare curl — or no Accept, or named some other type (application/xml) |
application/problem+json |
Requested a path under firefly.web.error-page.json-paths |
application/problem+json, whatever it asked for |
The rule is the client NAMED text/html, not acceptsHtml(). A bare curl sends */*, which
acceptsHtml() answers true for, so keying off it would have turned every unadorned command-line request
against an API into an HTML page — a worse regression than the bug being fixed.
json-paths is the stronger statement and is checked first: the Accept header says who is asking, the
path says what the URL is. It defaults to api/*, because a developer opening an API URL in a browser
wants the payload their client will receive, not a styled page telling them the endpoint renders HTML.
That third row is firefly.web.error-page.problem-fallback (default true), and it covers every caller
that is neither a browser nor a JSON client: a wildcard Accept, an absent one, and a caller that named
a concrete type LaraFly renders no error in — application/xml, text/plain, image/png. The wider rule
is the consistent one. A FireflyException has always been answered with application/problem+json
whatever the Accept header said, so catching only the wildcard would hand one XML client a problem
document for a taxonomy 404 and Laravel's stock HTML page for a router 404. Errors have exactly two shapes
here: a MessageConverter you add for XML converts what a controller returns, and neither error
renderer is wired through it. Set the key to false to let all of those callers fall through to Laravel's
handler instead. Like json-paths, it is withdrawn along with the page when
firefly.web.error-page.enabled is false — that key means "use Laravel's own error page", so it stops
LaraFly adding answers rather than changing which answer is given.
Three exceptions are Laravel's own and LaraFly never answers them, whatever the table above says:
ValidationException, AuthenticationException and HttpResponseException. Laravel's handler resolves
each of them itself immediately after it has consulted LaraFly's renderer, and none of the three is a
FireflyException or carries an HTTP status of its own — so describing them would replace a 422 with its
field errors, a 401, or a response the application had already built, with an opaque 500. A failed
$request->validate() in a LaraFly application behaves exactly as it does in a plain Laravel one.
| Member | Meaning | Published behavior |
|---|---|---|
type |
Identifies the kind of problem (§3.1.1) | about:blank by default; a configured HTTP(S) base derives a URI from the stable code; '' omits the member. |
title |
Short summary of the problem type (§3.1.2) | The reason phrase or the exception's authored title. |
status |
Advisory HTTP status (§3.1.3) | An integer matching the response status. |
detail |
Explanation of this occurrence (§3.1.4) | The authored sentence or the opaque sentence, subject to the disclosure gate. |
instance |
URI reference identifying this occurrence (§3.1.5) | A root-relative request path from ProblemMapper::instanceFor(); backslash, TAB, LF and CR are percent-encoded to keep the reference on this origin. |
| Extensions | Application-specific members (§3.2) | Stable code, reference ids, timestamp, field errors and safe exception extensions. |
Migration: instance now has a leading slash. A client comparing it to api/orders/42 must expect
/api/orders/42. type is now explicit by default; setting firefly.web.problem.type-uri to '' omits
that member but does not undo the corrected instance behavior. A configured base identifies a problem
type; the application is responsible for serving documentation at that URI.
Encoding degrades safely. Invalid UTF-8 is substituted. If encoding or a serialization callback fails, the renderer reports the failure and emits a document preserving scalar metadata, safe authored details and encodable field errors and extensions. The web rendering section describes what is disclosed.
firefly/web ships a page in the same visual language as the welcome page and the admin dashboard, showing
the status, the reason, the stable error code — the same one the problem document carries, so a support
ticket quoting it finds the same code in the log — and, when permitted, the exception, its previous chain,
the source around the throwing line, and the stack trace with your frames separated from your
dependencies'.
The reference configuration ships the whole block commented out at its defaults — uncomment the keys this deployment wants to change:
// 'error-page' => [
// // Turn this off to fall back to Laravel's own error page. Default: true.
// 'enabled' => true,
// …
// 'trace' => env('APP_DEBUG', false),
//
// // The name in the page's wordmark and title. Default: `app.name`.
// 'title' => env('APP_NAME', 'LaraFly'),
//
// // How many source lines to show around a throwing line, clamped to 0-40. 0 shows none.
// // Default: 7.
// 'excerpt-lines' => 7,
// …
// 'authored-detail' => env('FIREFLY_WEB_ERROR_PAGE_AUTHORED_DETAIL', true),
// …
// 'home' => env('FIREFLY_WEB_ERROR_PAGE_HOME', '/'),
// 'sign-in' => env('FIREFLY_WEB_ERROR_PAGE_SIGN_IN', '/login'),
// 'support' => env('FIREFLY_WEB_ERROR_PAGE_SUPPORT', 'https://support.example.test'),
// 'actions' => env('FIREFLY_WEB_ERROR_PAGE_ACTIONS', true),
// …
// 'copy-button' => env('FIREFLY_WEB_ERROR_PAGE_COPY_BUTTON', true),
// …
// 'json-paths' => 'api/*,webhooks/*',
// …
// 'views' => [
// '404' => 'errors.not-found',
// 'default' => 'errors.generic',
// ],
// ],The error surfaces and their shared reference use these configuration keys:
| Key | Type | Default | Meaning |
|---|---|---|---|
firefly.web.error-page.enabled |
bool | true |
Off falls back to Laravel's own error page. It gates RENDERING, not recognition: the security entry point still knows a browser from a client. |
firefly.web.error-page.trace |
bool | app.debug |
Whether the page carries the exception, its file and line, a source excerpt and the stack trace. Enforced where the report is BUILT. |
firefly.web.error-page.title |
string | app.name |
The name in the wordmark and the <title>. |
firefly.web.error-page.excerpt-lines |
int | 7 (0–40) |
Source lines around a throwing line. |
firefly.web.error-page.max-frames |
int | 40 (1–500) |
The most frames the page BUILDS. A trim in the report, before markup; your own frames are kept first, and the header says "30 of 104 frames". |
firefly.web.error-page.json-paths |
string (CSV) | api/* |
Path patterns answered as problem+json whatever the caller asked for. Checked before the Accept header. |
firefly.web.error-page.views |
array | [] |
Your own Blade view per status, or default. Bound by the same trace gate; a view that throws falls back to the built-in page. |
firefly.web.error-page.home |
string | / |
The "Go home" target. Scheme-guarded — see below. '' offers no link. |
firefly.web.error-page.sign-in |
string | '' |
The 401's "Sign in" target. Scheme-guarded. Offered on a 401 and on nothing else. |
firefly.web.error-page.support |
string | '' |
The "Contact support" target. Scheme-guarded. |
firefly.web.error-page.actions |
bool | true |
Whether the page offers any navigation at all. |
firefly.web.error-page.copy-button |
bool | true |
The Reference cell's Copy button — the page's only script, shipped hidden and revealed by it. |
firefly.web.error-page.authored-detail |
bool | true |
Whether the production lede is the sentence problem+json publishes for the same failure. |
firefly.web.error-page.problem-fallback |
bool | true |
Whether clients asking for wildcard, absent or unsupported Accept types get problem+json; withdrawn when the page is disabled. |
firefly.web.problem.disclose |
bool | false |
Whether an UNHANDLED throwable's own message may appear in detail. Follows nothing — not app.debug, not trace. |
firefly.web.problem.type-uri |
string | about:blank |
RFC 9457 type: that literal, '' to omit the member, or a base URI from which the stable code derives one. |
firefly.web.trace-id.enabled |
bool | true |
Whether the W3C trace id is what traceId, the page's Reference row and the echoed header publish. false puts the correlation id back in all three — the pre-trace behaviour — and turns the echo off. The trace id is still read for logs and the HTTP-exchange row; only publishing it stops. |
firefly.web.trace-id.header |
string | X-Trace-Id |
The response header the trace id is echoed on, beside X-Correlation-Id and never in place of it. '' disables the echo and leaves the document and the page untouched. It is deliberately not W3C traceresponse, which LaraFly does not implement. |
trace is enforced where the data is gathered, not where it is printed. With it off the framework never
walks the stack, never opens a source file and never copies the exception message — so there is nothing
assembled for a template mistake to leak. Production shows the status, the reason, the code, one lede
sentence, a row of actions, and the request's reference — the same id the problem document publishes
as traceId, which is the W3C trace id when the request had a valid span and the correlation id when it did
not, and the same value the response echoes on X-Trace-Id — so the 500 page reads
"quote the reference below if you report it" and the id a person screenshots is the one a trace search
resolves. The page prints that id once, in a Reference cell that is user-select:all: one click
takes the whole of it, with no JavaScript and no dragging a selection across a wrapped uuid. On a 5xx the
lede points at that cell rather than spelling the id into prose a second time, which is why the page's
sentence no longer matches the problem document's word for word — a payload has no cell to point at, so it
keeps the id inline. Both surfaces still carry the same id,
and that is the part a ticket and a trace search need. A Copy button sits beside the cell wherever the
browser can honour one, behind firefly.web.error-page.copy-button: it ships hidden and is revealed by the
page's only script, so scripts off, a Content-Security-Policy that refuses inline scripts, or a plain-http
origin (navigator.clipboard is a secure-context API) leave no control rather than a dead one, and a copy the
browser refuses at click time says Copy failed instead of failing silently. When the two ids differ the page
carries a Correlation cell beside the Reference one, holding the X-Correlation-Id value; when they are
the same string the cell is omitted, because two cells repeating one value teach a reader the ids are
interchangeable. That is enough to quote into a ticket and grep in a log, and no class, no file, no trace
and no framework hint — the boundary is the raw exception and everything downstream of it, not every word
about the failure: the lede below is the sentence the application itself wrote for the caller, which
problem+json publishes as detail for the same failure. The page's own advice about how to turn traces on
is suppressed outside non-production environments too, because naming the framework and a config key to an
anonymous visitor is a free hint about your stack.
The lede is the problem document's own sentence (firefly.web.error-page.authored-detail, default
true). An abort(404, 'No such tenant.'), or a ResourceNotFoundException carrying Order 42 does not exist., says that to the person and to the client alike — one failure, one wording, whichever surface
answered. What counts as authored is ProblemMapper's decision and not this key's: below 500 only, and with
every sentence the framework generated already replaced, so a QueryException's SQL and the
route-model-binding 404s that name a model class and a primary key are never ledes. Set the key false and
the lede falls back to the generic sentence for the status — the status-and-code page — with two exceptions
that do not move. A 405 always names the verbs: "That address does not accept a GET request. It accepts
POST.", built from the Allow header the router set, so there is nothing of yours in it for the key to
withhold, and the same list is the document's allowed member. And a bare abort(403) authored nothing: the
document publishes the reason phrase because it needs some detail, and the page declines to lede with a word
already printed beside the status code.
The action row offers what fits the status, and nothing it was not given: home (default /), sign-in
and support (both empty by default — the values in the reference above are examples, and an empty one
offers no link rather than a guessed route name), and actions (default true) to switch the row off
entirely. A 401 gets Sign in; every page gets Go home and Contact support wherever those are set.
Try again is offered on a 5xx and only for a GET or a HEAD, because it is a plain link and a link is a
GET: it carries the address and the query string of the request that failed and it cannot carry the verb or
the body, so on a failed POST it would either land the reader on the 405 page or send a different request
under a label that says "again". The address it names is the request's own — base path and all, so a
deployment served under a front controller gets a link back to the URL it really serves — and it goes through
the same scheme guard as every configured href on the page, which drops javascript:, data:, a
protocol-relative //host and its backslash spelling /\host. When the guard refuses, the page makes no
retry offer at all rather than one it cannot spell truthfully.
Overriding it. views hands a status — or default — to your own Blade view. The view receives the same
$error report the built-in page gets, so it is bound by the same trace gate and cannot print a stack
trace the settings withheld. A view that throws falls back to the built-in page rather than propagating:
this renders while the application is already failing, and an override is application code (a renamed
layout, a component querying the database that is down) — a white screen at that moment is the worst
possible outcome.
It is not a Blade view itself. The built-in page is assembled as a string with no container lookups, no view factory and no network font, because the failure being explained may be the view layer. String building is not the elegant choice; it is the one that still works when nothing else does.
The stack puts your code first. Application frames precede one native dependency disclosure. Frame
summaries keep the index, package, directory, filename, line and call on one line on desktop; narrow screens
place the call on a second line. Only one source excerpt
opens at a time, using native details behavior without JavaScript. firefly.web.error-page.max-frames
limits the frames assembled, with application frames taking priority; counts disclose any truncation.
SourcePaths removes literal and realpath-normalized roots, then uses roots inferred from vendor frames.
This also handles symlinked deployments and harnesses whose application root sits inside a vendor tree.