fix: bound social preview smoke response
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { PERFORMANCE_BUDGETS } from './lib/bundle-budgets.mjs';
|
||||
|
||||
const sha256Pattern = /^[a-f0-9]{64}$/u;
|
||||
const privateIdentifiers = /(?:simpastil|simpasteel|neon[ -]?debugger|yourhealth)/iu;
|
||||
@@ -144,6 +145,56 @@ async function request(fetchImpl, url, init = {}) {
|
||||
return response;
|
||||
}
|
||||
|
||||
async function cancelResponseBody(response) {
|
||||
try { await response.body?.cancel(); } catch { /* Preserve the validation error that required cancellation. */ }
|
||||
}
|
||||
|
||||
async function countBoundedResponseBody(response, label, maxBytes) {
|
||||
const contentLengthHeader = response.headers.get('content-length');
|
||||
let declaredBytes = null;
|
||||
if (contentLengthHeader !== null) {
|
||||
if (!/^(?:0|[1-9][0-9]*)$/u.test(contentLengthHeader)) {
|
||||
await cancelResponseBody(response);
|
||||
throw new Error(`${label} content-length is malformed.`);
|
||||
}
|
||||
declaredBytes = Number(contentLengthHeader);
|
||||
if (!Number.isSafeInteger(declaredBytes)) {
|
||||
await cancelResponseBody(response);
|
||||
throw new Error(`${label} content-length is outside the safe integer range.`);
|
||||
}
|
||||
if (declaredBytes === 0) {
|
||||
await cancelResponseBody(response);
|
||||
throw new Error(`${label} response is empty.`);
|
||||
}
|
||||
if (declaredBytes > maxBytes) {
|
||||
await cancelResponseBody(response);
|
||||
throw new Error(`${label} exceeds the ${maxBytes}-byte project-still limit.`);
|
||||
}
|
||||
}
|
||||
if (response.body === null) throw new Error(`${label} response body is missing.`);
|
||||
|
||||
const reader = response.body.getReader();
|
||||
let receivedBytes = 0;
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
receivedBytes += value.byteLength;
|
||||
if (receivedBytes > maxBytes) {
|
||||
try { await reader.cancel(); } catch { /* Preserve the byte-limit validation error. */ }
|
||||
throw new Error(`${label} exceeds the ${maxBytes}-byte project-still limit.`);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
if (receivedBytes === 0) throw new Error(`${label} response is empty.`);
|
||||
if (declaredBytes !== null && receivedBytes !== declaredBytes) {
|
||||
throw new Error(`${label} content-length does not match the streamed byte count.`);
|
||||
}
|
||||
return receivedBytes;
|
||||
}
|
||||
|
||||
export async function verifyProductionSmoke(options) {
|
||||
const canonical = rootHttpsUrl(options.canonicalUrl, 'Canonical URL');
|
||||
if (!sha256Pattern.test(options.expectedManifestDigest)) throw new Error('Expected signed manifest digest must be lowercase SHA-256.');
|
||||
@@ -182,7 +233,11 @@ export async function verifyProductionSmoke(options) {
|
||||
if (socialImageType === null || !new RegExp(`^${publicMetadata.socialImageType}(?:;|$)`, 'u').test(socialImageType)) {
|
||||
throw new Error('Production social image content-type is missing or unsupported.');
|
||||
}
|
||||
if ((await socialImageResponse.arrayBuffer()).byteLength === 0) throw new Error('Production social image response is empty.');
|
||||
await countBoundedResponseBody(
|
||||
socialImageResponse,
|
||||
'Production social image',
|
||||
PERFORMANCE_BUDGETS.projectStill,
|
||||
);
|
||||
for (const path of ['/robots.txt', '/sitemap.xml']) {
|
||||
const response = await request(fetchImpl, new URL(path, canonical));
|
||||
if (response.status !== 200) throw new Error(`${path} did not return 200.`);
|
||||
|
||||
Reference in New Issue
Block a user