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    mut headers: Vec<Header>,
30) -> Result<Vec<u8>, FetchUrlError> {
31    let http_svc = connect_to_protocol::<http::LoaderMarker>()
32        .map_err(FetchUrlError::FidlHttpServiceConnectionError)?;
33
34    let url_string = url.into();
35
36    if let Some(r) = &range {
37        const RANGE_HEADER_NAME: &[u8] = b"range";
38        if headers.iter().any(|h| h.name.eq_ignore_ascii_case(RANGE_HEADER_NAME)) {
39            return Err(FetchUrlError::DuplicateRangeHeader);
40        }
41
42        let range_string = if let Some(end) = r.end {
43            format!("bytes={}-{}", r.start, end)
44        } else {
45            format!("bytes={}-", r.start)
46        };
47        headers.push(Header { name: RANGE_HEADER_NAME.into(), value: range_string.into() });
48    }
49
50    let url_request = http::Request {
51        url: Some(url_string),
52        method: Some(String::from("GET")),
53        headers: if headers.is_empty() { None } else { Some(headers) },
54        body: None,
55        deadline: None,
56        ..Default::default()
57    };
58
59    let response = http_svc.fetch(url_request).await.map_err(FetchUrlError::LoaderFIDLError)?;
60
61    debug!("got HTTP status {:?} for final URL {:?}", response.status_code, response.final_url);
62
63    if let Some(e) = response.error {
64        return Err(FetchUrlError::LoaderFetchError(e));
65    }
66
67    let zx_socket = response.body.ok_or(FetchUrlError::UrlReadBodyError)?;
68    let mut socket = fasync::Socket::from_socket(zx_socket);
69
70    if let Some(range) = range {
71        match response.status_code {
72            Some(HTTP_PARTIAL_CONTENT_OK) => {
73                let mut body = Vec::new();
74                let bytes_received = socket
75                    .read_to_end(&mut body)
76                    .await
77                    .map_err(FetchUrlError::ReadFromSocketError)?
78                    as u64;
79                let start = range.start;
80                if let Some(end) = range.end {
81                    let expected = end - start + 1;
82                    if bytes_received != expected {
83                        return Err(FetchUrlError::SizeReadMismatch(bytes_received, expected));
84                    }
85                }
86                debug!(
87                    "successfully fetched partial content starting from {}, {} bytes total",
88                    start,
89                    body.len()
90                );
91                Ok(body)
92            }
93            Some(code) => Err(FetchUrlError::UnexpectedHttpStatusCode(code)),
94            None => Err(FetchUrlError::NoStatusResponse),
95        }
96    } else {
97        match response.status_code {
98            Some(HTTP_OK | HTTP_PARTIAL_CONTENT_OK) => {
99                let mut body = Vec::new();
100                let bytes_received = socket
101                    .read_to_end(&mut body)
102                    .await
103                    .map_err(FetchUrlError::ReadFromSocketError)?;
104                debug!("successfully fetched content, {} bytes total", bytes_received);
105                Ok(body)
106            }
107            Some(code) => Err(FetchUrlError::UnexpectedHttpStatusCode(code)),
108            None => Err(FetchUrlError::NoStatusResponse),
109        }
110    }
111}