// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// What to send after an answer: nothing more, or the next hop of a redirect.
priv enum Next {
Stop
Again(
verb~ : @method.Method,
url~ : @moonurl.Url,
fields~ : Array[(String, String)],
body~ : Body
)
}
///|
/// Whether an answer is a redirect this client follows, and what it sends next (RFC 9110
/// ยง15.4).
///
/// A 303, and a 301 or 302 answering a POST, turn into a GET without a body, as browsers and
/// every client in use do. Credentials stay with their origin: `authorization` and `cookie` are
/// dropped when a redirect leaves it, and a jar supplies the cookies the new origin has.
fn next_hop(
hop : Int32,
limit : Int32,
status : Int32,
location : String?,
verb : @method.Method,
url : @moonurl.Url,
fields : Array[(String, String)],
body : Body,
) -> Next raise Refused {
guard limit > 0 &&
status is (301 | 302 | 303 | 307 | 308) &&
location is Some(location) else {
return Stop
}
if hop == limit {
raise Redirects(limit~)
}
let next = url.resolve(parse(location))
check(next, location)
let to_get = (status == 303 && !(verb is Head)) ||
(status is (301 | 302) && verb is Post)
let (verb, body, fields) = if to_get {
(
@method.Method::Get,
Body::Empty,
without(fields, ["content-type", "content-length"]),
)
} else {
(verb, body, fields)
}
let fields = if Origin::of(next).key() == Origin::of(url).key() {
fields
} else {
without(fields, ["authorization", "cookie"])
}
Again(verb~, url=next, fields~, body~)
}