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::{Framer, Framing};
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 {
33            mode,
34            observation,
35            extensions,
36        } = exchange;
37        request.extensions_mut().extend(extensions);
38        // A whole reply has nothing to stream: it is read as a unary one is,
39        // so its document is a fact of the reply in both modes.
40        let streamed = framing.framer().filter(|_| mode == Mode::Streaming);
41        if streamed.is_some() && matches!(request.body(), Body::Multipart(_)) {
42            return Opening::failed(ProviderError::request(
43                "a multipart request cannot open a streamed reply",
44            ));
45        }
46        accept_header(&mut request, framing);
47        // Errors need the actual path; observations use the declared
48        // template to group attempts independently of concrete URLs.
49        let path = request.uri().path().to_owned();
50        let declared = route.map_or_else(|| path.clone(), str::to_owned);
51        let exchange = HttpExchange {
52            framing,
53            request_id_header,
54            relaxed_content_type,
55            path,
56            observation: observation.map(|context| (context, AdapterSlot::default())),
57            project,
58        };
59        let http = self.clone();
60        Opening::new(async move {
61            // Unpolled replies must not report transport attempts.
62            exchange.install(&request, &declared);
63            let slot = exchange.slot().cloned();
64            let mut opened = match streamed {
65                Some(framer) => match byte_request(request) {
66                    Ok(request) => exchange.streaming(&http, request, framer).await,
67                    Err(error) => Opened::failed(error),
68                },
69                None => exchange.unary(&http, request).await,
70            };
71            opened.slot = slot;
72            opened.analysis_only = analysis_only;
73            Ok(opened)
74        })
75    }
76}
77
78/// What one request's exchange needs besides the client.
79struct HttpExchange {
80    framing: Framing,
81    request_id_header: Option<&'static str>,
82    relaxed_content_type: bool,
83    path: String,
84    observation: Option<(AdapterContext, AdapterSlot)>,
85    project: Option<Projector>,
86}
87
88impl HttpExchange {
89    fn slot(&self) -> Option<&AdapterSlot> {
90        self.observation.as_ref().map(|(_, slot)| slot)
91    }
92
93    fn project(&self, payload: &[u8]) {
94        project(self.slot(), self.project, payload);
95    }
96
97    fn install<B>(&self, request: &http::Request<B>, declared: &str) {
98        if let Some((context, slot)) = &self.observation {
99            slot.install(context.attempt_for(request, declared));
100        }
101    }
102
103    fn failed(self, error: ProviderError, request_id: Option<String>) -> Opened<WireFrame> {
104        Opened::failed(error)
105            .with_request_id(request_id)
106            .with_route(self.path)
107    }
108
109    /// Send a buffered request and frame its whole body. Every frame is
110    /// projected as the driver reads it.
111    async fn unary<H: HttpClientExt>(
112        self,
113        http: &H,
114        request: http::Request<Body>,
115    ) -> Opened<WireFrame> {
116        let sent = match send(http, request, self.request_id_header, self.slot()).await {
117            Ok(sent) => sent,
118            Err(error) => {
119                // The reply the failure carries is still the provider's:
120                // project its facts before reporting the ending.
121                if let Some(body) = error.provider_response_body() {
122                    self.project(body.as_bytes());
123                }
124                return self.failed(error, None);
125            }
126        };
127        // The status said success, but an SSE framer over a body that is not
128        // an event stream yields no frames at all, which would fold to a
129        // contentless success. The reply the provider actually sent is the
130        // error.
131        if let Some(rejected) =
132            wrong_content_type(&sent.headers, self.framing, self.relaxed_content_type)
133        {
134            let error = ProviderError::from_transport_error(rejected)
135                .with_provider_status(Some(sent.status))
136                .with_provider_request_id(sent.provider_request_id.clone())
137                .with_response_headers(Some(sent.headers.clone()));
138            self.project(&sent.body);
139            return self.failed(error, sent.provider_request_id);
140        }
141        // Frame the reply the way a stream is framed: a wire whose unary
142        // reply is an event stream would otherwise hand every projector a
143        // document it cannot parse.
144        let document = serde_json::from_slice(&sent.body).ok();
145        let frames = self.framing.split(&sent.body);
146        let slot = self.slot().cloned();
147        let projector = self.project;
148        let frames = futures::stream::iter(frames).map(move |frame| {
149            project(slot.as_ref(), projector, frame.as_bytes());
150            Ok(frame)
151        });
152        Opened {
153            document,
154            ..Opened::new(frames)
155                .with_request_id(sent.provider_request_id)
156                .with_http(sent.status, sent.headers)
157                .with_route(self.path)
158        }
159    }
160
161    /// Open a streamed reply and frame its body as it arrives. Each frame is
162    /// projected only when the driver reads it, and the bytes left unframed
163    /// are recorded when the body ends.
164    async fn streaming<H: HttpClientExt>(
165        self,
166        http: &H,
167        request: http::Request<Vec<u8>>,
168        mut framer: Framer,
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            path,
217            project: projector,
218            ..
219        } = self;
220        let mut body = response.into_body();
221        let frames = async_stream::stream! {
222            while let Some(chunk) = body.next().await {
223                let chunk = match chunk {
224                    Ok(chunk) => chunk,
225                    Err(error) => {
226                        if let Some(slot) = &slot {
227                            slot.error_boundary(AdapterErrorBoundary::Transport);
228                        }
229                        yield Err(ProviderError::from_transport_error(error));
230                        return;
231                    }
232                };
233                for frame in framer.push(&chunk) {
234                    project(slot.as_ref(), projector, frame.as_bytes());
235                    yield Ok(frame);
236                }
237            }
238            if let Some(slot) = &slot {
239                slot.unframed(framer.pending());
240            }
241            if let Some(frame) = framer.finish() {
242                project(slot.as_ref(), projector, frame.as_bytes());
243                yield Ok(frame);
244            }
245        };
246        Opened::new(frames)
247            .with_request_id(request_id)
248            .with_http(status, headers)
249            .with_route(path)
250    }
251}
252
253/// Project a payload's observation facts onto the attempt, when the call is
254/// observed and the wire reads any.
255fn project(slot: Option<&AdapterSlot>, project: Option<Projector>, payload: &[u8]) {
256    if let (Some(slot), Some(project)) = (slot, project) {
257        slot.project(|sink| project(payload, sink));
258    }
259}
260
261/// What one send learned about its reply.
262struct Sent {
263    status: http::StatusCode,
264    headers: http::HeaderMap,
265    body: Bytes,
266    provider_request_id: Option<String>,
267}
268
269/// Sends a buffered request, preserving non-success response details and IDs.
270async fn send<H>(
271    http: &H,
272    request: http::Request<Body>,
273    request_id_header: Option<&'static str>,
274    observation: Option<&AdapterSlot>,
275) -> Result<Sent, ProviderError>
276where
277    H: HttpClientExt,
278{
279    let (parts, body) = request.into_parts();
280    let response = match body {
281        Body::Bytes(bytes) => {
282            http.send::<_, Bytes>(http::Request::from_parts(parts, bytes))
283                .await
284        }
285        Body::Multipart(form) => {
286            http.send_multipart::<Bytes>(http::Request::from_parts(parts, form))
287                .await
288        }
289    };
290    let response = match response {
291        Ok(response) => response,
292        // A transport that reports the non-success reply as an error: the
293        // reply is the provider's, so it funnels to a preserved provider
294        // response with the id read off its headers and the headers
295        // themselves; a response-less failure stays a transport error.
296        Err(error) => {
297            if let Some(observation) = observation
298                && let Some(status) = error.non_success_status()
299            {
300                observation.response_with_headers(status, error.non_success_headers());
301            }
302            let request_id = error
303                .non_success_headers()
304                .and_then(|headers| request_id_from(headers, request_id_header));
305            return Err(
306                ProviderError::from_transport_error(error).with_provider_request_id(request_id)
307            );
308        }
309    };
310
311    // Take the reply apart before awaiting the body: the headers are then
312    // owned, so preserving them onto an error costs no clone and every
313    // error path below can afford them.
314    let (parts, body) = response.into_parts();
315    let status = parts.status;
316    if let Some(observation) = observation {
317        observation.response_with_headers(status, Some(&parts.headers));
318    }
319    let provider_request_id = request_id_from(&parts.headers, request_id_header);
320    let body = body.await.map_err(ProviderError::from_transport_error)?;
321
322    if !status.is_success() {
323        return Err(
324            ProviderError::from_http_response(status, String::from_utf8_lossy(&body))
325                .with_provider_request_id(provider_request_id)
326                .with_response_headers(Some(parts.headers)),
327        );
328    }
329    Ok(Sent {
330        status,
331        headers: parts.headers,
332        body,
333        provider_request_id,
334    })
335}
336
337/// The provider's transport request id, when it names such a header and the
338/// reply carries a non-empty value.
339pub(crate) fn request_id_from(headers: &http::HeaderMap, header: Option<&str>) -> Option<String> {
340    header.and_then(|header| {
341        headers
342            .get(header)
343            .and_then(|value| value.to_str().ok())
344            .filter(|value| !value.is_empty())
345            .map(str::to_string)
346    })
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/// Bytes of a rejected reply's body kept on the error; a reply longer than
416/// this is cut there.
417const REJECTED_BODY_LIMIT: usize = 1 << 20;
418
419/// Chunks read off a rejected reply before giving up on it, so a transport
420/// that keeps yielding empty chunks cannot hold the opener.
421const REJECTED_CHUNK_LIMIT: usize = 4096;
422
423/// Turn a reply the driver will not stream (any status but 200, a 204
424/// included: a status is a status) into the non-success error, reading the
425/// body to its end (bounded in bytes and chunks) so the provider's payload
426/// and the transport's headers ride on the error.
427async fn reject_response(
428    response: http::Response<crate::http_client::BoxedStream>,
429) -> http_client::Error {
430    let status = response.status();
431    let headers = response.headers().clone();
432    let mut body = response.into_body();
433    let mut bytes: Vec<u8> = Vec::new();
434    let mut chunks = 0usize;
435    while let Some(chunk) = body.next().await {
436        chunks += 1;
437        if let Ok(chunk) = chunk {
438            let room = REJECTED_BODY_LIMIT.saturating_sub(bytes.len());
439            bytes.extend_from_slice(chunk.get(..chunk.len().min(room)).unwrap_or_default());
440        }
441        if bytes.len() >= REJECTED_BODY_LIMIT || chunks >= REJECTED_CHUNK_LIMIT {
442            break;
443        }
444    }
445    http_client::Error::InvalidStatusCodeWithDetails {
446        status,
447        body: String::from_utf8_lossy(&bytes).into_owned(),
448        headers,
449    }
450}