Skip to content
C.W.K.
Stream
Lesson 03 of 04 · published

The Placeholder That Shipped

~13 min · failure, tooling, silent-failure, design

Level 0Loose Parts
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

What Went Out

A consumer was added to a template source's target list. Its variable block was not added alongside it — an ordinary omission, one line in a manifest, no error anywhere.

Substitution was implemented the obvious way: for each key in the variable map, replace it in the text. With no map, the loop had nothing to iterate, so it replaced nothing and returned the source unchanged. The deploy then wrote out a service worker whose cache was literally named after the unsubstituted token, and whose shell list still carried the bare placeholder token where a value belonged. The second one is worse than a syntax error, because it is not one: a bare token is a valid identifier, so nothing complains at parse time and the failure waits until the code runs and the name resolves to nothing.

Everything downstream was clean. The file was written, the header was correct, the commit went in, review saw a service worker that looked like every other service worker. The one path that would have failed is the app being opened with no network, which is precisely the path nobody exercises deliberately.

Why This Class Is Nastier Than It Looks

The failure has three properties that make it a member of a genuinely dangerous family:

  • The bad output is well-formed. It is a file of the right type, in the right place, with the right header. Nothing about it invites suspicion.
  • The default behavior of the tool was to produce it. Nothing was overridden and no warning was suppressed. A missing key was simply not an event.
  • The blast radius is delayed and narrow. It breaks the offline path, weeks later, for whoever happens to be offline — which in an app family used from one desk might be nobody for a long time.

Compare that to the alternative failure: the deploy refuses, printing which placeholders survived and which manifest key would supply them. Same defect, ten seconds instead of weeks, and the person who caused it is the person who sees it.

An unsubstituted placeholder is a defect, and a tool must not be able to emit one. Any templating step should assert, after substitution, that no placeholder tokens remain. The pattern is trivial to detect — placeholders are chosen to be distinctive precisely so that they are greppable — and the assertion converts an entire class of silent, delayed, well-formed corruption into an immediate refusal with a remedy.

The Fix, and the Shape of Its Message

The repair was a regular expression for the placeholder syntax, run over the rendered body before writing, and a refusal that names three things: the consumer and target that would have received the file, which placeholders survived, and the exact manifest location where the values belong.

That third element is what makes it a good failure rather than merely a loud one. A message saying "unsubstituted variables found" leaves the reader to go discover how substitution is configured. A message that names the manifest key to add turns the failure into a two-minute fix by somebody who has never read the deploy script.

Code

The silent version, and the refusal that replaces it·python
import re

TEMPLATE_VAR = re.compile(r"__KIT_[A-Z0-9_]+__")


def substitute(content: str, vars_map: dict[str, str] | None) -> str:
    """The original. With vars_map=None the loop body never runs, so
    the placeholders survive and the caller writes them to disk."""
    if vars_map:
        for key, value in vars_map.items():
            content = content.replace(key, value)
    return content


def unsubstituted(content: str) -> list[str]:
    """Placeholders still standing after substitution.

    Deliberately distinctive syntax: a placeholder must be impossible
    to confuse with real code, so that this check can be a plain
    regex over the rendered text with no parsing at all.
    """
    return sorted(set(TEMPLATE_VAR.findall(content)))


# At the deploy site, before writing THIS target:
expected_body = substitute(raw, (entry.get("vars") or {}).get(repo))

leftover = unsubstituted(expected_body)
if leftover:
    print(
        f"unsubstituted template var(s) for {repo}:{target_rel}"
        f" - {', '.join(leftover)}."
        f' Add them to "vars"."{repo}" for {entry["source"]}'
        " in the manifest.",
        file=sys.stderr,
    )
    return 2      # abort the RUN. And be honest about what that is
                  # NOT: targets written earlier in the walk stay
                  # written, because each is written as it is
                  # reached. Refusing late is not a rollback - it
                  # only stops the remaining writes.


# What the bad output looked like on disk - well-formed, wrong:
#
#   const CACHE = "__KIT_APP_SLUG__-shell-v1";   // a real cache,
#                                                // named nonsense
#   const SHELL = [__KIT_SHELL_LIST__];          // NOT a syntax error.
#                                                // A bare token is a
#                                                // valid identifier: it
#                                                // parses, then is
#                                                // undefined at RUNTIME

External links

Exercise

Take a templating or code-generation step in your project and answer one question: what does it do when a value is missing? Try it — remove a variable and run the generator. If the output is a file containing a placeholder, an empty string where a value belonged, or the word 'None', add the post-substitution assertion and make its message name where the value should be defined.
Hint
The most dangerous result is not a crash and not a placeholder — it is an empty string, because an empty string is usually valid syntax. A cache named '' or a URL of '' will deploy, run, and fail in a way that never mentions the template.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.