fetch_url/
lib.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use fidl_fuchsia_net_http::{self as http, Header};
6use fuchsia_async as fasync;
7use fuchsia_component::client::connect_to_protocol;
8use futures::AsyncReadExt as _;
9use log::debug;
10
11pub mod errors;
12use errors::FetchUrlError;
13
14const HTTP_PARTIAL_CONTENT_OK: u32 = 206;
15const HTTP_OK: u32 = 200;
16
17/// The byte range of the fetch request
18pub struct Range {
19    /// The start offset in bytes, zero-indexed, inclusive
20    start: u64,
21    /// The end offset in bytes, inclusive
22    end: Option<u64>,
23}
24
25pub async fn fetch_url(
26    url: impl Into<String>,
27    range: Option<Range>,
28) -> Result<Vec<u8>, FetchUrlError> {
29    let http_svc = connect_to_protocol::<http::LoaderMarker>()
30        .map_err(FetchUrlError::FidlHttpServiceConnectionError)?;
31
32    let url_string = url.into();
33
34    // Support range requests to resume download of large blobs
35    let headers = if let Some(r) = &range {
36        let range_string = if let Some(end) = r.end {
37            format!("bytes={}-{}", r.start, end)
38        } else {
39            format!("bytes={}-", r.start)
40        };
41        Some(vec![Header { name: "Range".into(), value: range_string.into() }])
42    } else {
43        None
44    };
45
46    let url_request = http::Request {
47        url: Some(url_string),
48        method: Some(String::from("GET")),
49        headers: headers,
50        body: None,
51        deadline: None,
52        ..Default::default()
53    };
54
55    let response = http_svc.fetch(url_request).await.map_err(FetchUrlError::LoaderFIDLError)?;
56
57    debug!("got HTTP status {:?} for final URL {:?}", response.status_code, response.final_url);
58
59    if let Some(e) = response.error {
60        return Err(FetchUrlError::LoaderFetchError(e));
61    }
62
63    let zx_socket = response.body.ok_or(FetchUrlError::UrlReadBodyError)?;
64    let mut socket = fasync::Socket::from_socket(zx_socket);
65
66    if let Some(range) = range {
67        match response.status_code {
68            Some(HTTP_PARTIAL_CONTENT_OK) => {
69                let mut body = Vec::new();
70                let bytes_received = socket
71                    .read_to_end(&mut body)
72                    .await
73                    .map_err(FetchUrlError::ReadFromSocketError)?
74                    as u64;
75                let start = range.start;
76                if let Some(end) = range.end {
77                    let expected = end - start + 1;
78                    if bytes_received != expected {
79                        return Err(FetchUrlError::SizeReadMismatch(bytes_received, expected));
80                    }
81                }
82                debug!(
83                    "successfully fetched partial content starting from {}, {} bytes total",
84                    start,
85                    body.len()
86                );
87                Ok(body)
88            }
89            Some(code) => Err(FetchUrlError::UnexpectedHttpStatusCode(code)),
90            None => Err(FetchUrlError::NoStatusResponse),
91        }
92    } else {
93        match response.status_code {
94            Some(HTTP_OK) => {
95                let mut body = Vec::new();
96                let bytes_received = socket
97                    .read_to_end(&mut body)
98                    .await
99                    .map_err(FetchUrlError::ReadFromSocketError)?;
100                debug!("successfully fetched content, {} bytes total", bytes_received);
101                Ok(body)
102            }
103            Some(code) => Err(FetchUrlError::UnexpectedHttpStatusCode(code)),
104            None => Err(FetchUrlError::NoStatusResponse),
105        }
106    }
107}