//// Streaming HTTP send for the SDK runtime. Sits between the //// generated service code and the Erlang FFI in `aws_streaming_ffi`: //// translates a Gleam `Request(BitArray)` into the parameters //// `httpc:request/4` expects, dispatches via the streaming FFI, //// then wraps the response back into a `Response(StreamingBody)` //// so call sites read identically to the buffered transport. //// //// This is the foundational piece for `S3.GetObject` on multi-GB //// objects, event-stream operations (Transcribe, Kinesis), and any //// future S3 transfer manager — none of those can pre-buffer the //// full response into memory. import aws/internal/http_send.{ type HttpError, type StreamingSend, ConnectFailed, InvalidBody, Other, Timeout, default_timeout_seconds, } import aws/streaming.{type StreamingBody} import gleam/bit_array import gleam/erlang/atom.{type Atom} import gleam/http.{type Method} import gleam/http/request.{type Request} import gleam/http/response.{type Response} import gleam/list import gleam/uri /// Default streaming sender. Same TLS / timeout defaults as /// `http_send.default_send`; the response body is a `StreamingBody` /// carrying chunks as they arrived on the wire. Pass this as /// `ClientConfig.streaming_http_send` for the SDK runtime, or call /// it directly when wiring custom callers. pub fn default_send( req: Request(BitArray), ) -> Result(Response(StreamingBody), HttpError) { send_with( req, timeout_ms: default_timeout_seconds * 1000, verify_tls: True, http2: False, ) } /// HTTP/2 variant of `default_send`. Adds `{http_version, "HTTP/2"}` /// to the httpc option list; servers that don't speak HTTP/2 negotiate /// down to HTTP/1.1 via ALPN. Use this for endpoints known to benefit: /// S3 multipart uploads, Bedrock streaming responses, Transcribe. /// Caller-facing knob lives at `runtime.with_http2`. pub fn default_send_http2( req: Request(BitArray), ) -> Result(Response(StreamingBody), HttpError) { send_with( req, timeout_ms: default_timeout_seconds * 1000, verify_tls: True, http2: True, ) } /// Send a `StreamingSend` configurable on timeout and TLS verification. /// Use this for live-tested object-streaming GETs that need either /// fast-fail (IMDS-style 2s timeouts) or extra patience (multi-GB /// downloads). pub fn with_timeout_and_tls( timeout_ms timeout_ms: Int, verify_tls verify_tls: Bool, ) -> StreamingSend { fn(req) { send_with(req, timeout_ms, verify_tls, False) } } /// HTTP/2 + custom timeout / TLS builder. Same as `with_timeout_and_tls` /// but adds the HTTP/2 option to the httpc call. pub fn with_timeout_tls_http2( timeout_ms timeout_ms: Int, verify_tls verify_tls: Bool, ) -> StreamingSend { fn(req) { send_with(req, timeout_ms, verify_tls, True) } } fn send_with( req: Request(BitArray), timeout_ms timeout_ms: Int, verify_tls verify_tls: Bool, http2 http2: Bool, ) -> Result(Response(StreamingBody), HttpError) { let url = req |> request.to_uri |> uri.to_string case streaming_send( req.method, url, req.headers, req.body, timeout_ms, verify_tls, http2, ) { Ok(#(status, headers, chunks)) -> Ok(response.Response( status: status, headers: list_decode_headers(headers), body: streaming.from_chunks(chunks), )) Error(reason) -> Error(translate_error(reason)) } } // httpc returns response headers as binaries (or iolists on some OTP // minors). `bit_array.to_string` normalises both to a Gleam String; // non-UTF8 values fall through as empty (AWS response headers are // always ASCII so this never bites in practice). fn list_decode_headers( raw: List(#(BitArray, BitArray)), ) -> List(#(String, String)) { list.map(raw, fn(pair) { #(to_string_or_empty(pair.0), to_string_or_empty(pair.1)) }) } fn to_string_or_empty(b: BitArray) -> String { case bit_array.to_string(b) { Ok(s) -> s Error(_) -> "" } } fn translate_error(reason: Atom) -> HttpError { case atom.to_string(reason) { "failed_to_connect" -> ConnectFailed(reason: "could not connect to host") "response_timeout" -> Timeout "timeout" -> Timeout "invalid_utf8_response" -> InvalidBody(reason: "response body was not valid UTF-8") other -> Other(reason: other) } } @external(erlang, "aws_streaming_ffi", "streaming_send") fn streaming_send( method: Method, url: String, headers: List(#(String, String)), body: BitArray, timeout_ms: Int, verify_tls: Bool, http2: Bool, ) -> Result(#(Int, List(#(BitArray, BitArray)), List(BitArray)), Atom)