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