Merge pull request #2265 from navidemad/fix-a16-redirect-fragment-inherit

http: inherit request URL fragment across fragment-less redirect
This commit is contained in:
Karl Seguin
2026-04-27 18:15:16 +08:00
committed by GitHub
3 changed files with 93 additions and 1 deletions

View File

@@ -1572,7 +1572,19 @@ pub const Transfer = struct {
}
const base_url = try conn.getEffectiveUrl();
break :blk try URL.resolve(arena, std.mem.span(base_url), location.value, .{});
const resolved = try URL.resolve(arena, std.mem.span(base_url), location.value, .{});
// RFC 7231 §7.1.2: if the Location value has no fragment, the redirect
// inherits the fragment from the URI used to generate the request.
// URL.resolve follows RFC 3986 §5.3, which drops the base fragment when
// the relative ref has none, so we re-attach it here.
if (URL.getHash(resolved).len == 0) {
const original_hash = URL.getHash(transfer.url);
if (original_hash.len != 0) {
break :blk try std.mem.joinZ(arena, "", &.{ resolved, original_hash });
}
}
break :blk resolved;
};
try transfer.updateURL(url);

View File

@@ -1003,6 +1003,60 @@ test "cdp.frame: reload" {
}
}
test "cdp.frame: navigate inherits original fragment across redirect" {
// RFC 7231 §7.1.2: when a 3xx Location header has no fragment, the redirect
// inherits the fragment of the request URL.
var ctx = try testing.context();
defer ctx.deinit();
var bc = try ctx.loadBrowserContext(.{ .id = "BID-9", .url = "hi.html", .target_id = "FID-000000000X".* });
{
// Location: /redirect-target (no fragment) — must inherit #myfrag.
try ctx.processMessage(.{
.id = 40,
.method = "Page.navigate",
.params = .{ .url = "http://127.0.0.1:9582/redirect-no-fragment#myfrag" },
});
var runner = try bc.session.runner(.{});
try runner.wait(.{ .ms = 2000 });
const frame = bc.session.currentFrame() orelse unreachable;
try testing.expectEqualSlices(u8, "http://127.0.0.1:9582/redirect-target#myfrag", frame.url);
}
{
// Location: /redirect-target#target_fragment — target's fragment wins.
try ctx.processMessage(.{
.id = 41,
.method = "Page.navigate",
.params = .{ .url = "http://127.0.0.1:9582/redirect-with-fragment#requested" },
});
var runner = try bc.session.runner(.{});
try runner.wait(.{ .ms = 2000 });
const frame = bc.session.currentFrame() orelse unreachable;
try testing.expectEqualSlices(u8, "http://127.0.0.1:9582/redirect-target#target_fragment", frame.url);
}
{
// No fragment on either side — final URL has no fragment.
try ctx.processMessage(.{
.id = 42,
.method = "Page.navigate",
.params = .{ .url = "http://127.0.0.1:9582/redirect-no-fragment" },
});
var runner = try bc.session.runner(.{});
try runner.wait(.{ .ms = 2000 });
const frame = bc.session.currentFrame() orelse unreachable;
try testing.expectEqualSlices(u8, "http://127.0.0.1:9582/redirect-target", frame.url);
}
}
test "cdp.frame: addScriptToEvaluateOnNewDocument" {
var ctx = try testing.context();
defer ctx.deinit();

View File

@@ -610,6 +610,32 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void {
});
}
if (std.mem.eql(u8, path, "/redirect-no-fragment")) {
return req.respond("", .{
.status = .found,
.extra_headers = &.{
.{ .name = "Location", .value = "/redirect-target" },
},
});
}
if (std.mem.eql(u8, path, "/redirect-target")) {
return req.respond("<!DOCTYPE html><title>landed</title>", .{
.extra_headers = &.{
.{ .name = "Content-Type", .value = "text/html; charset=utf-8" },
},
});
}
if (std.mem.eql(u8, path, "/redirect-with-fragment")) {
return req.respond("", .{
.status = .found,
.extra_headers = &.{
.{ .name = "Location", .value = "/redirect-target#target_fragment" },
},
});
}
if (std.mem.eql(u8, path, "/xhr/404")) {
return req.respond("Not Found", .{
.status = .not_found,