runtime/globals/fetch/request/
mod.rs1use std::str::FromStr as _;
8
9use http::{HeaderMap, Method};
10use ion::class::{NativeObject as _, Reflector};
11use ion::function::Opt;
12use ion::typedarray::{ArrayBufferWrapper, Uint8ArrayWrapper};
13use ion::{ClassDefinition as _, Context, Error, ErrorKind, FromValue, Object, Promise, Result, TracedHeap, js_class};
14use mozjs::jsapi::{Heap, JSObject};
15pub use options::*;
16use url::Url;
17
18use crate::globals::abort::AbortSignal;
19use crate::globals::fetch::Headers;
20use crate::globals::fetch::body::FetchBody;
21use crate::globals::fetch::header::HeadersKind;
22use crate::promise::future_to_promise;
23
24mod options;
25
26#[derive(FromValue)]
27#[ion(inherit)]
28pub enum RequestInfo<'cx> {
29 Request(&'cx Request),
30 String(String),
31}
32
33#[js_class]
34pub struct Request {
35 reflector: Reflector,
36
37 pub(crate) headers: Box<Heap<*mut JSObject>>,
38 pub(crate) body: Option<FetchBody>,
39
40 #[trace(no_trace)]
41 pub(crate) method: Method,
42 #[trace(no_trace)]
43 pub(crate) url: Url,
44 #[trace(no_trace)]
45 pub(crate) locations: Vec<Url>,
46
47 pub(crate) referrer: Referrer,
48 pub(crate) referrer_policy: ReferrerPolicy,
49
50 pub(crate) mode: RequestMode,
51 pub(crate) credentials: RequestCredentials,
52 pub(crate) cache: RequestCache,
53 pub(crate) redirect: RequestRedirect,
54
55 pub(crate) integrity: String,
56
57 pub(crate) unsafe_request: bool,
58 pub(crate) keepalive: bool,
59
60 pub(crate) client_window: bool,
61 pub(crate) signal_object: Box<Heap<*mut JSObject>>,
62}
63
64#[js_class]
65impl Request {
66 #[ion(constructor)]
67 pub fn constructor(cx: &Context, info: RequestInfo, Opt(init): Opt<RequestInit>) -> Result<Request> {
68 let mut fallback_cors = false;
69
70 let mut request = match info {
71 RequestInfo::Request(request) => request.clone(),
72 RequestInfo::String(url) => {
73 let url = Url::from_str(&url)?;
74 if url.username() != "" || url.password().is_some() {
75 return Err(Error::new("Received URL with embedded credentials", ErrorKind::Type));
76 }
77
78 fallback_cors = true;
79
80 Request {
81 reflector: Reflector::default(),
82
83 headers: Box::default(),
84 body: None,
85
86 method: Method::GET,
87 url: url.clone(),
88 locations: vec![url],
89
90 referrer: Referrer::default(),
91 referrer_policy: ReferrerPolicy::default(),
92
93 mode: RequestMode::default(),
94 credentials: RequestCredentials::default(),
95 cache: RequestCache::default(),
96 redirect: RequestRedirect::default(),
97
98 integrity: String::new(),
99
100 unsafe_request: false,
101 keepalive: false,
102
103 client_window: true,
104 signal_object: Heap::boxed(AbortSignal::new_object(cx, Box::default())),
105 }
106 }
107 };
108
109 let mut headers = None;
110 let mut body = None;
111
112 if let Some(init) = init {
113 if let Some(window) = init.window {
114 if window.is_null() {
115 request.client_window = false;
116 } else {
117 return Err(Error::new("Received non-null window type", ErrorKind::Type));
118 }
119 }
120
121 if request.mode == RequestMode::Navigate {
122 request.mode = RequestMode::SameOrigin;
123 }
124
125 if let Some(referrer) = init.referrer {
126 request.referrer = referrer;
127 }
128 if let Some(policy) = init.referrer_policy {
129 request.referrer_policy = policy;
130 }
131
132 let mode = init.mode.or(fallback_cors.then_some(RequestMode::Cors));
133 if let Some(mode) = mode {
134 if mode == RequestMode::Navigate {
135 return Err(Error::new("Received 'navigate' mode", ErrorKind::Type));
136 }
137 request.mode = mode;
138 }
139
140 if let Some(credentials) = init.credentials {
141 request.credentials = credentials;
142 }
143 if let Some(cache) = init.cache {
144 request.cache = cache;
145 }
146 if let Some(redirect) = init.redirect {
147 request.redirect = redirect;
148 }
149 if let Some(integrity) = init.integrity {
150 request.integrity = integrity;
151 }
152 if let Some(keepalive) = init.keepalive {
153 request.keepalive = keepalive;
154 }
155
156 if let Some(signal_object) = init.signal {
157 request.signal_object.set(signal_object);
158 }
159
160 if let Some(mut method) = init.method {
161 method.make_ascii_uppercase();
162 let method = Method::from_str(&method)?;
163 if method == Method::CONNECT || method == Method::TRACE {
164 return Err(Error::new("Received invalid request method", ErrorKind::Type));
165 }
166 request.method = method;
167 }
168
169 headers = init.headers;
170 body = init.body;
171 }
172
173 if request.cache == RequestCache::OnlyIfCached && request.mode != RequestMode::SameOrigin {
174 return Err(Error::new(
175 "Request cache mode 'only-if-cached' can only be used with request mode 'same-origin'",
176 ErrorKind::Type,
177 ));
178 }
179
180 if request.mode == RequestMode::NoCors && !matches!(request.method, Method::GET | Method::HEAD | Method::POST) {
181 return Err(Error::new("Invalid request method", ErrorKind::Type));
182 }
183
184 let kind = if request.mode == RequestMode::NoCors {
185 HeadersKind::RequestNoCors
186 } else {
187 HeadersKind::Request
188 };
189
190 let mut headers = if let Some(headers) = headers {
191 headers.into_headers(HeaderMap::new(), kind)?
192 } else {
193 Headers {
194 reflector: Reflector::default(),
195 headers: HeaderMap::new(),
196 kind,
197 }
198 };
199
200 if let Some(body) = body {
201 body.add_content_type_header(&mut headers.headers);
202 request.body = Some(body);
203 }
204 request.headers.set(Headers::new_object(cx, Box::new(headers)));
205
206 Ok(request)
207 }
208
209 #[ion(get)]
210 pub fn get_method(&self) -> String {
211 self.method.to_string()
212 }
213
214 #[ion(get)]
215 pub fn get_url(&self) -> String {
216 self.url.to_string()
217 }
218
219 #[ion(get)]
220 pub fn get_headers(&self) -> *mut JSObject {
221 self.headers.get()
222 }
223
224 #[ion(get)]
225 pub fn get_destination(&self) -> String {
226 String::new()
227 }
228
229 #[ion(get)]
230 pub fn get_referrer(&self) -> String {
231 self.referrer.to_string()
232 }
233
234 #[ion(get)]
235 pub fn get_referrer_policy(&self) -> String {
236 self.referrer.to_string()
237 }
238
239 #[ion(get)]
240 pub fn get_mode(&self) -> String {
241 self.mode.to_string()
242 }
243
244 #[ion(get)]
245 pub fn get_credentials(&self) -> String {
246 self.credentials.to_string()
247 }
248
249 #[ion(get)]
250 pub fn get_cache(&self) -> String {
251 self.cache.to_string()
252 }
253
254 #[ion(get)]
255 pub fn get_redirect(&self) -> String {
256 self.redirect.to_string()
257 }
258
259 #[ion(get)]
260 pub fn get_integrity(&self) -> String {
261 self.integrity.clone()
262 }
263
264 #[ion(get)]
265 pub fn get_keepalive(&self) -> bool {
266 self.keepalive
267 }
268
269 #[ion(get)]
270 pub fn get_is_reload_navigation(&self) -> bool {
271 false
272 }
273
274 #[ion(get)]
275 pub fn get_is_history_navigation(&self) -> bool {
276 false
277 }
278
279 #[ion(get)]
280 pub fn get_signal(&self) -> *mut JSObject {
281 self.signal_object.get()
282 }
283
284 #[ion(get)]
285 pub fn get_duplex(&self) -> String {
286 String::from("half")
287 }
288
289 #[ion(get)]
290 pub fn get_body_used(&self) -> bool {
291 self.body.is_none()
292 }
293
294 async fn read_to_bytes(&mut self) -> Result<Vec<u8>> {
295 if let Some(body) = self.body.take() {
296 body.read_to_bytes().await
297 } else {
298 Err(Error::new("Request body has already been used.", None))
299 }
300 }
301
302 #[ion(name = "arrayBuffer")]
303 pub fn array_buffer<'cx>(&mut self, cx: &'cx Context) -> Option<Promise<'cx>> {
304 let this = TracedHeap::new(self.reflector().get());
305 future_to_promise::<_, _, _, Error>(cx, |cx| async move {
306 let request = Object::from(this.to_local());
307 let request = Request::get_mut_private(&cx, &request)?;
308 let bytes = request.read_to_bytes().await?;
309 Ok(ArrayBufferWrapper::from(bytes))
310 })
311 }
312
313 pub fn bytes<'cx>(&mut self, cx: &'cx Context) -> Option<Promise<'cx>> {
314 let this = TracedHeap::new(self.reflector().get());
315 future_to_promise::<_, _, _, Error>(cx, |cx| async move {
316 let request = Object::from(this.to_local());
317 let request = Request::get_mut_private(&cx, &request)?;
318 let bytes = request.read_to_bytes().await?;
319 Ok(Uint8ArrayWrapper::from(bytes))
320 })
321 }
322
323 pub fn text<'cx>(&mut self, cx: &'cx Context) -> Option<Promise<'cx>> {
324 let this = TracedHeap::new(self.reflector().get());
325 future_to_promise::<_, _, _, Error>(cx, |cx| async move {
326 let request = Object::from(this.to_local());
327 let request = Request::get_mut_private(&cx, &request)?;
328 let bytes = request.read_to_bytes().await?;
329 String::from_utf8(bytes).map_err(|e| Error::new(format!("Invalid UTF-8 sequence: {e}"), None))
330 })
331 }
332}
333
334impl Clone for Request {
335 fn clone(&self) -> Request {
336 let url = self.locations.last().unwrap().clone();
337
338 Request {
339 reflector: Reflector::default(),
340
341 headers: Box::default(),
342 body: self.body.clone(),
343
344 method: self.method.clone(),
345 url: url.clone(),
346 locations: vec![url],
347
348 referrer: self.referrer.clone(),
349 referrer_policy: self.referrer_policy,
350
351 mode: self.mode,
352 credentials: self.credentials,
353 cache: self.cache,
354 redirect: self.redirect,
355
356 integrity: self.integrity.clone(),
357
358 unsafe_request: true,
359 keepalive: self.keepalive,
360
361 client_window: self.client_window,
362 signal_object: Heap::boxed(self.signal_object.get()),
363 }
364 }
365}