use "net"
use uri = "uri"
primitive _Suppressing
class RedirectFollower is HTTPClientLifecycleEventReceiver
"""
Follows HTTP redirects transparently by sitting in the callback path
between an HTTPClientConnection and the user's receiver.
The connection delivers callbacks to the follower; the follower handles
redirects internally and forwards only non-redirect events to the user.
The user's callback code sees only the final non-redirect response.
Same-origin redirects reuse the existing TCP connection. Cross-origin
redirects use a RedirectConnectionFactory to create a new one.
Use `none()` as the field default so that `this` is `ref` in the actor
constructor body, then replace with `create()`:
```pony
actor MyClient is (HTTPClientConnectionActor & RedirectFollowerNotify)
var _http: RedirectFollower = RedirectFollower.none()
new create(auth: TCPConnectAuth, ssl_ctx: SSLContext val,
host: String, port: String)
=>
let config = ClientConnectionConfig
let conn = HTTPClientConnection.ssl(
auth, ssl_ctx, host, port, this, config)
let factory =
{ref(target: uri.URI val)
(auth, ssl_ctx, config, client = this)
: HTTPClientConnection
=>
let o = Origin.from_uri(target)
if o.host.size() == 0 then
return HTTPClientConnection.none()
end
if o.secure then
HTTPClientConnection.ssl(
auth, ssl_ctx, o.host, o.port, client, config)
else
HTTPClientConnection(
auth, o.host, o.port, client, config)
end
}
_http = RedirectFollower(conn, this, factory, 5,
Origin(true, host, port))
fun ref _http_client_connection(): HTTPClientConnection =>
_http.connection()
```
"""
var _conn: HTTPClientConnection
let _receiver: RedirectFollowerNotify ref
let _factory: RedirectConnectionFactory
var _remaining: USize
var _origin: Origin
var _last_request: (HTTPRequest val | None) = None
var _pending: (Redirect | _Suppressing | None) = None
new create(
conn: HTTPClientConnection,
receiver: RedirectFollowerNotify ref,
factory: RedirectConnectionFactory,
max_redirects: USize,
origin: Origin)
=>
"""
Create a redirect follower that wraps `conn`.
Inserts itself into the connection's callback path so redirect responses
are handled internally. Non-redirect responses forward to `receiver`.
Cross-origin redirects use `factory` to create a new connection.
`max_redirects` is how many hops to follow before refusing with
TooManyRedirects. `origin` is the scheme, host, and port of the initial
connection — used to detect cross-origin hops.
"""
_conn = conn
_receiver = receiver
_factory = factory
_remaining = max_redirects
_origin = origin
conn._set_lifecycle_receiver(this)
new _for_test(
conn': HTTPClientConnection,
receiver': RedirectFollowerNotify ref,
factory': RedirectConnectionFactory,
max_redirects': USize,
origin': Origin,
last_request': (HTTPRequest val | None) = None)
=>
"""
Test constructor that accepts pre-set state without installing the
follower into a live connection.
"""
_conn = conn'
_receiver = receiver'
_factory = factory'
_remaining = max_redirects'
_origin = origin'
_last_request = last_request'
new none() =>
"""
Placeholder for the actor field default, replaced in the actor
constructor body where `this` is available as `ref`.
"""
_conn = HTTPClientConnection.none()
_receiver = object ref is RedirectFollowerNotify end
_factory =
object ref is RedirectConnectionFactory
fun ref apply(target: uri.URI val): HTTPClientConnection =>
HTTPClientConnection.none()
end
_remaining = 0
_origin = Origin(false, "", "")
fun ref connection(): HTTPClientConnection =>
"""
The current underlying connection, which changes after a cross-origin
redirect.
"""
_conn
fun ref send_request(request: HTTPRequest val): SendRequestResult =>
"""
Send a request and remember it for redirect decisions.
"""
_last_request = request
_conn.send_request(request)
fun ref on_connected() =>
match _pending = None
| let r: Redirect =>
_last_request = r.request()
match _conn.send_request(r.request())
| ResponsePending => _Unreachable()
| ConnectionClosed => None // OS send failure; on_closed already fired
end
else
_receiver.on_connected()
end
fun ref on_response(response: Response val) =>
match _last_request
| let sent: HTTPRequest val =>
match \exhaustive\ _RedirectDecision(
sent, _origin, response, _remaining)
| let r: Redirect =>
_pending = r
return
| let err: RedirectError =>
_pending = _Suppressing
_receiver.on_redirect_error(err)
return
| _NotARedirect => None
end
end
_receiver.on_response(response)
fun ref on_body_chunk(data: Array[U8] val) =>
if _pending is None then
_receiver.on_body_chunk(data)
end
fun ref on_response_complete() =>
match _pending = None
| _Suppressing => return
| let r: Redirect =>
let target_origin = Origin.from_uri(r.target())
if _origin == target_origin then
_remaining = r.remaining()
_last_request = r.request()
match _conn.send_request(r.request())
| ResponsePending => _Unreachable()
| ConnectionClosed => None // OS send failure; on_closed already fired
end
else
_pending = r
_origin = target_origin
_remaining = r.remaining()
_conn.close()
_conn = _factory(r.target())
_conn._set_lifecycle_receiver(this)
end
else
_receiver.on_response_complete()
end
fun ref on_closed() =>
match _pending
| let _: Redirect => None
else
_receiver.on_closed()
end
fun ref on_connection_failure(reason: ConnectionFailureReason) =>
_receiver.on_connection_failure(reason)
fun ref on_parse_error(err: ParseError) =>
_receiver.on_parse_error(err)
fun ref on_throttled() =>
_receiver.on_throttled()
fun ref on_unthrottled() =>
_receiver.on_unthrottled()
fun ref on_timer(token: TimerToken) =>
_receiver.on_timer(token)
fun ref on_timer_failure() =>
_receiver.on_timer_failure()