Delivery

When your CDN rewrites your protected JavaScript

Everyone who integrates a protection step learns the ordering rule early: protect last, because anything that runs afterwards is operating on code it was not designed to read. What almost nobody checks is whether anything actually does run afterwards. The build is not the end of the pipeline. Between your artifact and the browser sit a content delivery network, possibly a hosting platform, possibly a proxy, and each of them may be quietly configured to transform JavaScript on the way through.

The symptom that starts this investigation

It always arrives in the same shape. The protected build passes locally. It passes in CI. A colleague serves the dist folder from a static server and everything works. Then it goes to production and something is wrong — a silent failure, a console error pointing at a line that does not exist in your output, an integrity mismatch, or a feature that works on one environment and not another that is supposedly identical.

The instinct is to suspect the protection settings, and that instinct sends people on long bisections through transform options. Before doing any of that, spend ten minutes proving whether the file the browser received is the file you built. It usually is not, and if it is, you have eliminated an entire category and can bisect with confidence.

The ten-minute check: compare the delivered bytes

Your protection step already recorded what it produced. A release manifest lists, per file, the SHA-256 of the input and the SHA-256 of the output. That output digest is the ground truth for what left your build.

Fetch the file from production with a plain HTTP client rather than a browser — browsers can apply their own handling, and you want the raw response body — then hash it and compare:

curl -s https://example.com/assets/app.9f2c1b.js -o /tmp/delivered.js
sha256sum /tmp/delivered.js
# compare against outputSha256 for that file in your release manifest

Two outcomes, and both are useful. If the digests match, the delivery path is exonerated and the problem is genuinely in your build or your code. If they differ, you have found something important: the code running on your customers’ machines is not the code you protected, tested and signed. Diff the two files and the responsible feature usually identifies itself immediately.

Who transforms JavaScript after the build

The list is longer than most teams expect, and the common thread is that these features are enabled at the account, site or hosting-plan level, often by whoever set up the domain, and they apply to everything by default.

Network-level minification. An option in most CDN dashboards that compresses HTML, CSS and JavaScript in transit. It was designed for hand-written scripts on sites without a build pipeline, and it is genuinely useful there. Applied to a modern bundle it achieves almost nothing, because your bundler already minified. Applied to a protected bundle it is a second minifier running over output that no minifier was written to expect.

Deferred script loaders. Features that improve perceived load time by rewriting script tags into a form the browser will not execute immediately, then running them later under their own scheduler. This does not change your code’s text so much as the environment and ordering it executes in, which is a different and subtler failure class.

Page-speed proxy modules. Server or proxy modules that inline small scripts, combine files, rewrite URLs, add cache-busting fingerprints and re-minify. They sit in the request path and rewrite responses on the fly.

Hosting and CMS optimisation plugins. On platforms where a site owner installs performance plugins, combining and minifying JavaScript is a headline feature. If you ship JavaScript that customers self-host, this is not hypothetical — it is the default state of a large fraction of installations, which is why the plugin distribution guide treats it as a first-class constraint.

Corporate proxies and security appliances. Less common but real. Inspecting proxies have been known to reformat or re-encode script bodies, and the failure appears for one customer, on one network, in a way nobody else can reproduce.

Why protected code is more sensitive than ordinary code

Any of these layers can break any JavaScript. Protected bundles trip over them more often, for reasons that follow directly from what protection does.

Integrity checks are the point. If you use subresource integrity, the browser compares a hash of the delivered bytes against the attribute you wrote. Any rewrite invalidates it and the browser refuses to execute the file, which looks alarming and is actually the system working correctly. Self-checking runtime defences behave analogously: code that verifies its own text will notice a reformatter.

Evaluated code does not get renamed with everything else. Two protection options route code through runtime evaluation. If a second minifier runs over that output, it renames identifiers in the surrounding scope but cannot see inside the string that will be evaluated later, so the reference and the declaration stop agreeing. The symptom is an undefined-variable error naming an identifier that appears nowhere in your source. It is a genuinely confusing failure, and it is entirely caused by the extra minification pass.

Directives are position-sensitive. A strict-mode directive is only a directive when it is the first statement in its scope. Tools that reorder, wrap or concatenate can demote it to a harmless string expression, at which point silent mode changes appear in code that relied on strict semantics. This is the same fragility discussed in the strict-mode article, arriving from the delivery side instead of the build side.

Generated names collide when files are combined. Protection emits helper functions and lookup tables. Concatenating two independently protected files can bring two sets of generated names into one scope. Your bundler would have kept them apart; a naive concatenation at the edge does not know it needs to.

The output is unusual by construction. Very long string arrays, deeply nested expressions, dense control flow. Parsers that are fine with everything they normally see can hit recursion limits or performance cliffs on this, and some optimisation layers respond by silently passing the file through half-processed.

Execution order: the failure with no byte difference

The byte comparison has one blind spot, and it is worth naming because it is the case that wastes the most time. A deferred-loading feature may leave your file’s contents identical while changing the tag that loads it and therefore when it runs.

What that disturbs: code that expects to initialise before other page scripts; runtime guards that sample the environment as the page loads and now sample it after other code has already modified globals; anti-tampering monitors that establish a baseline of built-in functions and now establish it after a third-party script has wrapped them; and anything that assumed a particular document readiness state.

The diagnosis is straightforward once you think to look. View the delivered HTML source and check whether your script tag still has its original type and attributes. If a loader has rewritten it, that is your answer, and the fix is to exclude your protected bundles from the feature rather than to weaken the guards.

The configuration you actually want

For paths that serve protected JavaScript, the target state is simple: deliver the exact bytes the build produced.

  • Disable network-level minification for JavaScript. Your bundler already did it, better, with the module graph in hand.
  • Disable script combination and inlining. If you want fewer requests, do it in the bundler before protection.
  • Disable deferred-execution rewriting for your bundles, or exclude them explicitly if the feature supports an exclusion list.
  • Keep transport compression on. Compression is applied and reversed around the content; it does not alter the bytes the browser parses.
  • Use hashed filenames and immutable caching, which is the right pattern for protected output anyway — see obfuscation and long-term caching.
  • Add integrity attributes to first-party bundles. They turn an invisible rewrite into a loud, immediate failure.

Then make the verification permanent. A post-deployment step that fetches each protected asset and compares its digest to the manifest takes seconds in CI and converts an entire category of production mystery into a failed pipeline stage. It also catches partial deployments, stale edge cache entries and the occasional truncated upload, so it earns its place regardless.

If you ship code other people host

Everything above assumes you control the delivery configuration. Vendors of plugins, widgets, embedded SDKs and self-hosted applications do not. Your customer’s hosting stack will minify and combine your files, and no amount of documentation prevents all of it.

Three practical adjustments. Keep runtime self-checks conservative, because a customer’s optimisation plugin will trip an aggressive one and generate a support ticket rather than catching an attacker. Ship files with names and structures that survive combination — avoid depending on being the only script in a scope. And write the requirement down plainly in your integration documentation, next to the supported browser list, so that when a ticket does arrive the first question has an answer already prepared.

The short version

Protection is the last transformation your code should receive, and the build is not where the pipeline ends. Before bisecting protection options over a production-only failure, fetch the deployed file and compare its digest with the one your release manifest recorded. If the bytes differ, an optimisation layer is rewriting your code and the fix is in a dashboard rather than a config file. If the bytes match but the behaviour still differs, look at execution order and script tag rewriting next. Then automate the digest comparison so the question never has to be asked twice.

Frequently asked questions

Why does our protected bundle work locally but fail in production?

The most common cause is that something between your build output and the browser transformed the file again. Content delivery networks, hosting platforms and optimisation plugins routinely minify, combine, defer or rewrite JavaScript in transit, and those features are frequently enabled at the account level by someone who was not thinking about your release. The decisive test is a byte comparison: fetch the deployed URL, hash it, and compare that hash with the one your build manifest recorded for the same file. If they differ, your code is not the code you shipped.

How do I tell whether a CDN modified my JavaScript file?

Download the file straight from the production URL with a plain HTTP client rather than a browser, and compute its SHA-256. Compare that against the output digest your protection step recorded for that file in its release manifest. Matching digests clear the delivery path entirely and send you back to your own code. Differing digests identify the problem precisely, and diffing the two files usually names the responsible feature within seconds because rewrites leave obvious fingerprints such as changed script type attributes or re-wrapped code.

Does a CDN minifier break obfuscated code?

Frequently, and the failures are unusually hard to read. A second minifier running over already-protected output faces long string arrays, deep expressions and generated helper functions that no ordinary source contains, and some of them time out or bail on it. Worse, if any protection option routes code through runtime evaluation, the second minifier renames identifiers in the surrounding scope without touching the string that will be evaluated later, so the names stop matching. Directive placement is another casualty. Run one minifier, before protection, and disable the network-level one.

Why did subresource integrity start failing after we enabled a CDN feature?

Because subresource integrity is doing its job. The integrity attribute pins the hash of the exact bytes you expect, and any transformation in transit changes those bytes, so the browser refuses the file. This is the single clearest signal that a delivery-layer rewrite exists, and it is a good reason to add integrity attributes even to first-party scripts. The fix is to stop the rewriting, not to remove the attribute.

Can a script loader that defers execution break runtime protection?

It can, because it changes when your code runs and what already exists when it does. Loaders of this kind rewrite script tags so the browser does not execute them normally, then run them later under their own scheduler. Code that expects to run before other page scripts, guards that sample the environment at load time, and integrity checks that assume a particular document state can all behave differently. If a protected bundle misbehaves only in production and the deployed bytes match your build, look at execution order next.

Is it safe to let a hosting optimisation plugin combine our protected files?

Combining is riskier than it looks. Concatenating protected files can collide generated helper names between builds, change the order in which top-level code runs, and break assumptions that each file made about its own scope. If you need fewer requests, do the combining in your bundler before protection, where the tooling understands module boundaries. Then exclude the result from any downstream combination feature.

What is the right configuration for serving protected JavaScript?

Serve the exact bytes you built. Turn off minification, script combination, deferred loading rewrites and any HTML or JavaScript optimisation feature for the paths that carry protected files, keep compression at the transport layer where it does not alter content, and treat the file as immutable with a hashed filename and a long cache lifetime. Then verify once, after deployment, that the delivered digest matches the manifest.

Related reading