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