fix(snapshots): account for malformed headers (#8241)

When browser receives multiple header values for the same header name,
we present them as LF-separated value. This is not considered valid in
Node, so we should split by LF when serving a snapshot.

There more invalid characters in headers, so just in case we try/catch it.
This commit is contained in:
Dmitry Gozman 2021-08-16 17:40:46 -07:00 committed by GitHub
parent 246495f705
commit 99993e173b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 60 additions and 38 deletions

View File

@ -183,8 +183,14 @@ export class SnapshotServer {
if (isTextEncoding && !contentType.includes('charset'))
contentType = `${contentType}; charset=utf-8`;
response.setHeader('Content-Type', contentType);
for (const { name, value } of resource.responseHeaders)
response.setHeader(name, value);
for (const { name, value } of resource.responseHeaders) {
try {
response.setHeader(name, value.split('\n'));
} catch (e) {
// Browser is able to handle the header, but Node is not.
// Swallow the error since we cannot do anything meaningful.
}
}
response.removeHeader('Content-Encoding');
response.removeHeader('Access-Control-Allow-Origin');

View File

@ -18,8 +18,9 @@ import { contextTest, expect } from './config/browserTest';
import { InMemorySnapshotter } from '../lib/server/snapshot/inMemorySnapshotter';
import { HttpServer } from '../lib/utils/httpServer';
import { SnapshotServer } from '../lib/server/snapshot/snapshotServer';
import type { Frame } from '..';
const it = contextTest.extend<{ snapshotPort: number, snapshotter: InMemorySnapshotter }>({
const it = contextTest.extend<{ snapshotPort: number, snapshotter: InMemorySnapshotter, showSnapshot: (snapshot: any) => Promise<Frame> }>({
snapshotPort: async ({}, run, testInfo) => {
await run(11000 + testInfo.workerIndex);
},
@ -35,6 +36,25 @@ const it = contextTest.extend<{ snapshotPort: number, snapshotter: InMemorySnaps
await snapshotter.dispose();
await httpServer.stop();
},
showSnapshot: async ({ contextFactory, snapshotPort }, use) => {
await use(async (snapshot: any) => {
const previewContext = await contextFactory();
const previewPage = await previewContext.newPage();
previewPage.on('console', console.log);
await previewPage.goto(`http://localhost:${snapshotPort}/snapshot/`);
const frameSnapshot = snapshot.snapshot();
await previewPage.evaluate(snapshotId => {
(window as any).showSnapshot(snapshotId);
}, `${frameSnapshot.pageId}?name=${frameSnapshot.snapshotName}`);
// wait for the render frame to load
while (previewPage.frames().length < 2)
await new Promise(f => previewPage.once('frameattached', f));
const frame = previewPage.frames()[1];
await frame.waitForLoadState();
return frame;
});
},
});
it.describe('snapshots', () => {
@ -129,7 +149,7 @@ it.describe('snapshots', () => {
expect(snapshotter.resourceContent(resource.responseSha1).toString()).toBe('button { color: blue; }');
});
it('should capture iframe', async ({ page, contextFactory, server, toImpl, browserName, snapshotter, snapshotPort }) => {
it('should capture iframe', async ({ page, server, toImpl, browserName, snapshotter, showSnapshot }) => {
it.skip(browserName === 'firefox');
await page.route('**/empty.html', route => {
@ -158,15 +178,10 @@ it.describe('snapshots', () => {
}
// Render snapshot, check expectations.
const previewContext = await contextFactory();
const previewPage = await previewContext.newPage();
await previewPage.goto(`http://localhost:${snapshotPort}/snapshot/`);
await previewPage.evaluate(snapshotId => {
(window as any).showSnapshot(snapshotId);
}, `${snapshot.snapshot().pageId}?name=snapshot${counter}`);
while (previewPage.frames().length < 3)
await new Promise(f => previewPage.once('frameattached', f));
const button = await previewPage.frames()[2].waitForSelector('button');
const frame = await showSnapshot(snapshot);
while (frame.childFrames().length < 1)
await new Promise(f => frame.page().once('frameattached', f));
const button = await frame.childFrames()[0].waitForSelector('button');
expect(await button.textContent()).toBe('Hello iframe');
});
@ -203,7 +218,7 @@ it.describe('snapshots', () => {
}
});
it('should contain adopted style sheets', async ({ page, toImpl, contextFactory, snapshotPort, snapshotter, browserName }) => {
it('should contain adopted style sheets', async ({ page, toImpl, showSnapshot, snapshotter, browserName }) => {
it.skip(browserName !== 'chromium', 'Constructed stylesheets are only in Chromium.');
await page.setContent('<button>Hello</button>');
await page.evaluate(() => {
@ -223,30 +238,19 @@ it.describe('snapshots', () => {
});
const snapshot1 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot1');
const previewContext = await contextFactory();
const previewPage = await previewContext.newPage();
previewPage.on('console', console.log);
await previewPage.goto(`http://localhost:${snapshotPort}/snapshot/`);
await previewPage.evaluate(snapshotId => {
(window as any).showSnapshot(snapshotId);
}, `${snapshot1.snapshot().pageId}?name=snapshot1`);
// wait for the render frame to load
while (previewPage.frames().length < 2)
await new Promise(f => previewPage.once('frameattached', f));
// wait for it to render
await previewPage.frames()[1].waitForSelector('button');
const buttonColor = await previewPage.frames()[1].$eval('button', button => {
const frame = await showSnapshot(snapshot1);
await frame.waitForSelector('button');
const buttonColor = await frame.$eval('button', button => {
return window.getComputedStyle(button).color;
});
expect(buttonColor).toBe('rgb(255, 0, 0)');
const divColor = await previewPage.frames()[1].$eval('div', div => {
const divColor = await frame.$eval('div', div => {
return window.getComputedStyle(div).color;
});
expect(divColor).toBe('rgb(0, 0, 255)');
await previewContext.close();
});
it('should restore scroll positions', async ({ page, contextFactory, toImpl, snapshotter, snapshotPort, browserName }) => {
it('should restore scroll positions', async ({ page, showSnapshot, toImpl, snapshotter, browserName }) => {
it.skip(browserName === 'firefox');
await page.setContent(`
@ -274,16 +278,28 @@ it.describe('snapshots', () => {
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'scrolled');
// Render snapshot, check expectations.
const previewContext = await contextFactory();
const previewPage = await previewContext.newPage();
await previewPage.goto(`http://localhost:${snapshotPort}/snapshot/`);
await previewPage.evaluate(snapshotId => {
(window as any).showSnapshot(snapshotId);
}, `${snapshot.snapshot().pageId}?name=scrolled`);
const div = await previewPage.frames()[1].waitForSelector('div');
await previewPage.frames()[1].waitForLoadState();
const frame = await showSnapshot(snapshot);
const div = await frame.waitForSelector('div');
expect(await div.evaluate(div => div.scrollTop)).toBe(136);
});
it('should handle multiple headers', async ({ page, server, showSnapshot, toImpl, snapshotter, browserName }) => {
it.skip(browserName === 'firefox');
server.setRoute('/foo.css', (req, res) => {
res.statusCode = 200;
res.setHeader('vary', ['accepts-encoding', 'accepts-encoding']);
res.end('body { padding: 42px }');
});
await page.goto(server.EMPTY_PAGE);
await page.setContent(`<head><link rel=stylesheet href="/foo.css"></head><body><div>Hello</div></body>`);
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'snapshot');
const frame = await showSnapshot(snapshot);
await frame.waitForSelector('div');
const padding = await frame.$eval('body', body => window.getComputedStyle(body).paddingLeft);
expect(padding).toBe('42px');
});
});
function distillSnapshot(snapshot) {