//// HTTP send abstraction used by all HTTP-based credential providers (and //// later by the request pipeline itself). //// //// Every provider that talks to AWS endpoints takes a `Send` value so tests //// can drive it with a stub. Production code calls `default_send`, which //// dispatches via `gleam_httpc` (Erlang's `httpc`). //// //// Errors are normalised to a single `HttpError` sum type so the providers //// can pattern-match on category without depending on `httpc`'s shape //// directly. import aws/streaming.{type StreamingBody} import gleam/http/request.{type Request} import gleam/http/response.{type Response} import gleam/httpc pub type HttpError { /// Could not reach the host (DNS, TCP, TLS). ConnectFailed(reason: String) /// Connection succeeded but no response came back in time. Timeout /// Response body was not the expected encoding. InvalidBody(reason: String) /// Anything else the transport surfaced. Other(reason: String) } /// A function that sends a request and returns a response (or an HTTP error). /// The body is `BitArray` so providers can deal in raw bytes without forcing /// UTF-8 decoding decisions on the transport. pub type Send = fn(Request(BitArray)) -> Result(Response(BitArray), HttpError) /// Streaming variant of `Send`. The response body is a `StreamingBody`, /// which v1 carries as a single buffered chunk and the eventual chunked /// transport delivers as a true byte stream. Object-streaming GETs /// (S3 `GetObject`, MediaLive, etc.) take this shape so call sites can /// migrate today and pick up the real streaming path later without an /// API change. pub type StreamingSend = fn(Request(BitArray)) -> Result(Response(StreamingBody), HttpError) /// Lift a buffered `Send` into a `StreamingSend` by wrapping its /// response body as a `Buffered` `StreamingBody`. Use this when a /// test wants to stub the streaming transport with a buffered fake, /// or when a caller has only a buffered sender available and wants /// it to satisfy a `StreamingSend` slot. Production code should /// take `http_streaming.default_send` for genuine chunked transfer. pub fn lift_to_streaming(send: Send) -> StreamingSend { fn(req) { case send(req) { Ok(resp) -> Ok(response.Response( status: resp.status, headers: resp.headers, body: streaming.from_bit_array(resp.body), )) Error(e) -> Error(e) } } } /// Default total-request timeout for `default_send`. 30 seconds is the /// gleam_httpc default and a reasonable upper bound for control-plane and /// list operations. Object-streaming GETs that may take longer want a /// dedicated, more generous Send. pub const default_timeout_seconds: Int = 30 /// IMDS gets a much shorter total timeout because its first call goes to a /// link-local address that doesn't resolve at all on a non-EC2 host. /// Without this, the TCP retransmit window can stall the whole credential /// chain for tens of seconds — enough to trip the credentials cache actor's /// 5-second call timeout. pub const imds_timeout_seconds: Int = 2 /// Production sender — 30 second total timeout, TLS verification on. pub fn default_send( req: Request(BitArray), ) -> Result(Response(BitArray), HttpError) { do_send(default_config(), req) } /// Build a `Send` with a custom total timeout. Use this for endpoints that /// need either fast-fail behaviour (IMDS) or extra patience (large object /// downloads). TLS verification and redirect-following match /// `default_send`'s defaults; if you need to tweak those, drop down to /// `gleam_httpc` directly. pub fn with_timeout(seconds seconds: Int) -> Send { let config = httpc.configure() |> httpc.timeout(seconds * 1000) fn(req) { do_send(config, req) } } /// IMDS-tuned sender. Equivalent to `with_timeout(seconds: imds_timeout_seconds)`, /// exported as a constant so call sites read meaningfully. pub fn imds_send( req: Request(BitArray), ) -> Result(Response(BitArray), HttpError) { with_timeout(seconds: imds_timeout_seconds)(req) } fn default_config() -> httpc.Configuration { httpc.configure() |> httpc.timeout(default_timeout_seconds * 1000) } fn do_send( config: httpc.Configuration, req: Request(BitArray), ) -> Result(Response(BitArray), HttpError) { case httpc.dispatch_bits(config, req) { Ok(response) -> Ok(response) Error(httpc.FailedToConnect(_, _)) -> Error(ConnectFailed(reason: "could not connect to host")) Error(httpc.ResponseTimeout) -> Error(Timeout) Error(httpc.InvalidUtf8Response) -> Error(InvalidBody(reason: "response body was not valid UTF-8")) } }