Skip to main content

nex_protocol/
client.rs

1//! The nex client: send a path, read until close.
2
3use std::time::Duration;
4
5use tokio::io::{AsyncReadExt, AsyncWriteExt};
6use tokio::net::TcpStream;
7use url::Url;
8
9use super::NEX_PORT;
10
11/// Options for a [`fetch`].
12#[derive(Debug, Clone)]
13pub struct FetchOptions {
14    /// Per-step timeout (connect, write, read). Default 30s.
15    pub timeout: Duration,
16    /// Refuse responses larger than this. Default 16 MiB.
17    pub max_body: usize,
18    /// Connect here instead of resolving the URL's host. For tests and odd
19    /// deployments.
20    pub connect_addr: Option<std::net::SocketAddr>,
21}
22
23impl Default for FetchOptions {
24    fn default() -> Self {
25        Self {
26            timeout: Duration::from_secs(30),
27            max_body: 16 * 1024 * 1024,
28            connect_addr: None,
29        }
30    }
31}
32
33/// Why a fetch failed. Nex has no protocol-level errors: a successful
34/// exchange is whatever bytes the server sent.
35#[derive(Debug)]
36pub enum ClientError {
37    BadUrl(String),
38    Io(String),
39    Timeout(&'static str),
40    BodyTooLarge { max: usize },
41}
42
43impl std::fmt::Display for ClientError {
44    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        match self {
46            Self::BadUrl(message) => write!(formatter, "bad nex URL: {message}"),
47            Self::Io(message) => write!(formatter, "nex IO error: {message}"),
48            Self::Timeout(step) => write!(formatter, "nex {step} timed out"),
49            Self::BodyTooLarge { max } => {
50                write!(formatter, "nex response exceeds {max} bytes")
51            }
52        }
53    }
54}
55
56impl std::error::Error for ClientError {}
57
58/// Fetch a `nex://` URL and return the raw response bytes.
59pub async fn fetch(url: &str, options: &FetchOptions) -> Result<Vec<u8>, ClientError> {
60    let url = Url::parse(url).map_err(|error| ClientError::BadUrl(error.to_string()))?;
61    if url.scheme() != "nex" {
62        return Err(ClientError::BadUrl(format!(
63            "expected nex:// scheme, got {}://",
64            url.scheme()
65        )));
66    }
67    let host = url
68        .host_str()
69        .ok_or_else(|| ClientError::BadUrl("URL has no host".to_string()))?;
70    let port = url.port().unwrap_or(NEX_PORT);
71    fetch_path(host, port, url.path(), options).await
72}
73
74/// Fetch by explicit host/port/path. The path may be empty (the spec allows
75/// an empty selector for the root).
76pub async fn fetch_path(
77    host: &str,
78    port: u16,
79    path: &str,
80    options: &FetchOptions,
81) -> Result<Vec<u8>, ClientError> {
82    let connect = async {
83        match options.connect_addr {
84            Some(addr) => TcpStream::connect(addr).await,
85            None => TcpStream::connect((host, port)).await,
86        }
87    };
88    let mut stream = tokio::time::timeout(options.timeout, connect)
89        .await
90        .map_err(|_| ClientError::Timeout("connect"))?
91        .map_err(|error| ClientError::Io(error.to_string()))?;
92
93    let request = format!("{path}\r\n");
94    tokio::time::timeout(options.timeout, stream.write_all(request.as_bytes()))
95        .await
96        .map_err(|_| ClientError::Timeout("request write"))?
97        .map_err(|error| ClientError::Io(error.to_string()))?;
98
99    let read = async {
100        let mut body = Vec::new();
101        let mut chunk = [0u8; 8192];
102        loop {
103            let count = stream.read(&mut chunk).await?;
104            if count == 0 {
105                break;
106            }
107            body.extend_from_slice(&chunk[..count]);
108            if body.len() > options.max_body {
109                return Ok::<_, std::io::Error>(None);
110            }
111        }
112        Ok(Some(body))
113    };
114    tokio::time::timeout(options.timeout, read)
115        .await
116        .map_err(|_| ClientError::Timeout("response read"))?
117        .map_err(|error| ClientError::Io(error.to_string()))?
118        .ok_or(ClientError::BodyTooLarge {
119            max: options.max_body,
120        })
121}