1use crate::{Event, EventSource};
6#[cfg(target_os = "fuchsia")]
7use fidl_fuchsia_net_http as fnet_http;
8#[cfg(target_os = "fuchsia")]
9use fuchsia_async as fasync;
10use fuchsia_hyper::HttpsClient;
11#[cfg(target_os = "fuchsia")]
12use futures::io::AsyncReadExt as _;
13use futures::stream::{Stream, StreamExt as _};
14use futures::task::{Context, Poll};
15use hyper::{Request, StatusCode};
16use std::pin::Pin;
17use thiserror::Error;
18
19#[derive(derivative::Derivative)]
21#[derivative(Debug)]
22pub struct Client {
23 #[derivative(Debug = "ignore")]
24 chunks: futures::stream::BoxStream<'static, Result<hyper::body::Bytes, anyhow::Error>>,
25 source: EventSource,
26 events: std::vec::IntoIter<Event>,
27}
28
29impl Client {
30 pub async fn from_hyper_client(
32 https_client: &HttpsClient,
33 url: impl AsRef<str>,
34 ) -> Result<Self, FromHyperClientError> {
35 let request = Request::get(url.as_ref())
36 .header("accept", "text/event-stream")
37 .body(http_body_util::Full::default())
38 .map_err(|e| FromHyperClientError::CreateRequest(e))?;
39 let response = https_client
40 .request(request)
41 .await
42 .map_err(|e| FromHyperClientError::MakeRequest(e))?;
43 if response.status() != StatusCode::OK {
44 return Err(FromHyperClientError::HttpStatus(response.status()));
45 }
46 Ok(Self {
47 chunks: http_body_util::BodyStream::new(response.into_body())
48 .filter_map(|frame_res| async move {
49 match frame_res {
50 Ok(frame) => match frame.into_data() {
51 Ok(bytes) => Some(Ok(bytes)),
52 Err(_) => None,
53 },
54 Err(e) => Some(Err(anyhow::anyhow!(e))),
55 }
56 })
57 .boxed(),
58 source: EventSource::new(),
59 events: vec![].into_iter(),
60 })
61 }
62
63 #[cfg(target_os = "fuchsia")]
69 pub async fn from_http_loader(
70 loader: &fnet_http::LoaderProxy,
71 url: String,
72 stream_buf_size: usize,
73 ) -> Result<Self, FromHttpLoaderError> {
74 let resp = loader
75 .fetch(fnet_http::Request {
76 method: None,
77 url: Some(url),
78 headers: Some(vec![fnet_http::Header {
79 name: b"accept".to_vec(),
80 value: b"text/event-stream".to_vec(),
81 }]),
82 body: None,
83 deadline: None,
84 ..Default::default()
85 })
86 .await
87 .map_err(FromHttpLoaderError::FetchFidl)?;
88 let socket = match resp {
89 fnet_http::Response {
90 error: None, body: Some(body), status_code: Some(200), ..
91 } => body,
92 fnet_http::Response { error, status_code, status_line, .. } => {
93 return Err(FromHttpLoaderError::FetchError { error, status_code, status_line });
94 }
95 };
96 Ok(Self {
97 chunks: stream_from_socket(fasync::Socket::from_socket(socket), stream_buf_size),
98 source: EventSource::new(),
99 events: vec![].into_iter(),
100 })
101 }
102}
103
104#[cfg(target_os = "fuchsia")]
105fn stream_from_socket(
106 socket: fasync::Socket,
107 stream_buf_size: usize,
108) -> futures::stream::BoxStream<'static, Result<hyper::body::Bytes, anyhow::Error>> {
109 let initial_buf = vec![0; stream_buf_size];
110 futures::stream::try_unfold((socket, initial_buf), |(mut socket, mut buf)| async move {
111 match socket.read(&mut buf).await {
112 Ok(0) => Ok(None),
113 Ok(n) => {
114 let chunk = hyper::body::Bytes::copy_from_slice(&buf[..n]);
115 Ok(Some((chunk, (socket, buf))))
116 }
117 Err(e) => Err(anyhow::anyhow!(e).context("reading from socket")),
118 }
119 })
120 .boxed()
121}
122
123#[derive(Debug, Error)]
124pub enum FromHyperClientError {
125 #[error("error creating http request")]
126 CreateRequest(#[source] hyper::http::Error),
127
128 #[error("error making http request")]
129 MakeRequest(#[source] hyper_util::client::legacy::Error),
130
131 #[error("http server responded with status other than OK: {0}")]
132 HttpStatus(hyper::StatusCode),
133}
134
135#[cfg(target_os = "fuchsia")]
136#[derive(Debug, Error)]
137pub enum FromHttpLoaderError {
138 #[error("fuchsia.net.http/Loader.Fetch fidl error")]
139 FetchFidl(#[source] fidl::Error),
140
141 #[error(
142 "fuchsia.net.http/Loader.Fetch error: status {status_code:?} {error:?} {status_line:?}"
143 )]
144 FetchError {
145 error: Option<fnet_http::Error>,
146 status_code: Option<u32>,
147 status_line: Option<Vec<u8>>,
148 },
149}
150
151impl Stream for Client {
152 type Item = Result<Event, ClientPollError>;
153
154 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
155 loop {
156 if let Some(event) = self.events.next() {
157 return Poll::Ready(Some(Ok(event)));
158 }
159 match Pin::new(&mut self.chunks).poll_next(cx) {
160 Poll::Ready(Some(Ok(chunk))) => {
161 self.events = self.source.parse(&chunk).into_iter();
162 }
163 Poll::Ready(Some(Err(e))) => {
164 return Poll::Ready(Some(Err(ClientPollError::NextChunk(e))));
165 }
166 Poll::Ready(None) => {
167 return Poll::Ready(None);
168 }
169 Poll::Pending => {
170 return Poll::Pending;
171 }
172 }
173 }
174 }
175}
176
177#[derive(Debug, Error)]
178pub enum ClientPollError {
179 #[error("error downloading next chunk")]
180 NextChunk(#[source] anyhow::Error),
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use assert_matches::assert_matches;
187 use fuchsia_async as fasync;
188 use fuchsia_async::net::TcpListener;
189 use fuchsia_hyper::new_https_client;
190 use futures::TryStreamExt as _;
191 use futures::future::Future;
192 use hyper::Response;
193 use hyper::service::service_fn;
194 use std::net::{Ipv4Addr, SocketAddr};
195 use test_case::test_case;
196 pub enum Body {
197 Empty(http_body_util::Empty<hyper::body::Bytes>),
198 Full(http_body_util::Full<hyper::body::Bytes>),
199 Stream(
200 http_body_util::StreamBody<
201 std::pin::Pin<
202 Box<
203 dyn futures::stream::Stream<
204 Item = Result<
205 hyper::body::Frame<hyper::body::Bytes>,
206 Box<dyn std::error::Error + Send + Sync + 'static>,
207 >,
208 > + Send,
209 >,
210 >,
211 >,
212 ),
213 }
214
215 impl Body {
216 pub fn empty() -> Self {
217 Body::Empty(http_body_util::Empty::new())
218 }
219 pub fn wrap_stream<S, O, E>(stream: S) -> Self
220 where
221 S: futures::stream::Stream<Item = Result<O, E>> + Send + 'static,
222 O: Into<hyper::body::Bytes>,
223 E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
224 {
225 let stream = stream.map(|res| match res {
226 Ok(data) => Ok(hyper::body::Frame::data(data.into())),
227 Err(e) => Err(e.into()),
228 });
229 Body::Stream(http_body_util::StreamBody::new(Box::pin(stream)))
230 }
231 }
232 impl From<Vec<u8>> for Body {
233 fn from(data: Vec<u8>) -> Self {
234 Body::Full(http_body_util::Full::new(data.into()))
235 }
236 }
237 impl hyper::body::Body for Body {
238 type Data = hyper::body::Bytes;
239 type Error = Box<dyn std::error::Error + Send + Sync + 'static>;
240
241 fn poll_frame(
242 self: std::pin::Pin<&mut Self>,
243 cx: &mut std::task::Context<'_>,
244 ) -> std::task::Poll<Option<Result<hyper::body::Frame<Self::Data>, Self::Error>>> {
245 match self.get_mut() {
246 Body::Empty(b) => std::pin::Pin::new(b).poll_frame(cx).map_err(|e| match e {}),
247 Body::Full(b) => std::pin::Pin::new(b).poll_frame(cx).map_err(|e| match e {}),
248 Body::Stream(b) => std::pin::Pin::new(b).poll_frame(cx),
249 }
250 }
251 }
252
253 fn spawn_server<F>(handle_req: fn(Request<hyper::body::Incoming>) -> F) -> String
254 where
255 F: Future<Output = Result<Response<Body>, hyper::Error>> + Send + 'static,
256 {
257 use futures::StreamExt as _;
258 use hyper_util::rt::TokioIo;
259
260 let (listener, url) = {
261 let listener =
262 TcpListener::bind(&SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0)).unwrap();
263 let local_addr = listener.local_addr().unwrap();
264 (listener.accept_stream(), format!("http://{}", local_addr))
265 };
266 let builder = hyper_util::server::conn::auto::Builder::new(fuchsia_hyper::Executor);
267 fasync::Task::spawn(async move {
268 let mut tasks = futures::stream::FuturesUnordered::new();
269 let mut listener = listener.fuse();
270 loop {
271 futures::select! {
272 res = listener.next() => {
273 match res {
274 Some(Ok((stream, _addr))) => {
275 let stream = fuchsia_hyper::TcpStream { stream };
276 let service = service_fn(handle_req);
277 let builder = builder.clone();
278 tasks.push(fasync::Task::spawn(async move {
279 let _ = builder.serve_connection(TokioIo::new(stream), service).await;
280 }));
281 }
282 _ => break,
283 }
284 }
285 _ = tasks.next() => {}
286 }
287 }
288 }).detach();
289 url
290 }
291
292 fn make_event() -> Event {
293 Event::from_type_and_data("event_type", "data_contents").unwrap()
294 }
295
296 async fn make_client(byte_source: &str, url: String) -> Client {
297 match byte_source {
298 "hyper" => Client::from_hyper_client(&new_https_client(), url).await.unwrap(),
299 #[cfg(target_os = "fuchsia")]
300 "loader" => Client::from_http_loader(
301 &fuchsia_component::client::connect_to_protocol::<fnet_http::LoaderMarker>()
302 .unwrap(),
303 url,
304 50,
305 )
306 .await
307 .unwrap(),
308 s => panic!("unexpected byte_soure {s}"),
309 }
310 }
311
312 #[test_case("hyper")]
313 #[cfg(target_os = "fuchsia")]
314 #[test_case("loader")]
315 #[fasync::run_singlethreaded(test)]
316 async fn receive_one_event(byte_source: &str) {
317 async fn handle_req(
318 _req: Request<hyper::body::Incoming>,
319 ) -> Result<Response<Body>, hyper::Error> {
320 Ok(Response::builder()
321 .status(StatusCode::OK)
322 .header("content-type", "text/event-stream")
323 .body(make_event().to_vec().into())
324 .unwrap())
325 }
326 let url = spawn_server(handle_req);
327
328 let client = make_client(byte_source, url).await;
329 let events: Result<Vec<_>, _> = client.collect::<Vec<_>>().await.into_iter().collect();
330
331 assert_eq!(events.unwrap(), vec![make_event()]);
332 }
333
334 #[test_case("hyper")]
335 #[cfg(target_os = "fuchsia")]
336 #[test_case("loader")]
337 #[fasync::run_singlethreaded(test)]
338 async fn client_sends_correct_http_headers(byte_source: &str) {
339 async fn handle_req(
340 req: Request<hyper::body::Incoming>,
341 ) -> Result<Response<Body>, hyper::Error> {
342 assert_eq!(req.method(), &hyper::Method::GET);
343 assert_eq!(
344 req.headers().get("accept").map(|h| h.as_bytes()),
345 Some(&b"text/event-stream"[..])
346 );
347 Ok(Response::builder()
348 .status(StatusCode::OK)
349 .header("content-type", "text/event-stream")
350 .body(make_event().to_vec().into())
351 .unwrap())
352 }
353 let url = spawn_server(handle_req);
354
355 let client = make_client(byte_source, url).await;
356 client.collect::<Vec<_>>().await;
357 }
358
359 #[fasync::run_singlethreaded(test)]
360 async fn error_create_request() {
361 assert_matches!(
362 Client::from_hyper_client(&new_https_client(), "\n").await,
363 Err(FromHyperClientError::CreateRequest(_))
364 );
365 }
366
367 #[fasync::run_singlethreaded(test)]
368 async fn error_make_request() {
369 assert_matches!(
370 Client::from_hyper_client(&new_https_client(), "bad_url2").await,
371 Err(FromHyperClientError::MakeRequest(_))
372 );
373 }
374
375 #[fasync::run_singlethreaded(test)]
376 async fn hyper_error_http_status() {
377 async fn handle_req(
378 _req: Request<hyper::body::Incoming>,
379 ) -> Result<Response<Body>, hyper::Error> {
380 Ok(Response::builder().status(StatusCode::NOT_FOUND).body(Body::empty()).unwrap())
381 }
382 let url = spawn_server(handle_req);
383
384 assert_matches!(
385 Client::from_hyper_client(&new_https_client(), url).await,
386 Err(FromHyperClientError::HttpStatus(_))
387 );
388 }
389
390 #[fasync::run_singlethreaded(test)]
391 async fn loader_error_http_status() {
392 async fn handle_req(
393 _req: Request<hyper::body::Incoming>,
394 ) -> Result<Response<Body>, hyper::Error> {
395 Ok(Response::builder().status(StatusCode::NOT_FOUND).body(Body::empty()).unwrap())
396 }
397 let url = spawn_server(handle_req);
398
399 assert_matches!(
400 Client::from_http_loader(
401 &fuchsia_component::client::connect_to_protocol::<fnet_http::LoaderMarker>()
402 .unwrap(),
403 url,
404 50,
405 )
406 .await,
407 Err(FromHttpLoaderError::FetchError { status_code: Some(404), .. })
408 );
409 }
410
411 #[fasync::run_singlethreaded(test)]
415 async fn error_downloading_chunk() {
416 const BODY_SIZE_LARGE_ENOUGH_TO_TRIGGER_DELAYED_STREAMING: usize = 1_000_000;
422
423 async fn handle_req(
424 _req: Request<hyper::body::Incoming>,
425 ) -> Result<Response<Body>, hyper::Error> {
426 Ok(Response::builder()
427 .status(StatusCode::OK)
428 .header(
429 "content-length",
430 &format!("{}", BODY_SIZE_LARGE_ENOUGH_TO_TRIGGER_DELAYED_STREAMING),
431 )
432 .header("content-type", "text/event-stream")
433 .body(Body::wrap_stream(futures::stream::iter(vec![
434 Ok(vec![b' '; BODY_SIZE_LARGE_ENOUGH_TO_TRIGGER_DELAYED_STREAMING - 1]),
435 Err("error-text".to_string()),
436 ])))
437 .unwrap())
438 }
439 let url = spawn_server(handle_req);
440 let mut client = Client::from_hyper_client(&new_https_client(), url).await.unwrap();
441
442 assert_matches!(client.try_next().await, Err(ClientPollError::NextChunk(_)));
443 }
444
445 #[test]
446 fn test_stream_from_socket() {
447 let mut executor = fasync::TestExecutor::new_with_fake_time();
448 let (snd, rcv) = zx::Socket::create_stream();
449 let mut stream = stream_from_socket(fasync::Socket::from_socket(rcv), 10);
450
451 let mut next = stream.next();
453 assert_matches!(executor.run_until_stalled(&mut next), Poll::Pending);
454
455 assert_eq!(snd.write(b"test msg").unwrap(), 8);
457 assert_matches!(
458 executor.run_until_stalled(&mut next),
459 Poll::Ready(Some(Ok(b))) if (&*b)[..] == *b"test msg".as_slice()
460 );
461 drop(next);
462
463 assert_eq!(snd.write(b"0000000000111").unwrap(), 13);
465 assert_matches!(
466 executor.run_until_stalled(&mut stream.next()),
467 Poll::Ready(Some(Ok(b))) if (&*b)[..] == *b"0000000000".as_slice()
468 );
469 assert_matches!(
470 executor.run_until_stalled(&mut stream.next()),
471 Poll::Ready(Some(Ok(b))) if (&*b)[..] == *b"111".as_slice()
472 );
473
474 drop(snd);
476 assert_matches!(executor.run_until_stalled(&mut stream.next()), Poll::Ready(None));
477 }
478}