Skip to main content

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
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub struct Range {
20    /// The start offset in bytes, zero-indexed, inclusive
21    pub start: u64,
22    /// The end offset in bytes, inclusive
23    pub end: Option<u64>,
24}
25
26pub async fn fetch_url(
27    url: impl Into<String>,
28    range: Option<Range>,
29) -> Result<Vec<u8>, FetchUrlError> {
30    let http_svc = connect_to_protocol::<http::LoaderMarker>()
31        .map_err(FetchUrlError::FidlHttpServiceConnectionError)?;
32
33    let url_string = url.into();
34
35    // Support range requests to resume download of large blobs
36    let headers = if let Some(r) = &range {
37        let range_string = if let Some(end) = r.end {
38            format!("bytes={}-{}", r.start, end)
39        } else {
40            format!("bytes={}-", r.start)
41        };
42        Some(vec![Header { name: "Range".into(), value: range_string.into() }])
43    } else {
44        None
45    };
46
47    let url_request = http::Request {
48        url: Some(url_string),
49        method: Some(String::from("GET")),
50        headers: headers,
51        body: None,
52        deadline: None,
53        ..Default::default()
54    };
55
56    let response = http_svc.fetch(url_request).await.map_err(FetchUrlError::LoaderFIDLError)?;
57
58    debug!("got HTTP status {:?} for final URL {:?}", response.status_code, response.final_url);
59
60    if let Some(e) = response.error {
61        return Err(FetchUrlError::LoaderFetchError(e));
62    }
63
64    let zx_socket = response.body.ok_or(FetchUrlError::UrlReadBodyError)?;
65    let mut socket = fasync::Socket::from_socket(zx_socket);
66
67    if let Some(range) = range {
68        match response.status_code {
69            Some(HTTP_PARTIAL_CONTENT_OK) => {
70                let mut body = Vec::new();
71                let bytes_received = socket
72                    .read_to_end(&mut body)
73                    .await
74                    .map_err(FetchUrlError::ReadFromSocketError)?
75                    as u64;
76                let start = range.start;
77                if let Some(end) = range.end {
78                    let expected = end - start + 1;
79                    if bytes_received != expected {
80                        return Err(FetchUrlError::SizeReadMismatch(bytes_received, expected));
81                    }
82                }
83                debug!(
84                    "successfully fetched partial content starting from {}, {} bytes total",
85                    start,
86                    body.len()
87                );
88                Ok(body)
89            }
90            Some(code) => Err(FetchUrlError::UnexpectedHttpStatusCode(code)),
91            None => Err(FetchUrlError::NoStatusResponse),
92        }
93    } else {
94        match response.status_code {
95            Some(HTTP_OK) => {
96                let mut body = Vec::new();
97                let bytes_received = socket
98                    .read_to_end(&mut body)
99                    .await
100                    .map_err(FetchUrlError::ReadFromSocketError)?;
101                debug!("successfully fetched content, {} bytes total", bytes_received);
102                Ok(body)
103            }
104            Some(code) => Err(FetchUrlError::UnexpectedHttpStatusCode(code)),
105            None => Err(FetchUrlError::NoStatusResponse),
106        }
107    }
108}