I was writing a header comment for a new include prototype, and the usage example I pasted into it created a delimiter collision worth removing before the file was used. Not the logic, not a typo: a <% ... %> snippet placed a literal %> inside a file that Classic ASP scans for those boundaries before the JScript comment is parsed.
Here is the setup. I was building a drop-in feature-gate include for an existing module. It needs a good header comment, because the whole point is that other pages drop it in and call it. The whole file opens with <% and a JScript block comment, /* ... */, holding the documentation. So the natural thing to write inside that block is a worked example: here is the include line, here is how you call it. A <!--#include ...--> line and then a snippet showing <% if(!featureCheck.isEnabled('feature-name')) { ... } %>. Show people the delimiters so they know it is server script.
That snippet was the problem. A literal %> in the example could be interpreted as the end of the active server-script block before the JScript comment is understood. What looked like an inert documentation block was carrying a real template boundary token, so I removed the literal delimiter from the prototype.
Why a comment can’t protect you here
The instinct, and it is a reasonable instinct, is that a comment is inert. You wrap text in comment syntax and the engine ignores it. That is true for a lot of languages because the comment is recognized during parsing, as a unit, and its contents never get tokenized as code.
In Classic ASP, the <% and %> delimiters are recognized before the JScript or VBScript parser has any concept of the comment. The template scanner divides the source into literal output and script regions by looking for the raw delimiters; only then are those regions handed to the script engine. That ordering is why the comment cannot be assumed to neutralize a literal closing delimiter.
So when the scanner is inside a script block and hits %>, that block ends. Full stop. It does not matter that, three lines up, a human reading the source would say “obviously this is a comment, it is documentation.” The tokenizer has not reached the point where comment-ness exists. By the time anything understands a VBScript ' or a JScript // or /* */ as “ignore this,” the delimiter has already done its work and the boundaries of the script block are already set. A block comment does not save you any more than a line comment does, because both are a parsing-stage concept and the delimiter scan is upstream of parsing.
The broader diagnostic pattern is to identify which parser owns a boundary first. Other template and embedding systems can have similar edge cases, but their exact delimiter and escaping rules differ. Verify the specific engine rather than importing an ASP rule wholesale.
The fix: describe the syntax, don’t reproduce it
For this Classic ASP include, the safe documentation choice was to avoid placing the literal delimiter sequence in the file. The template scanner sees the raw boundary before JScript escape handling or comment parsing could help.
So the fix was to rewrite the usage example in prose, with the delimiters removed. Instead of pasting a runnable <% ... %> snippet, the comment now reads: “include the file, then call: if(!featureCheck.isEnabled('feature-name')) { ... }”. The <!--#include--> line and the wrapping <%/%> are gone. The header even spells out why, in the comment itself: the delimiters are kept out of the example “so the ASP parser doesn’t see them as script delimiters when this file is read by grep.” The reader still gets the include name, the call shape, the method, the argument. They just don’t get a copy-pasteable block, and that is the trade you make: the documentation describes the syntax instead of reproducing it verbatim.
That is marginally less convenient as documentation, but it is safer than leaving an unverified delimiter collision in a prototype include. The file never merged to the main branch; the rewrite removed the risk before it reached a live path.
The general rule
The general lesson is not that every template engine behaves exactly like Classic ASP. It is that a file can pass through more than one parser, and the parser that recognizes outer boundaries may run before the language parser that recognizes a comment or string. When documenting syntax inside a processed template file, verify that engine’s delimiter rules and avoid literal boundary sequences unless its documentation explicitly supports the context.
These problems tend to appear in header comments, generated help text, test fixtures, and examples embedded in processed files. When I needed to show the ASP syntax, I moved the full example to documentation outside the include and described the call shape in the header instead. That kept the source comment readable without depending on an unverified delimiter trick.
Related
- Response.Redirect Blew Up the Error Page. Server.Transfer Didn’t.: another Classic ASP execution-path surprise where the active layer matters more than a plausible surface explanation
- Why Request.Form Is Empty: FormData vs URLSearchParams in Classic ASP AJAX: a request-parsing gotcha that also rewards checking the boundary between layers