-
-
Notifications
You must be signed in to change notification settings - Fork 622
Expand file tree
/
Copy pathrequest.rs
More file actions
240 lines (217 loc) · 8.4 KB
/
request.rs
File metadata and controls
240 lines (217 loc) · 8.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
//! The `Request` JavaScript class and adjacent types, implemented as [`JsRequest`].
//!
//! See the [Request interface documentation][mdn] for more information.
//!
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Request
use super::HttpRequest;
use super::headers::JsHeaders;
use boa_engine::value::{Convert, TryFromJs};
use boa_engine::{
Finalize, JsData, JsObject, JsResult, JsString, JsValue, Trace, boa_class, js_error,
};
use either::Either;
use std::mem;
/// A [RequestInit][mdn] object. This is a JavaScript object (not a
/// class) that can be used as options for creating a [`JsRequest`].
///
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/RequestInit
// TODO: This class does not contain all fields that are defined in the spec.
#[derive(Debug, Clone, TryFromJs, Trace, Finalize)]
pub struct RequestInit {
body: Option<JsValue>,
headers: Option<JsHeaders>,
method: Option<Convert<JsString>>,
signal: Option<JsObject>,
}
impl RequestInit {
/// Takes the abort signal from the options, if present.
pub fn take_signal(&mut self) -> Option<JsObject> {
self.signal.take()
}
/// Create an [`http::request::Builder`] object and return both the
/// body specified by JavaScript and the builder.
///
/// # Errors
/// If the body is not a valid type, an error is returned.
pub fn into_request_builder(
mut self,
request: Option<HttpRequest<Vec<u8>>>,
) -> JsResult<HttpRequest<Vec<u8>>> {
let mut builder = HttpRequest::builder();
let mut inherited_body = None;
if let Some(r) = request {
let (parts, body) = r.into_parts();
// https://fetch.spec.whatwg.org/#dom-request - "Let inputBody be input's request's body if input is a Request object; otherwise null."
inherited_body = Some(body);
builder = builder
.method(parts.method)
.uri(parts.uri)
.version(parts.version);
for (key, value) in &parts.headers {
builder = builder.header(key, value);
}
}
if let Some(headers) = self.headers.take() {
for (k, v) in headers.as_header_map().borrow().iter() {
builder = builder.header(k, v);
}
}
if let Some(Convert(ref method)) = self.method.take() {
let method = method.to_std_string().map_err(
|_| js_error!(TypeError: "Request constructor: {} is an invalid method", method.to_std_string_escaped()),
)?;
// 25. If init["method"] exists, then:
// 1. Let method be init["method"].
// 2. If method is not a method or method is a forbidden method, throw a TypeError.
// 3. Normalize method.
// 4. Set request's method to method.
// https://fetch.spec.whatwg.org/#dom-request
if method.eq_ignore_ascii_case("CONNECT")
|| method.eq_ignore_ascii_case("TRACE")
|| method.eq_ignore_ascii_case("TRACK")
{
return Err(js_error!(
TypeError: "'{}' HTTP method is unsupported.",
method
));
}
let is_get_or_head_method =
method.eq_ignore_ascii_case("GET") || method.eq_ignore_ascii_case("HEAD");
// Fetch Standard §5.4 Request constructor:
// If either init["body"] exists and is non-null or inputBody is non-null,
// and request's method is GET or HEAD, then throw a TypeError.
// https://fetch.spec.whatwg.org/#dom-request
if is_get_or_head_method && (self.body.is_some() || inherited_body.is_some()) {
return Err(js_error!(TypeError: "Request with GET/HEAD method cannot have body."));
}
builder = builder.method(method.as_str());
}
let request_body = if let Some(body) = &self.body {
// TODO: add more support types.
if let Some(body) = body.as_string() {
let body = body.to_std_string().map_err(
|_| js_error!(TypeError: "Request constructor: body is not a valid string"),
)?;
body.into_bytes()
} else {
return Err(
js_error!(TypeError: "Request constructor: body is not a supported type"),
);
}
} else {
inherited_body.unwrap_or_default()
};
let request = builder
.body(request_body)
.map_err(|_| js_error!(Error: "Cannot construct request"))?;
Ok(request)
}
}
/// The JavaScript `Request` class.
///
/// The `Request` interface of the [Fetch API][mdn] represents a resource request.
///
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
#[derive(Clone, Debug, JsData, Trace, Finalize)]
pub struct JsRequest {
#[unsafe_ignore_trace]
inner: HttpRequest<Vec<u8>>,
signal: Option<JsObject>,
}
impl JsRequest {
/// Get the inner `http::Request` object. This drops the body (if any).
pub fn into_inner(mut self) -> HttpRequest<Vec<u8>> {
mem::replace(&mut self.inner, HttpRequest::new(Vec::new()))
}
/// Split this request into its HTTP request and abort signal.
fn into_parts(mut self) -> (HttpRequest<Vec<u8>>, Option<JsObject>) {
let request = mem::replace(&mut self.inner, HttpRequest::new(Vec::new()));
let signal = self.signal.take();
(request, signal)
}
/// Get a reference to the inner `http::Request` object.
pub fn inner(&self) -> &HttpRequest<Vec<u8>> {
&self.inner
}
/// Get the abort signal associated with this request, if any.
pub(crate) fn signal(&self) -> Option<JsObject> {
self.signal.clone()
}
/// Get the URI of the request.
pub fn uri(&self) -> &http::Uri {
self.inner.uri()
}
/// Create a [`JsRequest`] instance from JavaScript arguments, similar to
/// calling its constructor in JavaScript.
///
/// # Errors
/// If the URI is invalid, an error is returned.
pub fn create_from_js(
input: Either<JsString, JsRequest>,
options: Option<RequestInit>,
) -> JsResult<Self> {
let (request, signal) = match input {
Either::Left(uri) => {
let uri = http::Uri::try_from(
uri.to_std_string()
.map_err(|_| js_error!(URIError: "URI cannot have unpaired surrogates"))?,
)
.map_err(|_| js_error!(URIError: "Invalid URI"))?;
let request = http::request::Request::builder()
.uri(uri)
.body(Vec::<u8>::new())
.map_err(|_| js_error!(Error: "Cannot construct request"))?;
(request, None)
}
Either::Right(r) => r.into_parts(),
};
if let Some(mut options) = options {
let signal = options.take_signal().or(signal);
let inner = options.into_request_builder(Some(request))?;
Ok(Self { inner, signal })
} else {
Ok(Self {
inner: request,
signal,
})
}
}
}
impl From<HttpRequest<Vec<u8>>> for JsRequest {
fn from(inner: HttpRequest<Vec<u8>>) -> Self {
Self {
inner,
signal: None,
}
}
}
#[boa_class(rename = "Request")]
#[boa(rename_all = "camelCase")]
impl JsRequest {
/// # Errors
/// Will return an error if the URL or any underlying error occurred in the
/// context.
#[boa(constructor)]
pub fn constructor(
input: Either<JsString, JsObject>,
options: Option<RequestInit>,
) -> JsResult<Self> {
// Need to use a match as `Either::map_right` does not have an equivalent
// `Either::map_right_ok`.
let input = match input {
Either::Right(r) => {
if let Ok(request) = r.clone().downcast::<JsRequest>() {
Either::Right(request.borrow().data().clone())
} else {
return Err(js_error!(TypeError: "invalid input argument"));
}
}
Either::Left(i) => Either::Left(i),
};
JsRequest::create_from_js(input, options)
}
#[boa(rename = "clone")]
fn clone_request(&self) -> Self {
self.clone()
}
}