Files
reverse-engineering-anything/tests/boundary/cli/harBrowserSerializedUrlsCli.test.ts
T
Kaoru0822andClaude Opus 5.5 c5f50e80d8 fix(captures): admit browser-serialized HAR request URLs (#8112)
* 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>
2026-10-30 01:03:35 -07:00

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",
},
],
},
});
},
);