Skip to main content

eggfetch_node/
response.rs

1//! Eggfetch Response for Node.js.
2
3use napi::bindgen_prelude::Buffer;
4use napi_derive::napi;
5use std::collections::HashMap;
6use std::ptr;
7
8/// HTTP response from eggfetch.
9#[napi]
10pub struct EggfetchResponse {
11    status: u32,
12    url: String,
13    /// All header pairs in wire order. Duplicates (e.g. multiple
14    /// `Set-Cookie` headers) are preserved; use `getAll` to observe
15    /// every value for a name.
16    headers: Vec<(String, String)>,
17    body: Vec<u8>,
18}
19
20impl EggfetchResponse {
21    /// Create from a raw FFI response handle, consuming it immediately.
22    pub(crate) fn from_raw(resp: *mut eggfetch_ffi::ResponseHandle) -> Self {
23        unsafe {
24            let status = u32::from(eggfetch_ffi::eggfetch_response_status(resp));
25            let url_ptr = eggfetch_ffi::eggfetch_response_url(resp);
26            let url = if url_ptr.is_null() {
27                String::new()
28            } else {
29                std::ffi::CStr::from_ptr(url_ptr)
30                    .to_string_lossy()
31                    .into_owned()
32            };
33            if !url_ptr.is_null() {
34                eggfetch_ffi::eggfetch_string_free(url_ptr);
35            }
36
37            let header_count = eggfetch_ffi::eggfetch_response_header_count(resp);
38            let mut headers = Vec::with_capacity(header_count);
39            for i in 0..header_count {
40                let mut name: *mut std::os::raw::c_char = ptr::null_mut();
41                let mut value: *mut std::os::raw::c_char = ptr::null_mut();
42                let rc = eggfetch_ffi::eggfetch_response_header(resp, i, &mut name, &mut value);
43                if rc == 0 {
44                    let n = if name.is_null() {
45                        String::new()
46                    } else {
47                        std::ffi::CStr::from_ptr(name)
48                            .to_string_lossy()
49                            .into_owned()
50                    };
51                    let v = if value.is_null() {
52                        String::new()
53                    } else {
54                        std::ffi::CStr::from_ptr(value)
55                            .to_string_lossy()
56                            .into_owned()
57                    };
58                    if !name.is_null() {
59                        eggfetch_ffi::eggfetch_string_free(name);
60                    }
61                    if !value.is_null() {
62                        eggfetch_ffi::eggfetch_string_free(value);
63                    }
64                    headers.push((n, v));
65                }
66            }
67
68            let mut body_ptr = ptr::null_mut();
69            let mut body_len = 0;
70            let body = if eggfetch_ffi::eggfetch_response_body(resp, &mut body_ptr, &mut body_len)
71                == 0
72                && body_len > 0
73                && !body_ptr.is_null()
74            {
75                let body = std::slice::from_raw_parts(body_ptr, body_len).to_vec();
76                eggfetch_ffi::eggfetch_body_free(body_ptr, body_len);
77                body
78            } else {
79                Vec::new()
80            };
81
82            eggfetch_ffi::eggfetch_response_free(resp);
83
84            Self {
85                status,
86                url,
87                headers,
88                body,
89            }
90        }
91    }
92}
93
94#[napi]
95impl EggfetchResponse {
96    /// HTTP status code.
97    #[napi(getter)]
98    pub fn status(&self) -> u32 {
99        self.status
100    }
101
102    /// Response URL.
103    #[napi(getter)]
104    pub fn url(&self) -> String {
105        self.url.clone()
106    }
107
108    /// Response body as text.
109    #[napi(getter)]
110    pub fn text(&self) -> String {
111        String::from_utf8_lossy(&self.body).into_owned()
112    }
113
114    /// Response body as a Node.js `Buffer`.
115    #[napi(getter)]
116    pub fn bytes(&self) -> Buffer {
117        Buffer::from(self.body.clone())
118    }
119
120    /// Response body as JSON (parsed).
121    ///
122    /// # Errors
123    ///
124    /// Returns an error if the body is not valid JSON.
125    #[napi(getter)]
126    pub fn json(&self) -> napi::Result<serde_json::Value> {
127        serde_json::from_slice(&self.body)
128            .map_err(|e| napi::Error::from_reason(format!("JSON parse error: {e}")))
129    }
130
131    /// Response headers as an object.
132    ///
133    /// When a header appears multiple times, the object joins the values
134    /// with `", "` (standard HTTP combining). Use [`Self::get_all`] to
135    /// retrieve every value individually.
136    #[napi(getter)]
137    pub fn headers(&self) -> HashMap<String, String> {
138        let mut map: HashMap<String, String> = HashMap::with_capacity(self.headers.len());
139        for (name, value) in &self.headers {
140            match map.entry(name.clone()) {
141                std::collections::hash_map::Entry::Occupied(mut existing) => {
142                    let joined = existing.get_mut();
143                    joined.push_str(", ");
144                    joined.push_str(value);
145                }
146                std::collections::hash_map::Entry::Vacant(slot) => {
147                    slot.insert(value.clone());
148                }
149            }
150        }
151        map
152    }
153
154    /// All values for a response header, case-insensitively.
155    ///
156    /// Unlike the `headers` object, this preserves duplicate headers
157    /// such as multiple `Set-Cookie` lines.
158    // N-API method arguments must be owned (`FromNapiValue`) values;
159    // taking `&str` is not expressible here.
160    #[allow(clippy::needless_pass_by_value)]
161    #[napi]
162    pub fn get_all(&self, name: String) -> Vec<String> {
163        self.headers
164            .iter()
165            .filter(|(header_name, _)| header_name.eq_ignore_ascii_case(&name))
166            .map(|(_, value)| value.clone())
167            .collect()
168    }
169
170    /// Whether the response status is 2xx.
171    #[napi(getter)]
172    pub fn ok(&self) -> bool {
173        (200..300).contains(&self.status)
174    }
175}