chore: support range requests in trace viewer http server (#10434)
This commit is contained in:
parent
206a877cea
commit
85197e68c9
|
|
@ -37,13 +37,13 @@ export async function showTraceViewer(traceUrl: string, browserName: string, hea
|
||||||
const relativePath = url.pathname.slice('/trace'.length);
|
const relativePath = url.pathname.slice('/trace'.length);
|
||||||
if (relativePath.startsWith('/file')) {
|
if (relativePath.startsWith('/file')) {
|
||||||
try {
|
try {
|
||||||
return server.serveFile(response, url.searchParams.get('path')!);
|
return server.serveFile(request, response, url.searchParams.get('path')!);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const absolutePath = path.join(__dirname, '..', '..', '..', 'webpack', 'traceViewer', ...relativePath.split('/'));
|
const absolutePath = path.join(__dirname, '..', '..', '..', 'webpack', 'traceViewer', ...relativePath.split('/'));
|
||||||
return server.serveFile(response, absolutePath);
|
return server.serveFile(request, response, absolutePath);
|
||||||
});
|
});
|
||||||
|
|
||||||
const urlPrefix = await server.start(port);
|
const urlPrefix = await server.start(port);
|
||||||
|
|
|
||||||
|
|
@ -80,22 +80,77 @@ export class HttpServer {
|
||||||
return this._urlPrefix;
|
return this._urlPrefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
serveFile(response: http.ServerResponse, absoluteFilePath: string, headers?: { [name: string]: string }): boolean {
|
serveFile(request: http.IncomingMessage, response: http.ServerResponse, absoluteFilePath: string, headers?: { [name: string]: string }): boolean {
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(absoluteFilePath);
|
|
||||||
response.statusCode = 200;
|
|
||||||
const contentType = mime.getType(path.extname(absoluteFilePath)) || 'application/octet-stream';
|
|
||||||
response.setHeader('Content-Type', contentType);
|
|
||||||
response.setHeader('Content-Length', content.byteLength);
|
|
||||||
for (const [name, value] of Object.entries(headers || {}))
|
for (const [name, value] of Object.entries(headers || {}))
|
||||||
response.setHeader(name, value);
|
response.setHeader(name, value);
|
||||||
response.end(content);
|
if (request.headers.range)
|
||||||
|
this._serveRangeFile(request, response, absoluteFilePath);
|
||||||
|
else
|
||||||
|
this._serveFile(response, absoluteFilePath);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_serveFile(response: http.ServerResponse, absoluteFilePath: string) {
|
||||||
|
const content = fs.readFileSync(absoluteFilePath);
|
||||||
|
response.statusCode = 200;
|
||||||
|
const contentType = mime.getType(path.extname(absoluteFilePath)) || 'application/octet-stream';
|
||||||
|
response.setHeader('Content-Type', contentType);
|
||||||
|
response.setHeader('Content-Length', content.byteLength);
|
||||||
|
response.end(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
_serveRangeFile(request: http.IncomingMessage, response: http.ServerResponse, absoluteFilePath: string) {
|
||||||
|
const range = request.headers.range;
|
||||||
|
if (!range || !range.startsWith('bytes=') || range.includes(', ') || [...range].filter(char => char === '-').length !== 1) {
|
||||||
|
response.statusCode = 400;
|
||||||
|
return response.end('Bad request');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the range header: https://datatracker.ietf.org/doc/html/rfc7233#section-2.1
|
||||||
|
const [startStr, endStr] = range.replace(/bytes=/, '').split('-');
|
||||||
|
|
||||||
|
// Both start and end (when passing to fs.createReadStream) and the range header are inclusive and start counting at 0.
|
||||||
|
let start: number;
|
||||||
|
let end: number;
|
||||||
|
const size = fs.statSync(absoluteFilePath).size;
|
||||||
|
if (startStr !== '' && endStr === '') {
|
||||||
|
// No end specified: use the whole file
|
||||||
|
start = +startStr;
|
||||||
|
end = size - 1;
|
||||||
|
} else if (startStr === '' && endStr !== '') {
|
||||||
|
// No start specified: calculate start manually
|
||||||
|
start = size - +endStr;
|
||||||
|
end = size - 1;
|
||||||
|
} else {
|
||||||
|
start = +startStr;
|
||||||
|
end = +endStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle unavailable range request
|
||||||
|
if (Number.isNaN(start) || Number.isNaN(end) || start >= size || end >= size || start > end) {
|
||||||
|
// Return the 416 Range Not Satisfiable: https://datatracker.ietf.org/doc/html/rfc7233#section-4.4
|
||||||
|
response.writeHead(416, {
|
||||||
|
'Content-Range': `bytes */${size}`
|
||||||
|
});
|
||||||
|
return response.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sending Partial Content: https://datatracker.ietf.org/doc/html/rfc7233#section-4.1
|
||||||
|
response.writeHead(206, {
|
||||||
|
'Content-Range': `bytes ${start}-${end}/${size}`,
|
||||||
|
'Accept-Ranges': 'bytes',
|
||||||
|
'Content-Length': end - start + 1,
|
||||||
|
'Content-Type': mime.getType(path.extname(absoluteFilePath))!,
|
||||||
|
});
|
||||||
|
|
||||||
|
const readable = fs.createReadStream(absoluteFilePath, { start, end });
|
||||||
|
readable.pipe(response);
|
||||||
|
}
|
||||||
|
|
||||||
async serveVirtualFile(response: http.ServerResponse, vfs: VirtualFileSystem, entry: string, headers?: { [name: string]: string }) {
|
async serveVirtualFile(response: http.ServerResponse, vfs: VirtualFileSystem, entry: string, headers?: { [name: string]: string }) {
|
||||||
try {
|
try {
|
||||||
const content = await vfs.read(entry);
|
const content = await vfs.read(entry);
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,7 @@ export function startHtmlReportServer(folder: string): HttpServer {
|
||||||
if (relativePath.startsWith('/trace/file')) {
|
if (relativePath.startsWith('/trace/file')) {
|
||||||
const url = new URL('http://localhost' + request.url!);
|
const url = new URL('http://localhost' + request.url!);
|
||||||
try {
|
try {
|
||||||
return server.serveFile(response, url.searchParams.get('path')!);
|
return server.serveFile(request, response, url.searchParams.get('path')!);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -202,7 +202,7 @@ export function startHtmlReportServer(folder: string): HttpServer {
|
||||||
if (relativePath === '/')
|
if (relativePath === '/')
|
||||||
relativePath = '/index.html';
|
relativePath = '/index.html';
|
||||||
const absolutePath = path.join(folder, ...relativePath.split('/'));
|
const absolutePath = path.join(folder, ...relativePath.split('/'));
|
||||||
return server.serveFile(response, absolutePath);
|
return server.serveFile(request, response, absolutePath);
|
||||||
});
|
});
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue