Skip to content

Commit 74bed6d

Browse files
authored
Merge pull request #2613 from adobe/terragon/research-issue-2414-km0irh
fix(server): transform plain text 401/403 to Chrome-compatible HTML for sidekick (#2414)
2 parents c8286ce + 2714a63 commit 74bed6d

File tree

2 files changed

+129
-9
lines changed

2 files changed

+129
-9
lines changed

src/server/utils.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -412,15 +412,15 @@ window.LiveReloadOptions = {
412412
return;
413413
}
414414

415-
let textBody = await ret.text();
416-
textBody = `<html>
417-
<head><meta property="hlx:proxyUrl" content="${url}"></head>
418-
<body>
419-
<pre>${textBody}</pre>
420-
<p>Click <b><a href="${opts.loginPath}">here</a></b> to login.</p>
421-
</body>
422-
</html>
423-
`;
415+
// Transform plain text 401/403 responses into Chrome-compatible HTML
416+
// This allows the sidekick to recognize the error page and enable login
417+
const statusText = ret.status === 401 ? '401 Unauthorized' : '403 Forbidden';
418+
const escapedUrl = url
419+
.replace(/&/g, '&amp;')
420+
.replace(/"/g, '&quot;');
421+
422+
const textBody = `<html><head><meta name="color-scheme" content="light dark"><meta property="hlx:proxyUrl" content="${escapedUrl}"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">${statusText}</pre></body></html>`;
423+
424424
respHeaders['content-type'] = 'text/html';
425425
res
426426
.set(respHeaders)

test/server.test.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,126 @@ describe('Helix Server', () => {
561561
}
562562
});
563563

564+
it('transforms plain text 401 responses into Chrome-compatible HTML with meta tag', async () => {
565+
const cwd = await setupProject(path.join(__rootdir, 'test', 'fixtures', 'project'), testRoot);
566+
const project = new HelixProject()
567+
.withCwd(cwd)
568+
.withHttpPort(0)
569+
.withProxyUrl('http://main--foo--bar.aem.page');
570+
571+
await project.init();
572+
project.log.level = 'silly';
573+
574+
// Simulate real-world scenario: pipeline returns plain text for 401
575+
nock('http://main--foo--bar.aem.page')
576+
.get('/protected')
577+
.reply(401, 'Unauthorized', {
578+
'content-type': 'text/plain',
579+
});
580+
581+
try {
582+
await project.start();
583+
const resp = await getFetch()(`http://127.0.0.1:${project.server.port}/protected`, {
584+
cache: 'no-store',
585+
redirect: 'manual',
586+
});
587+
assert.strictEqual(resp.status, 401);
588+
assert.ok(resp.headers.get('content-type').startsWith('text/html'));
589+
590+
const body = await resp.text();
591+
// Verify Chrome-compatible structure
592+
assert.ok(body.includes('<html><head>'));
593+
assert.ok(body.includes('<meta name="color-scheme" content="light dark">'));
594+
assert.ok(body.includes('<meta property="hlx:proxyUrl" content="http://main--foo--bar.aem.page/protected">'));
595+
assert.ok(body.includes('</head><body>'));
596+
assert.ok(body.includes('<pre style="word-wrap: break-word; white-space: pre-wrap;">401 Unauthorized</pre>'));
597+
assert.ok(body.includes('</body></html>'));
598+
599+
// Verify exact structure that sidekick expects
600+
assert.ok(body.match(/<body><pre[^>]*>401 Unauthorized<\/pre><\/body>/));
601+
} finally {
602+
await project.stop();
603+
}
604+
});
605+
606+
it('transforms plain text 403 responses into Chrome-compatible HTML with meta tag', async () => {
607+
const cwd = await setupProject(path.join(__rootdir, 'test', 'fixtures', 'project'), testRoot);
608+
const project = new HelixProject()
609+
.withCwd(cwd)
610+
.withHttpPort(0)
611+
.withProxyUrl('http://main--foo--bar.aem.page');
612+
613+
await project.init();
614+
project.log.level = 'silly';
615+
616+
// Simulate real-world scenario: pipeline returns plain text for 403
617+
nock('http://main--foo--bar.aem.page')
618+
.get('/forbidden')
619+
.reply(403, 'Forbidden', {
620+
'content-type': 'text/plain',
621+
});
622+
623+
try {
624+
await project.start();
625+
const resp = await getFetch()(`http://127.0.0.1:${project.server.port}/forbidden`, {
626+
cache: 'no-store',
627+
redirect: 'manual',
628+
});
629+
assert.strictEqual(resp.status, 403);
630+
assert.ok(resp.headers.get('content-type').startsWith('text/html'));
631+
632+
const body = await resp.text();
633+
// Verify Chrome-compatible structure
634+
assert.ok(body.includes('<html><head>'));
635+
assert.ok(body.includes('<meta name="color-scheme" content="light dark">'));
636+
assert.ok(body.includes('<meta property="hlx:proxyUrl" content="http://main--foo--bar.aem.page/forbidden">'));
637+
assert.ok(body.includes('</head><body>'));
638+
assert.ok(body.includes('<pre style="word-wrap: break-word; white-space: pre-wrap;">403 Forbidden</pre>'));
639+
assert.ok(body.includes('</body></html>'));
640+
641+
// Verify exact structure that sidekick expects
642+
assert.ok(body.match(/<body><pre[^>]*>403 Forbidden<\/pre><\/body>/));
643+
} finally {
644+
await project.stop();
645+
}
646+
});
647+
648+
it('escapes special characters in URL when transforming 401 responses', async () => {
649+
const cwd = await setupProject(path.join(__rootdir, 'test', 'fixtures', 'project'), testRoot);
650+
const project = new HelixProject()
651+
.withCwd(cwd)
652+
.withHttpPort(0)
653+
.withProxyUrl('http://main--foo--bar.aem.page');
654+
655+
await project.init();
656+
project.log.level = 'silly';
657+
658+
// URL with special characters that need escaping
659+
nock('http://main--foo--bar.aem.page')
660+
.get('/path?param=value&other="quoted"')
661+
.reply(401, 'Unauthorized', {
662+
'content-type': 'text/plain',
663+
});
664+
665+
try {
666+
await project.start();
667+
const resp = await getFetch()(`http://127.0.0.1:${project.server.port}/path?param=value&other="quoted"`, {
668+
cache: 'no-store',
669+
redirect: 'manual',
670+
});
671+
assert.strictEqual(resp.status, 401);
672+
673+
const body = await resp.text();
674+
// URL is already percent-encoded by the time it reaches the server
675+
// Just verify the meta tag is present and ampersands are escaped
676+
assert.ok(body.includes('<meta property="hlx:proxyUrl" content="http://main--foo--bar.aem.page/path?param=value&amp;other=%22quoted%22">'));
677+
// Verify structure is Chrome-compatible
678+
assert.ok(body.includes('<pre style="word-wrap: break-word; white-space: pre-wrap;">401 Unauthorized</pre>'));
679+
} finally {
680+
await project.stop();
681+
}
682+
});
683+
564684
it('receives site token, saves it and uses it', async () => {
565685
const siteToken = `hlxtst_${new UnsecuredJWT({ email: '[email protected]' }).encode()}`;
566686

0 commit comments

Comments
 (0)