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.
345pub(crate) fn request_id_from(headers: &http::HeaderMap, header: Option<&str>) -> Option<String> {
346    header.and_then(|header| {
347        headers
348            .get(header)
349            .and_then(|value| value.to_str().ok())
350            .filter(|value| !value.is_empty())
351            .map(str::to_string)
352    })
353}
354
355/// Defaults byte-body requests to `application/json`, including bodyless GETs.
356/// Preserves an explicitly supplied content type.
357fn content_type(request: &mut http::Request<Body>) {
358    if matches!(request.body(), Body::Bytes(_)) {
359        request
360            .headers_mut()
361            .entry(http::header::CONTENT_TYPE)
362            .or_insert(http::HeaderValue::from_static("application/json"));
363    }
364}
365
366/// Add `Accept: text/event-stream` to an SSE request, without overriding an
367/// `Accept` the wire set itself.
368fn accept_header(request: &mut http::Request<Body>, framing: Framing) {
369    content_type(request);
370    if framing == Framing::Sse {
371        request
372            .headers_mut()
373            .entry("Accept")
374            .or_insert(http::HeaderValue::from_static("text/event-stream"));
375    }
376}
377
378/// Whether a reply's content type is not the event stream an SSE wire asked
379/// for: the framer would silently produce no frames, which reads as
380/// truncation rather than as the wrong endpoint. A reply that names no
381/// content type at all is accepted only by a wire that opted in.
382///
383/// The one predicate both paths ask, so a unary reply and a streamed one
384/// cannot disagree about what the provider sent.
385fn wrong_content_type(
386    headers: &http::HeaderMap,
387    framing: Framing,
388    relaxed: bool,
389) -> Option<http_client::Error> {
390    if framing != Framing::Sse {
391        return None;
392    }
393    let Some(content_type) = headers.get(&http::header::CONTENT_TYPE) else {
394        return (!relaxed)
395            .then(|| http_client::Error::InvalidContentType(http::HeaderValue::from_static("")));
396    };
397    let event_stream = content_type
398        .to_str()
399        .ok()
400        .and_then(|value| value.parse::<mime::Mime>().ok())
401        .is_some_and(|mime_type| {
402            matches!(
403                (mime_type.type_(), mime_type.subtype()),
404                (mime::TEXT, mime::EVENT_STREAM)
405            )
406        });
407    (!event_stream).then(|| http_client::Error::InvalidContentType(content_type.clone()))
408}
409
410/// A request whose body is bytes. Multipart replies are never streamed.
411fn byte_request(request: http::Request<Body>) -> Result<http::Request<Vec<u8>>, ProviderError> {
412    let (parts, body) = request.into_parts();
413    match body {
414        Body::Bytes(bytes) => Ok(http::Request::from_parts(parts, bytes)),
415        Body::Multipart(_) => Err(ProviderError::request(
416            "a multipart request cannot open a streamed reply",
417        )),
418    }
419}
420
421/// Observable payload with an optional decoder frame. Whitespace-only SSE
422/// payloads are observed as heartbeats but not decoded.
423struct Framed {
424    payload: Vec<u8>,
425    frame: bool,
426}
427
428impl Framed {
429    fn payload(&self) -> &[u8] {
430        &self.payload
431    }
432
433    fn into_frame(self) -> Option<WireFrame> {
434        self.frame.then(|| match String::from_utf8(self.payload) {
435            Ok(text) => WireFrame::Text(text),
436            Err(error) => WireFrame::Bytes(error.into_bytes()),
437        })
438    }
439}
440
441/// The framer for one reply's bytes.
442enum Framer {
443    Sse(SseFramer),
444    Ndjson(NdjsonFramer),
445    Whole(Vec<u8>),
446}
447
448impl Framer {
449    fn new(framing: Framing) -> Self {
450        match framing {
451            Framing::Sse => Self::Sse(SseFramer::new()),
452            Framing::Ndjson => Self::Ndjson(NdjsonFramer::new()),
453            Framing::Whole => Self::Whole(Vec::new()),
454        }
455    }
456
457    fn push(&mut self, chunk: &[u8]) -> Vec<Framed> {
458        match self {
459            Self::Sse(framer) => framer
460                .push(chunk)
461                .map(|event| Framed {
462                    frame: !event.data.trim().is_empty(),
463                    payload: event.data.into_bytes(),
464                })
465                .collect(),
466            Self::Ndjson(framer) => framer
467                .push(chunk)
468                .map(|line| Framed {
469                    frame: true,
470                    payload: line,
471                })
472                .collect(),
473            Self::Whole(buffer) => {
474                buffer.extend_from_slice(chunk);
475                Vec::new()
476            }
477        }
478    }
479
480    fn finish(&mut self) -> Vec<Framed> {
481        match self {
482            // The grammar dispatches only on a blank line: an unterminated
483            // trailing event is not a frame.
484            Self::Sse(_) => Vec::new(),
485            Self::Ndjson(framer) => framer
486                .finish()
487                .map(|line| Framed {
488                    frame: true,
489                    payload: line,
490                })
491                .into_iter()
492                .collect(),
493            Self::Whole(buffer) => {
494                let payload = std::mem::take(buffer);
495                if payload.is_empty() {
496                    Vec::new()
497                } else {
498                    vec![Framed {
499                        frame: true,
500                        payload,
501                    }]
502                }
503            }
504        }
505    }
506}
507
508/// Bytes of a rejected reply's body kept on the error; a reply longer than
509/// this is cut there.
510const REJECTED_BODY_LIMIT: usize = 1 << 20;
511
512/// Chunks read off a rejected reply before giving up on it, so a transport
513/// that keeps yielding empty chunks cannot hold the opener.
514const REJECTED_CHUNK_LIMIT: usize = 4096;
515
516/// Turn a reply the driver will not stream (any status but 200, a 204
517/// included: a status is a status) into the non-success error, reading the
518/// body to its end (bounded in bytes and chunks) so the provider's payload
519/// and the transport's headers ride on the error.
520async fn reject_response(
521    response: http::Response<crate::http_client::BoxedStream>,
522) -> http_client::Error {
523    let status = response.status();
524    let headers = response.headers().clone();
525    let mut body = response.into_body();
526    let mut bytes: Vec<u8> = Vec::new();
527    let mut chunks = 0usize;
528    while let Some(chunk) = body.next().await {
529        chunks += 1;
530        if let Ok(chunk) = chunk {
531            let room = REJECTED_BODY_LIMIT.saturating_sub(bytes.len());
532            bytes.extend_from_slice(chunk.get(..chunk.len().min(room)).unwrap_or_default());
533        }
534        if bytes.len() >= REJECTED_BODY_LIMIT || chunks >= REJECTED_CHUNK_LIMIT {
535            break;
536        }
537    }
538    http_client::Error::InvalidStatusCodeWithDetails {
539        status,
540        body: String::from_utf8_lossy(&bytes).into_owned(),
541        headers,
542    }
543}