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:
parent
246495f705
commit
99993e173b
|
|
@ -183,8 +183,14 @@ export class SnapshotServer {
|
||||||
if (isTextEncoding && !contentType.includes('charset'))
|
if (isTextEncoding && !contentType.includes('charset'))
|
||||||
contentType = `${contentType}; charset=utf-8`;
|
contentType = `${contentType}; charset=utf-8`;
|
||||||
response.setHeader('Content-Type', contentType);
|
response.setHeader('Content-Type', contentType);
|
||||||
for (const { name, value } of resource.responseHeaders)
|
for (const { name, value } of resource.responseHeaders) {
|
||||||
response.setHeader(name, value);
|
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('Content-Encoding');
|
||||||
response.removeHeader('Access-Control-Allow-Origin');
|
response.removeHeader('Access-Control-Allow-Origin');
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,9 @@ import { contextTest, expect } from './config/browserTest';
|
||||||
import { InMemorySnapshotter } from '../lib/server/snapshot/inMemorySnapshotter';
|
import { InMemorySnapshotter } from '../lib/server/snapshot/inMemorySnapshotter';
|
||||||
import { HttpServer } from '../lib/utils/httpServer';
|
import { HttpServer } from '../lib/utils/httpServer';
|
||||||
import { SnapshotServer } from '../lib/server/snapshot/snapshotServer';
|
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) => {
|
snapshotPort: async ({}, run, testInfo) => {
|
||||||
await run(11000 + testInfo.workerIndex);
|
await run(11000 + testInfo.workerIndex);
|
||||||
},
|
},
|
||||||
|
|
@ -35,6 +36,25 @@ const it = contextTest.extend<{ snapshotPort: number, snapshotter: InMemorySnaps
|
||||||
await snapshotter.dispose();
|
await snapshotter.dispose();
|
||||||
await httpServer.stop();
|
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', () => {
|
it.describe('snapshots', () => {
|
||||||
|
|
@ -129,7 +149,7 @@ it.describe('snapshots', () => {
|
||||||
expect(snapshotter.resourceContent(resource.responseSha1).toString()).toBe('button { color: blue; }');
|
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');
|
it.skip(browserName === 'firefox');
|
||||||
|
|
||||||
await page.route('**/empty.html', route => {
|
await page.route('**/empty.html', route => {
|
||||||
|
|
@ -158,15 +178,10 @@ it.describe('snapshots', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render snapshot, check expectations.
|
// Render snapshot, check expectations.
|
||||||
const previewContext = await contextFactory();
|
const frame = await showSnapshot(snapshot);
|
||||||
const previewPage = await previewContext.newPage();
|
while (frame.childFrames().length < 1)
|
||||||
await previewPage.goto(`http://localhost:${snapshotPort}/snapshot/`);
|
await new Promise(f => frame.page().once('frameattached', f));
|
||||||
await previewPage.evaluate(snapshotId => {
|
const button = await frame.childFrames()[0].waitForSelector('button');
|
||||||
(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');
|
|
||||||
expect(await button.textContent()).toBe('Hello iframe');
|
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.');
|
it.skip(browserName !== 'chromium', 'Constructed stylesheets are only in Chromium.');
|
||||||
await page.setContent('<button>Hello</button>');
|
await page.setContent('<button>Hello</button>');
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
|
|
@ -223,30 +238,19 @@ it.describe('snapshots', () => {
|
||||||
});
|
});
|
||||||
const snapshot1 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot1');
|
const snapshot1 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot1');
|
||||||
|
|
||||||
const previewContext = await contextFactory();
|
const frame = await showSnapshot(snapshot1);
|
||||||
const previewPage = await previewContext.newPage();
|
await frame.waitForSelector('button');
|
||||||
previewPage.on('console', console.log);
|
const buttonColor = await frame.$eval('button', button => {
|
||||||
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 => {
|
|
||||||
return window.getComputedStyle(button).color;
|
return window.getComputedStyle(button).color;
|
||||||
});
|
});
|
||||||
expect(buttonColor).toBe('rgb(255, 0, 0)');
|
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;
|
return window.getComputedStyle(div).color;
|
||||||
});
|
});
|
||||||
expect(divColor).toBe('rgb(0, 0, 255)');
|
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');
|
it.skip(browserName === 'firefox');
|
||||||
|
|
||||||
await page.setContent(`
|
await page.setContent(`
|
||||||
|
|
@ -274,16 +278,28 @@ it.describe('snapshots', () => {
|
||||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'scrolled');
|
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'scrolled');
|
||||||
|
|
||||||
// Render snapshot, check expectations.
|
// Render snapshot, check expectations.
|
||||||
const previewContext = await contextFactory();
|
const frame = await showSnapshot(snapshot);
|
||||||
const previewPage = await previewContext.newPage();
|
const div = await frame.waitForSelector('div');
|
||||||
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();
|
|
||||||
expect(await div.evaluate(div => div.scrollTop)).toBe(136);
|
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) {
|
function distillSnapshot(snapshot) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue