Skip to main content

rig_core/driver/
http_transport.rs

1//! The HTTP transport: any [`HttpClientExt`] sends [`Encoded`] requests and
2//! delivers their replies as [`WireFrame`]s. It owns every HTTP concern:
3//! headers, framing, status and content-type checks, request ids and the
4//! observation of the exchange.
5
6use std::sync::Arc;
7
8use bytes::Bytes;
9use futures::StreamExt;
10
11use super::{Exchange, Opened, Opening, Received, Transport};
12use crate::error::ProviderError;
13use crate::http_client::framing::{Framer, Framing};
14use crate::http_client::{self, HttpClientExt};
15use crate::observe::{AdapterContext, AdapterErrorBoundary, AdapterSlot};
16use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
17use crate::wire::{Body, Encoded, Mode, Projector, Wire, WireFrame};
18
19impl<W, H> Transport<W> for H
20where
21    W: Wire<Payload = Encoded, Frame = WireFrame>,
22    H: HttpClientExt + Clone + WasmCompatSend + WasmCompatSync + 'static,
23{
24    fn send(&self, payload: Encoded, exchange: Exchange) -> Opening<WireFrame> {
25        let Encoded {
26            mut request,
27            framing,
28            request_id_header,
29            relaxed_content_type,
30            route,
31            project,
32            analysis_only,
33        } = payload;
34        let Exchange {
35            mode,
36            observation,
37            extensions,
38        } = exchange;
39        request.extensions_mut().extend(extensions);
40        // A whole reply has nothing to stream: it is read as a unary one is,
41        // so its document is a fact of the reply in both modes.
42        let streamed = framing.framer().filter(|_| mode == Mode::Streaming);
43        if streamed.is_some() && matches!(request.body(), Body::Multipart(_)) {
44            return Opening::failed(ProviderError::request(
45                "a multipart request cannot open a streamed reply",
46            ));
47        }
48        accept_header(&mut request, framing);
49        // Errors need the actual path; observations use the declared
50        // template to group attempts independently of concrete URLs.
51        let path = request.uri().path().to_owned();
52        let declared = route.map_or_else(|| path.clone(), str::to_owned);
53        let exchange = HttpExchange {
54            framing,
55            request_id_header,
56            relaxed_content_type,
57            path,
58            observation: observation.map(|context| (context, AdapterSlot::default())),
59            project,
60        };
61        let http = self.clone();
62        let slot = exchange.slot().cloned();
63        let opening = slot.clone();
64        Opening::new(async move {
65            // Unpolled replies must not report transport attempts.
66            exchange.install(&request, &declared);
67            let mut opened = match streamed {
68                Some(framer) => match byte_request(request) {
69                    Ok(request) => exchange.streaming(&http, request, framer).await,
70                    Err(error) => Opened::failed(error),
71                },
72                None => exchange.unary(&http, request).await,
73            };
74            opened.slot = slot;
75            opened.analysis_only = analysis_only;
76            Ok(opened)
77        })
78        .with_slot(opening)
79    }
80}
81
82/// What one request's exchange needs besides the client.
83struct HttpExchange {
84    framing: Framing,
85    request_id_header: Option<&'static str>,
86    relaxed_content_type: bool,
87    path: String,
88    observation: Option<(AdapterContext, AdapterSlot)>,
89    project: Option<Projector>,
90}
91
92impl HttpExchange {
93    fn slot(&self) -> Option<&AdapterSlot> {
94        self.observation.as_ref().map(|(_, slot)| slot)
95    }
96
97    fn project(&self, payload: &[u8]) {
98        project(self.slot(), self.project, payload);
99    }
100
101    fn install<B>(&self, request: &http::Request<B>, declared: &str) {
102        if let Some((context, slot)) = &self.observation {
103            slot.install(context.attempt_for(request, declared));
104        }
105    }
106
107    fn failed(self, error: ProviderError, request_id: Option<String>) -> Opened<WireFrame> {
108        Opened::failed(error)
109            .with_request_id(request_id)
110            .with_route(self.path)
111    }
112
113    /// Send a buffered request and frame its whole body. Every frame is
114    /// projected as the driver reads it.
115    async fn unary<H: HttpClientExt>(
116        self,
117        http: &H,
118        request: http::Request<Body>,
119    ) -> Opened<WireFrame> {
120        let sent = match send(http, request, self.request_id_header, self.slot()).await {
121            Ok(sent) => sent,
122            Err(error) => {
123                // The reply the failure carries is still the provider's:
124                // project its facts before reporting the ending.
125                if let Some(body) = error.provider_response_body() {
126                    self.project(body.as_bytes());
127                }
128                return self.failed(error, None);
129            }
130        };
131        // The status said success, but an SSE framer over a body that is not
132        // an event stream yields no frames at all, which would fold to a
133        // contentless success. The reply the provider actually sent is the
134        // error.
135        if let Some(rejected) =
136            wrong_content_type(&sent.headers, self.framing, self.relaxed_content_type)
137        {
138            let error = ProviderError::from_transport_error(rejected)
139                .with_provider_status(Some(sent.status))
140                .with_provider_request_id(sent.provider_request_id.clone())
141                .with_response_headers(Some(sent.headers.clone()));
142            self.project(&sent.body);
143            return self.failed(error, sent.provider_request_id);
144        }
145        // Frame the reply the way a stream is framed: a wire whose unary
146        // reply is an event stream would otherwise hand every projector a
147        // document it cannot parse.
148        let document = serde_json::from_slice(&sent.body).ok();
149        let frames = self.framing.split(&sent.body);
150        let slot = self.slot().cloned();
151        let projector = self.project;
152        let frames = futures::stream::iter(frames).map(move |frame| {
153            project(slot.as_ref(), projector, frame.as_bytes());
154            Ok(frame)
155        });
156        Opened {
157            document,
158            ..Opened::new(frames)
159                .with_request_id(sent.provider_request_id)
160                .with_http(sent.status, sent.headers)
161                .with_route(self.path)
162        }
163    }
164
165    /// Open a streamed reply and frame its body as it arrives. Each frame is
166    /// projected only when the driver reads it, and the bytes left unframed
167    /// are recorded when the body ends.
168    async fn streaming<H: HttpClientExt>(
169        self,
170        http: &H,
171        request: http::Request<Vec<u8>>,
172        mut framer: Framer,
173    ) -> Opened<WireFrame> {
174        let response = match http.send_streaming(request).await {
175            // Custom transports may return rejected responses directly; preserve
176            // their status, headers, and bounded body in the error.
177            Ok(response) if response.status() != http::StatusCode::OK => {
178                Err(reject_response(response).await)
179            }
180            Ok(response) => {
181                match wrong_content_type(
182                    response.headers(),
183                    self.framing,
184                    self.relaxed_content_type,
185                ) {
186                    Some(error) => Err(error),
187                    None => Ok(response),
188                }
189            }
190            other => other,
191        };
192        let response = match response {
193            Ok(response) => response,
194            Err(error) => {
195                if let Some(slot) = self.slot() {
196                    slot.error_boundary(AdapterErrorBoundary::from_http(&error));
197                    if let Some(status) = error.non_success_status() {
198                        slot.response_with_headers(status, error.non_success_headers());
199                    }
200                    if let Some(body) = error.non_success_body() {
201                        self.project(body.as_bytes());
202                    }
203                }
204                let request_id = error
205                    .non_success_headers()
206                    .and_then(|headers| request_id_from(headers, self.request_id_header));
207                let error = ProviderError::from_transport_error(error)
208                    .with_provider_request_id(request_id.clone());
209                return self.failed(error, request_id);
210            }
211        };
212        if let Some(slot) = self.slot() {
213            slot.response_with_headers(response.status(), Some(response.headers()));
214        }
215        let request_id = request_id_from(response.headers(), self.request_id_header);
216        let status = response.status();
217        let headers = response.headers().clone();
218        let slot = self.slot().cloned();
219        let Self {
220            path,
221            project: projector,
222            ..
223        } = self;
224        let mut body = response.into_body();
225        let received = Arc::new(Received::new());
226        let marked = Arc::clone(&received);
227        let frames = async_stream::stream! {
228            while let Some(chunk) = body.next().await {
229                let chunk = match chunk {
230                    Ok(chunk) => {
231                        marked.mark();
232                        chunk
233                    }
234                    Err(error) => {
235                        if let Some(slot) = &slot {
236                            slot.error_boundary(AdapterErrorBoundary::Transport);
237                        }
238                        yield Err(ProviderError::from_transport_error(error));
239                        return;
240                    }
241                };
242                for frame in framer.push(&chunk) {
243                    project(slot.as_ref(), projector, frame.as_bytes());
244                    yield Ok(frame);
245                }
246            }
247            if let Some(slot) = &slot {
248                slot.unframed(framer.pending());
249            }
250            if let Some(frame) = framer.finish() {
251                project(slot.as_ref(), projector, frame.as_bytes());
252                yield Ok(frame);
253            }
254        };
255        Opened {
256            received: Some(received),
257            ..Opened::new(frames)
258                .with_request_id(request_id)
259                .with_http(status, headers)
260                .with_route(path)
261        }
262    }
263}
264
265/// Project a payload's observation facts onto the attempt, when the call is
266/// observed and the wire reads any.
267fn project(slot: Option<&AdapterSlot>, project: Option<Projector>, payload: &[u8]) {
268    if let (Some(slot), Some(project)) = (slot, project) {
269        slot.project(|sink| project(payload, sink));
270    }
271}
272
273/// What one send learned about its reply.
274struct Sent {
275    status: http::StatusCode,
276    headers: http::HeaderMap,
277    body: Bytes,
278    provider_request_id: Option<String>,
279}
280
281/// Sends a buffered request, preserving non-success response details and IDs.
282async fn send<H>(
283    http: &H,
284    request: http::Request<Body>,
285    request_id_header: Option<&'static str>,
286    observation: Option<&AdapterSlot>,
287) -> Result<Sent, ProviderError>
288where
289    H: HttpClientExt,
290{
291    let (parts, body) = request.into_parts();
292    let response = match body {
293        Body::Bytes(bytes) => {
294            http.send::<_, Bytes>(http::Request::from_parts(parts, bytes))
295                .await
296        }
297        Body::Multipart(form) => {
298            http.send_multipart::<Bytes>(http::Request::from_parts(parts, form))
299                .await
300        }
301    };
302    let response = match response {
303        Ok(response) => response,
304        // A transport that reports the non-success reply as an error: the
305        // reply is the provider's, so it funnels to a preserved provider
306        // response with the id read off its headers and the headers
307        // themselves; a response-less failure stays a transport error.
308        Err(error) => {
309            if let Some(observation) = observation
310                && let Some(status) = error.non_success_status()
311            {
312                observation.response_with_headers(status, error.non_success_headers());
313            }
314            let request_id = error
315                .non_success_headers()
316                .and_then(|headers| request_id_from(headers, request_id_header));
317            return Err(
318                ProviderError::from_transport_error(error).with_provider_request_id(request_id)
319            );
320        }
321    };
322
323    // Take the reply apart before awaiting the body: the headers are then
324    // owned, so preserving them onto an error costs no clone and every
325    // error path below can afford them.
326    let (parts, body) = response.into_parts();
327    let status = parts.status;
328    if let Some(observation) = observation {
329        observation.response_with_headers(status, Some(&parts.headers));
330    }
331    let provider_request_id = request_id_from(&parts.headers, request_id_header);
332    let body = body.await.map_err(ProviderError::from_transport_error)?;
333
334    if !status.is_success() {
335        return Err(
336            ProviderError::from_http_response(status, String::from_utf8_lossy(&body))
337                .with_provider_request_id(provider_request_id)
338                .with_response_headers(Some(parts.headers)),
339        );
340    }
341    Ok(Sent {
342        status,
343        headers: parts.headers,
344        body,
345        provider_request_id,
346    })
347}
348
349/// The provider's transport request id, when it names such a header and the
350/// reply carries a non-empty value.
351pub(crate) fn request_id_from(headers: &http::HeaderMap, header: Option<&str>) -> Option<String> {
352    header.and_then(|header| {
353        headers
354            .get(header)
355            .and_then(|value| value.to_str().ok())
356            .filter(|value| !value.is_empty())
357            .map(str::to_string)
358    })
359}
360
361/// Defaults byte-body requests to `application/json`, including bodyless GETs.
362/// Preserves an explicitly supplied content type.
363fn content_type(request: &mut http::Request<Body>) {
364    if matches!(request.body(), Body::Bytes(_)) {
365        request
366            .headers_mut()
367            .entry(http::header::CONTENT_TYPE)
368            .or_insert(http::HeaderValue::from_static("application/json"));
369    }
370}
371
372/// Add `Accept: text/event-stream` to an SSE request, without overriding an
373/// `Accept` the wire set itself.
374fn accept_header(request: &mut http::Request<Body>, framing: Framing) {
375    content_type(request);
376    if framing == Framing::Sse {
377        request
378            .headers_mut()
379            .entry("Accept")
380            .or_insert(http::HeaderValue::from_static("text/event-stream"));
381    }
382}
383
384/// Whether a reply's content type is not the event stream an SSE wire asked
385/// for: the framer would silently produce no frames, which reads as
386/// truncation rather than as the wrong endpoint. A reply that names no
387/// content type at all is accepted only by a wire that opted in.
388///
389/// The one predicate both paths ask, so a unary reply and a streamed one
390/// cannot disagree about what the provider sent.
391fn wrong_content_type(
392    headers: &http::HeaderMap,
393    framing: Framing,
394    relaxed: bool,
395) -> Option<http_client::Error> {
396    if framing != Framing::Sse {
397        return None;
398    }
399    let Some(content_type) = headers.get(&http::header::CONTENT_TYPE) else {
400        return (!relaxed)
401            .then(|| http_client::Error::InvalidContentType(http::HeaderValue::from_static("")));
402    };
403    let event_stream = content_type
404        .to_str()
405        .ok()
406        .and_then(|value| value.parse::<mime::Mime>().ok())
407        .is_some_and(|mime_type| {
408            matches!(
409                (mime_type.type_(), mime_type.subtype()),
410                (mime::TEXT, mime::EVENT_STREAM)
411            )
412        });
413    (!event_stream).then(|| http_client::Error::InvalidContentType(content_type.clone()))
414}
415
416/// A request whose body is bytes. Multipart replies are never streamed.
417fn byte_request(request: http::Request<Body>) -> Result<http::Request<Vec<u8>>, ProviderError> {
418    let (parts, body) = request.into_parts();
419    match body {
420        Body::Bytes(bytes) => Ok(http::Request::from_parts(parts, bytes)),
421        Body::Multipart(_) => Err(ProviderError::request(
422            "a multipart request cannot open a streamed reply",
423        )),
424    }
425}
426
427/// Bytes of a rejected reply's body kept on the error; a reply longer than
428/// this is cut there.
429const REJECTED_BODY_LIMIT: usize = 1 << 20;
430
431/// Chunks read off a rejected reply before giving up on it, so a transport
432/// that keeps yielding empty chunks cannot hold the opener.
433const REJECTED_CHUNK_LIMIT: usize = 4096;
434
435/// Turn a reply the driver will not stream (any status but 200, a 204
436/// included: a status is a status) into the non-success error, reading the
437/// body to its end (bounded in bytes and chunks) so the provider's payload
438/// and the transport's headers ride on the error.
439async fn reject_response(
440    response: http::Response<crate::http_client::BoxedStream>,
441) -> http_client::Error {
442    let status = response.status();
443    let headers = response.headers().clone();
444    let mut body = response.into_body();
445    let mut bytes: Vec<u8> = Vec::new();
446    let mut chunks = 0usize;
447    while let Some(chunk) = body.next().await {
448        chunks += 1;
449        if let Ok(chunk) = chunk {
450            let room = REJECTED_BODY_LIMIT.saturating_sub(bytes.len());
451            bytes.extend_from_slice(chunk.get(..chunk.len().min(room)).unwrap_or_default());
452        }
453        if bytes.len() >= REJECTED_BODY_LIMIT || chunks >= REJECTED_CHUNK_LIMIT {
454            break;
455        }
456    }
457    http_client::Error::InvalidStatusCodeWithDetails {
458        status,
459        body: String::from_utf8_lossy(&bytes).into_owned(),
460        headers,
461    }
462}