diff --git a/src/browser/HttpClient.zig b/src/browser/HttpClient.zig index fc217e3d..0832db1a 100644 --- a/src/browser/HttpClient.zig +++ b/src/browser/HttpClient.zig @@ -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); diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index b0aae79e..2bfb8988 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -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(); diff --git a/src/testing.zig b/src/testing.zig index e2500f81..73defeed 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -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("