From cbb9608f27306261f4445b721c91b020ba91da5a Mon Sep 17 00:00:00 2001 From: Kevin Rassool Date: Fri, 28 Aug 2026 14:54:07 +1000 Subject: [PATCH] fix(local): `raisely local` hangs on every request and drops page overrides Two independent bugs make `raisely local` unusable in 2.1.0. 1. Every proxied request hangs. The proxy response handler is registered as `onProxyRes`, which is the http-proxy-middleware **v2** option name. package.json pins v3.0.5, where handlers live under `on: { proxyRes }`. v3 ignores the unknown top-level option but still honours `selfHandleResponse: true`, so the response is handed to a handler that was never registered, nothing writes to `res`, and the request hangs until the client times out. This affects every URL, not just pages -- `/robots.txt` hangs too. Only `/reload` still answers, because it is a local Express route that never reaches the proxy. 2. Local page overrides silently never apply. The generated override payload is passed to `String.prototype.replace` as a REPLACEMENT STRING: output.replace(afterCampaignBootstrap, `$1${pageOverride}$2`) The payload embeds page copy verbatim, so `$1`..`$9`, `$&`, `` $` ``, `$'` and `$$` in that copy are interpreted as replacement patterns. A campaign page containing "surviving on less than $2 a day" expands `$2` to capture group 2 -- the matched ` but before later bundles (and before the small +// `if (window.campaign)` script). Edge strips +// before HTML is sent, and injecting before runs too late (React has +// already read pageSchemas). +const AFTER_CAMPAIGN_BOOTSTRAP = + /(<\/script>)(\s* but before later bundles (and before the small - // `if (window.campaign)` script). Edge strips - // before HTML is sent, and injecting before runs too late (React - // already read pageSchemas). - if (pageOverride) { - const afterCampaignBootstrap = - /(<\/script>)(\s* ` - ); - } - ), + ); + }, }) ); diff --git a/tests/local-proxy.test.js b/tests/local-proxy.test.js new file mode 100644 index 0000000..453c63f --- /dev/null +++ b/tests/local-proxy.test.js @@ -0,0 +1,148 @@ +import { describe, test } from 'vitest'; +import assert from 'node:assert/strict'; +import http from 'node:http'; +import vm from 'node:vm'; + +import express from 'express'; + +import { createCampaignProxy, injectPageOverride } from '../src/local.js'; +import { buildPageOverrideScript } from '../src/actions/pages.js'; + +/** Campaign HTML shaped like the real one: pageSchemas bootstrap, then the small window.campaign script. */ +function campaignHtml(bodyCopy = 'hello') { + return [ + 't', + ``, + '', + '', + ].join('\n'); +} + +async function listen(server) { + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + return server.address().port; +} + +describe('injectPageOverride', () => { + test('inserts the payload between the bootstrap and the window.campaign script', () => { + const out = injectPageOverride(campaignHtml(), ''); + assert.ok(out.includes('];')); + assert.ok( + out.indexOf('PAYLOAD') > out.indexOf('window.pageSchemas'), + 'payload must come after the pageSchemas bootstrap' + ); + assert.ok( + out.indexOf('PAYLOAD') < out.indexOf('if (window.campaign)'), + 'payload must come before the window.campaign script' + ); + }); + + test('returns the html untouched when there is no payload', () => { + const html = campaignHtml(); + assert.equal(injectPageOverride(html, ''), html); + }); + + // Regression: page copy legitimately contains dollar amounts. `$1`..`$9`, `$&`, + // backtick-$ and `$'` are all special in a String.replace REPLACEMENT string, so + // interpolating the payload into a template literal silently rewrites it and the + // injected `; + const out = injectPageOverride(campaignHtml(), payload); + assert.ok( + out.includes(payload), + `payload was rewritten by String.replace: ${out.slice( + out.indexOf('var copy'), + out.indexOf('var copy') + 120 + )}` + ); + }); + } + + test('a compiled page containing "$2" still yields a parseable script', () => { + // Full path: real compiled-template payload -> injection -> must still parse. + const script = buildPageOverrideScript({ + 'page-1': 'return "surviving on less than $2 a day";', + }); + const out = injectPageOverride(campaignHtml(), script); + + assert.ok(out.includes(script), 'payload must survive injection byte-for-byte'); + + const inner = script + .replace(/^\s*