mirror of
https://github.com/morluto/rea.git
synced 2026-10-31 11:56:12 +00:00
* fix(captures): admit browser-serialized HAR request URLs
har-schema@2.0.0 checks request.url with ajv-formats' strict RFC 3986
`uri` format. Browsers record and send URLs as WHATWG serializes them,
which keeps characters such as `|` and `{` unescaped in the query, so one
Google Fonts or GraphQL GET entry made inspect-web-network-capture and
environment-build reject the whole HAR.
HAR 1.2 defines request.url only as the absolute request URL. Own that
correction in the HAR validator: every URL the RFC 3986 check accepts
stays accepted, and a URL that WHATWG URL serialization reproduces
unchanged is accepted too. Spellings WHATWG would rewrite (a backslash
authority, a tab, a space) and relative references still fail.
Fixes #8110
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(captures): state that RFC 3986 request URLs stay admitted
The HAR request URL rule is "passes RFC 3986, or WHATWG serialization
reproduces it unchanged". A spelling WHATWG would rewrite therefore fails
only when it also fails the RFC 3986 check; an uppercase host still
passes. Say so in the docs and the validator comment, and pin the
accepted uppercase-host case in the CLI test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
119 lines
3.9 KiB
TypeScript
119 lines
3.9 KiB
TypeScript
import { writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { expect } from "vitest";
|
|
|
|
import { historicalHar } from "../../fixtures/historicalHar.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
|
|
/** A HAR whose entries copy the fixture entry with each request URL. */
|
|
const harWithRequestUrls = (urls: readonly string[]) => {
|
|
const har = historicalHar();
|
|
const [entry] = har.log.entries;
|
|
if (entry === undefined) throw new Error("Expected a fixture HAR entry");
|
|
return {
|
|
log: {
|
|
...har.log,
|
|
entries: urls.map((url) => ({
|
|
...entry,
|
|
request: { ...entry.request, url },
|
|
})),
|
|
},
|
|
};
|
|
};
|
|
|
|
const writeHar = async (urls: readonly string[]): Promise<string> => {
|
|
const path = join(
|
|
await createTestTempDirectory("rea-har-request-url-"),
|
|
"capture.har",
|
|
);
|
|
await writeFile(path, JSON.stringify(harWithRequestUrls(urls)));
|
|
return path;
|
|
};
|
|
|
|
// Each URL is exactly what a WHATWG URL serializer emits, as browsers record
|
|
// and send it; RFC 3986 alone rejects the unescaped characters.
|
|
const BROWSER_SERIALIZED = [
|
|
"https://fonts.googleapis.com/css?family=Roboto:400,700|Open+Sans",
|
|
"https://api.example.test/graphql?variables={%22id%22:1}",
|
|
"https://api.example.test/search?q=`x`^[1]&rate=100%",
|
|
"https://api.example.test/a|b?q=c\\d",
|
|
];
|
|
|
|
cliTest(
|
|
"keeps admitting an RFC 3986 request URL that WHATWG would rewrite",
|
|
async ({ cli }) => {
|
|
const url = "https://EXAMPLE.test/a";
|
|
expect(new URL(url).href).not.toBe(url);
|
|
const har = await writeHar([url]);
|
|
const inspected = await cli.run({
|
|
arguments: ["inspect-web-network-capture", har, "har", "--json"],
|
|
});
|
|
expect(inspected.exitCode, inspected.stdout + inspected.stderr).toBe(0);
|
|
expect(inspected.json).toMatchObject({
|
|
normalized_result: { records: [{ reported: { request: { url } } }] },
|
|
});
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"inspects and builds HARs whose request URLs are browser serializations",
|
|
async ({ cli }) => {
|
|
for (const url of BROWSER_SERIALIZED)
|
|
expect(new URL(url).href, "fixture must be a WHATWG serialization").toBe(
|
|
url,
|
|
);
|
|
const har = await writeHar(BROWSER_SERIALIZED);
|
|
const inspected = await cli.run({
|
|
arguments: ["inspect-web-network-capture", har, "har", "--json"],
|
|
});
|
|
expect(inspected.exitCode, inspected.stdout + inspected.stderr).toBe(0);
|
|
expect(inspected.json).toMatchObject({
|
|
normalized_result: {
|
|
total_records: BROWSER_SERIALIZED.length,
|
|
records: BROWSER_SERIALIZED.map((url) => ({
|
|
reported: { request: { url } },
|
|
})),
|
|
},
|
|
});
|
|
const built = await cli.run({
|
|
arguments: ["environment-build", har, "--dry-run", "--json"],
|
|
timeoutMs: 60_000,
|
|
});
|
|
expect(built.exitCode, built.stdout + built.stderr).toBe(0);
|
|
expect(built.json).toMatchObject({
|
|
status: "planned",
|
|
capture: { records: BROWSER_SERIALIZED.length, input: "har" },
|
|
});
|
|
},
|
|
);
|
|
|
|
cliTest.for([
|
|
// WHATWG rewrites these spellings, so the recorded text and its reading differ.
|
|
{ name: "backslash authority", url: "http:\\\\user:pass@host.test/" },
|
|
{ name: "tab", url: "https://host.test/a\tb" },
|
|
{ name: "space", url: "https://host.test/a b" },
|
|
{ name: "relative reference", url: "/a|b" },
|
|
])(
|
|
"still rejects a request URL that is neither RFC 3986 nor a WHATWG serialization: $name",
|
|
async ({ url }, { cli }) => {
|
|
const har = await writeHar([url]);
|
|
const result = await cli.run({
|
|
arguments: ["inspect-web-network-capture", har, "har", "--json"],
|
|
});
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.json).toMatchObject({
|
|
code: "invalid_request",
|
|
details: {
|
|
issues: [
|
|
{
|
|
path: ["capture_path", "/log/entries/0/request/url"],
|
|
reason: "invalid_format",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
},
|
|
);
|