Reproduced on @stacksjs/stx 0.2.605. #1854 described this and is closed COMPLETED; the failure is present again, and one detail is now worse: with failOnViolation: true there is no log line at all, so nothing points at the cause.
The repro
A single prohibited call on a public marketing page, strict: { enabled: true, failOnViolation: true }:
@extends('layouts/marketing')
@section('content')
<script>
const probe = document.getElementById('nope')
</script>
...| baseline | with the violation | |
|---|---|---|
| HTTP | 200 | 200 |
| bytes | 38329 | 9396 |
<h1> | present | gone |
</footer> | present | gone |
| dev server log | — | nothing |
The <body> the visitor receives:
[Error processing layout: [STX] DOM API violation in resources/views/features/scheduling.stx
(composed into resources/layouts/marketing.stx): ✗ document.getElementById() is prohibited
(script line 2) → Use useRef("name") instead ...]So the whole page is replaced by an error string that leaks source paths and the project's directory layout, at HTTP 200, with no server-side signal.
Why nothing is logged — the part that is new
dist/script-validation.js ends:
if (strictConfig.failOnViolation)
throw Error(baseMessage);
else
console.warn(baseMessage);It is either/or. Turning the flag on does not add a throw to the warning, it replaces the warning. A/B on two cold-started dev servers, same planted violation, nothing else changed:
failOnViolation | server log | HTTP |
|---|---|---|
false | [STX] DOM API violation in resources/views/blog.stx … | 200 |
true | (silent) | 200 |
true is therefore strictly less observable than false — the opposite of what the flag is for.
Why it is swallowed
dist/process.js:416-425:
} catch (error) {
if (error instanceof StxValidationError)
throw error; // 417
const enhancedError = new StxRuntimeError(`Template processing failed: ${msg}`, …);
errorLogger.log(enhancedError, { filePath, context });
devHelpers.logDetailedError(enhancedError, { … });
if (options.debug)
throw enhancedError;
return errorRecovery.createFallbackContent("Template Processing", enhancedError);
}Line 417 is the intended escape hatch and it never fires, because validateClientScript throws a plain Error. The violation falls through to createFallbackContent, which is what the visitor gets.
Confirmed by calling the renderer directly, outside any server:
const out = await processDirectives(src, {}, 'scratch/probe.stx',
{ strict: { enabled: true, failOnViolation: true } }, new Set())
// NO THROW — processDirectives returned 332 charsThe class for this already exists and is never used
StxValidationError carries isFatal = true and defaults its code to DOM_API_VIOLATION — it was written for exactly this path:
class StxValidationError extends StxError {
isFatal = true
constructor(…) { super(e, "stx_VALIDATION_ERROR", …, a || t.DOM_API_VIOLATION); … }
}Every occurrence of it in the shipped package:
dist/errors/types.js class StxValidationError extends StxError {
dist/errors/types.d.ts declaration
dist/errors/index.js re-export
dist/errors/index.d.ts re-export
dist/process.js:24 import { … StxValidationError } from "./error-handling.js"
dist/process.js:417 if (error instanceof StxValidationError)grep -rhoE "new (StxValidationError|hr)\(" dist/*.js → no matches. It is defined, exported, imported and type-checked against, and never constructed anywhere. The rethrow branch at process.js:417 is dead code.
Ask
validateClientScriptshould throwStxValidationError, notError, soprocess.js:417rethrows it andfailOnViolationis actually fatal. This looks like the one-line fix the branch was written for.- Warn and throw, rather than either/or. A fatal error whose message only reaches the response body is not a diagnostic — log it server-side on both settings.
- A render failure must not return 200. #1854 asked for this and it still does;
createFallbackContentoutput should carry a 500 so health gates and crawlers see it. - Never put
createFallbackContent's text in a production response — it contains absolute-ish source paths and the project's directory structure. - A regression test: a page with one prohibited call,
failOnViolation: true, asserting the render throws rather than resolving.
Note for anyone hitting this now
allowPatterns is rule-global (#2049), but the inline form is per-line and per-rule and does work in 0.2.605:
// stx-strict-ignore-next-line location.assign
location.assign('/login')The directive targets the next line only — a comment between it and the statement silently shifts the target and the violation still fires.
Related: #1854 (same swallow, closed), #2049 (allowPatterns is rule-global), #1911 (comment false positives blocking failOnViolation).