1use crate::FieldMap;
2use crate::p3::bindings::clocks::monotonic_clock::Duration;
3use crate::p3::bindings::http::types::{
4 ErrorCode, FieldName, FieldValue, Fields, HeaderError, Headers, Host, HostFields, HostRequest,
5 HostRequestOptions, HostRequestWithStore, HostResponse, HostResponseWithStore, Method, Request,
6 RequestOptions, RequestOptionsError, Response, Scheme, StatusCode, Trailers,
7};
8use crate::p3::body::{Body, HostBodyStreamProducer};
9use crate::p3::{HeaderResult, HttpError, RequestOptionsResult, WasiHttp, WasiHttpCtxView};
10use core::mem;
11use core::pin::Pin;
12use core::task::{Context, Poll, ready};
13use http::header::CONTENT_LENGTH;
14use std::sync::Arc;
15use tokio::sync::oneshot;
16use wasmtime::component::{
17 Access, FutureProducer, FutureReader, Resource, ResourceTable, StreamReader,
18};
19use wasmtime::error::Context as _;
20use wasmtime::{AsContextMut, StoreContextMut};
21
22fn get_fields<'a>(
23 table: &'a ResourceTable,
24 fields: &Resource<Fields>,
25) -> wasmtime::Result<&'a Fields> {
26 table
27 .get(&fields)
28 .context("failed to get fields from table")
29}
30
31fn get_fields_mut<'a>(
32 table: &'a mut ResourceTable,
33 fields: &Resource<Fields>,
34) -> HeaderResult<&'a mut Fields> {
35 table
36 .get_mut(&fields)
37 .context("failed to get fields from table")
38 .map_err(crate::p3::HeaderError::trap)
39}
40
41fn push_fields(table: &mut ResourceTable, fields: Fields) -> wasmtime::Result<Resource<Fields>> {
42 table.push(fields).context("failed to push fields to table")
43}
44
45fn delete_fields(table: &mut ResourceTable, fields: Resource<Fields>) -> wasmtime::Result<Fields> {
46 let mut fields = table
47 .delete(fields)
48 .context("failed to delete fields from table")?;
49 fields.set_immutable();
54 Ok(fields)
55}
56
57fn get_request<'a>(
58 table: &'a ResourceTable,
59 req: &Resource<Request>,
60) -> wasmtime::Result<&'a Request> {
61 table.get(req).context("failed to get request from table")
62}
63
64fn get_request_mut<'a>(
65 table: &'a mut ResourceTable,
66 req: &Resource<Request>,
67) -> wasmtime::Result<&'a mut Request> {
68 table
69 .get_mut(req)
70 .context("failed to get request from table")
71}
72
73fn get_response<'a>(
74 table: &'a ResourceTable,
75 res: &Resource<Response>,
76) -> wasmtime::Result<&'a Response> {
77 table.get(res).context("failed to get response from table")
78}
79
80fn get_response_mut<'a>(
81 table: &'a mut ResourceTable,
82 res: &Resource<Response>,
83) -> wasmtime::Result<&'a mut Response> {
84 table
85 .get_mut(res)
86 .context("failed to get response from table")
87}
88
89fn get_request_options<'a>(
90 table: &'a ResourceTable,
91 opts: &Resource<RequestOptions>,
92) -> wasmtime::Result<&'a RequestOptions> {
93 table
94 .get(opts)
95 .context("failed to get request options from table")
96}
97
98fn get_request_options_mut<'a>(
99 table: &'a mut ResourceTable,
100 opts: &Resource<RequestOptions>,
101) -> RequestOptionsResult<&'a mut RequestOptions> {
102 table
103 .get_mut(opts)
104 .context("failed to get request options from table")
105 .map_err(crate::p3::RequestOptionsError::trap)
106}
107
108fn push_request_options(
109 table: &mut ResourceTable,
110 opts: RequestOptions,
111) -> wasmtime::Result<Resource<RequestOptions>> {
112 table
113 .push(opts)
114 .context("failed to push request options to table")
115}
116
117fn delete_request_options(
118 table: &mut ResourceTable,
119 opts: Resource<RequestOptions>,
120) -> wasmtime::Result<RequestOptions> {
121 table
122 .delete(opts)
123 .context("failed to delete request options from table")
124}
125
126fn parse_header_value(
127 name: &http::HeaderName,
128 value: impl AsRef<[u8]>,
129) -> Result<http::HeaderValue, HeaderError> {
130 if name == CONTENT_LENGTH {
131 let s = str::from_utf8(value.as_ref()).or(Err(HeaderError::InvalidSyntax))?;
132 if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
136 return Err(HeaderError::InvalidSyntax);
137 }
138 let v: u64 = s.parse().or(Err(HeaderError::InvalidSyntax))?;
139 Ok(v.into())
140 } else {
141 http::HeaderValue::from_bytes(value.as_ref()).or(Err(HeaderError::InvalidSyntax))
142 }
143}
144
145enum GuestBodyResultProducer {
146 Receiver(oneshot::Receiver<Box<dyn Future<Output = Result<(), ErrorCode>> + Send>>),
147 Future(Pin<Box<dyn Future<Output = Result<(), ErrorCode>> + Send>>),
148}
149
150fn poll_future<T>(
151 cx: &mut Context<'_>,
152 fut: Pin<&mut (impl Future<Output = T> + ?Sized)>,
153 finish: bool,
154) -> Poll<Option<T>> {
155 match fut.poll(cx) {
156 Poll::Ready(v) => Poll::Ready(Some(v)),
157 Poll::Pending if finish => Poll::Ready(None),
158 Poll::Pending => Poll::Pending,
159 }
160}
161
162impl<D> FutureProducer<D> for GuestBodyResultProducer {
163 type Item = Result<(), ErrorCode>;
164
165 fn poll_produce(
166 mut self: Pin<&mut Self>,
167 cx: &mut Context<'_>,
168 _: StoreContextMut<D>,
169 finish: bool,
170 ) -> Poll<wasmtime::Result<Option<Self::Item>>> {
171 match &mut *self {
172 Self::Receiver(rx) => {
173 match ready!(poll_future(cx, Pin::new(rx), finish)) {
174 Some(Ok(fut)) => {
175 let mut fut = Box::into_pin(fut);
176 let res = poll_future(cx, fut.as_mut(), finish);
178 *self = Self::Future(fut);
179 res.map(Ok)
180 }
181 Some(Err(..)) => {
182 Poll::Ready(Ok(Some(Ok(()))))
184 }
185 None => Poll::Ready(Ok(None)),
186 }
187 }
188 Self::Future(fut) => poll_future(cx, fut.as_mut(), finish).map(Ok),
189 }
190 }
191}
192
193impl HostFields for WasiHttpCtxView<'_> {
194 fn new(&mut self) -> wasmtime::Result<Resource<Fields>> {
195 push_fields(self.table, FieldMap::new_mutable(self.ctx.field_size_limit))
196 }
197
198 fn from_list(
199 &mut self,
200 entries: Vec<(FieldName, FieldValue)>,
201 ) -> HeaderResult<Resource<Fields>> {
202 let mut fields = FieldMap::new_mutable(self.ctx.field_size_limit);
203 for (name, value) in entries {
204 let name = name.parse().or(Err(HeaderError::InvalidSyntax))?;
205 if self.hooks.is_forbidden_header(&name) {
206 return Err(HeaderError::Forbidden.into());
207 }
208 let value = parse_header_value(&name, value)?;
209 fields.append(name, value)?;
210 }
211 let fields = push_fields(self.table, fields).map_err(crate::p3::HeaderError::trap)?;
212 Ok(fields)
213 }
214
215 fn get(
216 &mut self,
217 fields: Resource<Fields>,
218 name: FieldName,
219 ) -> wasmtime::Result<Vec<FieldValue>> {
220 let fields = get_fields(self.table, &fields)?;
221 Ok(fields
222 .get_all(name)
223 .into_iter()
224 .map(|val| val.as_bytes().into())
225 .collect())
226 }
227
228 fn has(&mut self, fields: Resource<Fields>, name: FieldName) -> wasmtime::Result<bool> {
229 let fields = get_fields(self.table, &fields)?;
230 Ok(fields.contains_key(name))
231 }
232
233 fn set(
234 &mut self,
235 fields: Resource<Fields>,
236 name: FieldName,
237 value: Vec<FieldValue>,
238 ) -> HeaderResult<()> {
239 let name = name.parse().map_err(|_| HeaderError::InvalidSyntax)?;
240 if self.hooks.is_forbidden_header(&name) {
241 return Err(HeaderError::Forbidden.into());
242 }
243 let mut values = Vec::with_capacity(value.len());
244 for value in value {
245 let value = parse_header_value(&name, value)?;
246 values.push(value);
247 }
248 get_fields_mut(self.table, &fields)?.set(name, values)?;
249 Ok(())
250 }
251
252 fn delete(&mut self, fields: Resource<Fields>, name: FieldName) -> HeaderResult<()> {
253 let name = name.parse().map_err(|_| HeaderError::InvalidSyntax)?;
254 if self.hooks.is_forbidden_header(&name) {
255 return Err(HeaderError::Forbidden.into());
256 }
257 get_fields_mut(self.table, &fields)?.remove_all(name)?;
258 Ok(())
259 }
260
261 fn get_and_delete(
262 &mut self,
263 fields: Resource<Fields>,
264 name: FieldName,
265 ) -> HeaderResult<Vec<FieldValue>> {
266 let name = name.parse().or(Err(HeaderError::InvalidSyntax))?;
267 if self.hooks.is_forbidden_header(&name) {
268 return Err(HeaderError::Forbidden.into());
269 }
270 let values = get_fields_mut(self.table, &fields)?
271 .remove_all(name)?
272 .into_iter();
273 Ok(values.map(|value| value.as_bytes().into()).collect())
274 }
275
276 fn append(
277 &mut self,
278 fields: Resource<Fields>,
279 name: FieldName,
280 value: FieldValue,
281 ) -> HeaderResult<()> {
282 let name = name.parse().or(Err(HeaderError::InvalidSyntax))?;
283 if self.hooks.is_forbidden_header(&name) {
284 return Err(HeaderError::Forbidden.into());
285 }
286 let value = parse_header_value(&name, value)?;
287 get_fields_mut(self.table, &fields)?.append(name, value)?;
288 Ok(())
289 }
290
291 fn copy_all(
292 &mut self,
293 fields: Resource<Fields>,
294 ) -> wasmtime::Result<Vec<(FieldName, FieldValue)>> {
295 let fields = get_fields(self.table, &fields)?;
296 let fields = fields
297 .iter()
298 .map(|(name, value)| (name.as_str().into(), value.as_bytes().into()))
299 .collect();
300 Ok(fields)
301 }
302
303 fn clone(&mut self, fields: Resource<Fields>) -> wasmtime::Result<Resource<Fields>> {
304 let mut fields = get_fields(self.table, &fields)?.clone();
305 fields.set_mutable(self.ctx.field_size_limit);
306 push_fields(self.table, fields)
307 }
308
309 fn drop(&mut self, fields: Resource<Fields>) -> wasmtime::Result<()> {
310 delete_fields(self.table, fields)?;
311 Ok(())
312 }
313}
314
315impl<T> HostRequestWithStore<T> for WasiHttp {
316 fn new(
317 mut store: Access<T, Self>,
318 headers: Resource<Headers>,
319 contents: Option<StreamReader<u8>>,
320 trailers: FutureReader<Result<Option<Resource<Trailers>>, ErrorCode>>,
321 options: Option<Resource<RequestOptions>>,
322 ) -> wasmtime::Result<(Resource<Request>, FutureReader<Result<(), ErrorCode>>)> {
323 let (result_tx, result_rx) = oneshot::channel();
324 let body = match contents
325 .map(|rx| rx.try_into::<HostBodyStreamProducer<T>>(store.as_context_mut()))
326 {
327 Some(Ok(mut producer)) => Body::Host {
328 body: mem::take(&mut producer.body),
329 result_tx,
330 },
331 Some(Err(rx)) => Body::Guest {
332 contents_rx: Some(rx),
333 trailers_rx: trailers,
334 result_tx,
335 },
336 None => Body::Guest {
337 contents_rx: None,
338 trailers_rx: trailers,
339 result_tx,
340 },
341 };
342 let WasiHttpCtxView { table, .. } = store.get();
343 let headers = delete_fields(table, headers)?;
344 let options = options
345 .map(|options| delete_request_options(table, options))
346 .transpose()?;
347 let req = Request {
348 method: http::Method::GET,
349 scheme: None,
350 authority: None,
351 path_with_query: None,
352 headers,
353 options: options.map(Into::into),
354 body,
355 };
356 let req = table.push(req).context("failed to push request to table")?;
357 Ok((
358 req,
359 FutureReader::new(&mut store, GuestBodyResultProducer::Receiver(result_rx))?,
360 ))
361 }
362
363 fn consume_body(
364 mut store: Access<T, Self>,
365 req: Resource<Request>,
366 fut: FutureReader<Result<(), ErrorCode>>,
367 ) -> wasmtime::Result<(
368 StreamReader<u8>,
369 FutureReader<Result<Option<Resource<Trailers>>, ErrorCode>>,
370 )> {
371 let getter = store.getter();
372 let Request { body, .. } = store
373 .get()
374 .table
375 .delete(req)
376 .context("failed to delete request from table")?;
377 body.consume(store, fut, getter)
378 }
379
380 fn drop(mut store: Access<'_, T, Self>, req: Resource<Request>) -> wasmtime::Result<()> {
381 let Request { body, .. } = store
382 .get()
383 .table
384 .delete(req)
385 .context("failed to delete request from table")?;
386 body.drop(store)?;
387 Ok(())
388 }
389}
390
391impl HostRequest for WasiHttpCtxView<'_> {
392 fn get_method(&mut self, req: Resource<Request>) -> wasmtime::Result<Method> {
393 let Request { method, .. } = get_request(self.table, &req)?;
394 Ok(method.into())
395 }
396
397 fn set_method(
398 &mut self,
399 req: Resource<Request>,
400 method: Method,
401 ) -> wasmtime::Result<Result<(), ()>> {
402 let req = get_request_mut(self.table, &req)?;
403 let Ok(method) = method.try_into() else {
404 return Ok(Err(()));
405 };
406 req.method = method;
407 Ok(Ok(()))
408 }
409
410 fn get_path_with_query(&mut self, req: Resource<Request>) -> wasmtime::Result<Option<String>> {
411 let Request {
412 path_with_query, ..
413 } = get_request(self.table, &req)?;
414 Ok(path_with_query.as_ref().map(|pq| pq.as_str().into()))
415 }
416
417 fn set_path_with_query(
418 &mut self,
419 req: Resource<Request>,
420 path_with_query: Option<String>,
421 ) -> wasmtime::Result<Result<(), ()>> {
422 let req = get_request_mut(self.table, &req)?;
423 let Some(path_with_query) = path_with_query else {
424 req.path_with_query = None;
425 return Ok(Ok(()));
426 };
427 let Ok(path_with_query) = path_with_query.try_into() else {
428 return Ok(Err(()));
429 };
430 req.path_with_query = Some(path_with_query);
431 Ok(Ok(()))
432 }
433
434 fn get_scheme(&mut self, req: Resource<Request>) -> wasmtime::Result<Option<Scheme>> {
435 let Request { scheme, .. } = get_request(self.table, &req)?;
436 Ok(scheme.as_ref().map(Into::into))
437 }
438
439 fn set_scheme(
440 &mut self,
441 req: Resource<Request>,
442 scheme: Option<Scheme>,
443 ) -> wasmtime::Result<Result<(), ()>> {
444 let req = get_request_mut(self.table, &req)?;
445 let Some(scheme) = scheme else {
446 req.scheme = None;
447 return Ok(Ok(()));
448 };
449 let Ok(scheme) = scheme.try_into() else {
450 return Ok(Err(()));
451 };
452 req.scheme = Some(scheme);
453 Ok(Ok(()))
454 }
455
456 fn get_authority(&mut self, req: Resource<Request>) -> wasmtime::Result<Option<String>> {
457 let Request { authority, .. } = get_request(self.table, &req)?;
458 Ok(authority.as_ref().map(|auth| auth.as_str().into()))
459 }
460
461 fn set_authority(
462 &mut self,
463 req: Resource<Request>,
464 authority: Option<String>,
465 ) -> wasmtime::Result<Result<(), ()>> {
466 let req = get_request_mut(self.table, &req)?;
467 let Some(authority) = authority else {
468 req.authority = None;
469 return Ok(Ok(()));
470 };
471 let has_port = authority.contains(':');
472 let Ok(authority) = http::uri::Authority::try_from(authority) else {
473 return Ok(Err(()));
474 };
475 if has_port && authority.port_u16().is_none() {
476 return Ok(Err(()));
477 }
478 req.authority = Some(authority);
479 Ok(Ok(()))
480 }
481
482 fn get_options(
483 &mut self,
484 req: Resource<Request>,
485 ) -> wasmtime::Result<Option<Resource<RequestOptions>>> {
486 let Request { options, .. } = get_request(self.table, &req)?;
487 if let Some(options) = options {
488 let options = push_request_options(
489 self.table,
490 RequestOptions::new_immutable(Arc::clone(options)),
491 )?;
492 Ok(Some(options))
493 } else {
494 Ok(None)
495 }
496 }
497
498 fn get_headers(&mut self, req: Resource<Request>) -> wasmtime::Result<Resource<Headers>> {
499 let Request { headers, .. } = get_request(self.table, &req)?;
500 push_fields(self.table, headers.clone())
501 }
502}
503
504impl HostRequestOptions for WasiHttpCtxView<'_> {
505 fn new(&mut self) -> wasmtime::Result<Resource<RequestOptions>> {
506 push_request_options(self.table, RequestOptions::new_mutable_default())
507 }
508
509 fn get_connect_timeout(
510 &mut self,
511 opts: Resource<RequestOptions>,
512 ) -> wasmtime::Result<Option<Duration>> {
513 let opts = get_request_options(self.table, &opts)?;
514 let Some(connect_timeout) = opts.connect_timeout else {
515 return Ok(None);
516 };
517 let ns = connect_timeout.as_nanos();
518 let ns = Duration::try_from(ns)
519 .context("connect timeout duration nanoseconds do not fit in u64")?;
520 Ok(Some(ns))
521 }
522
523 fn set_connect_timeout(
524 &mut self,
525 opts: Resource<RequestOptions>,
526 duration: Option<Duration>,
527 ) -> RequestOptionsResult<()> {
528 let opts = get_request_options_mut(self.table, &opts)?;
529 let opts = opts.get_mut().ok_or(RequestOptionsError::Immutable)?;
530 opts.connect_timeout = duration.map(core::time::Duration::from_nanos);
531 Ok(())
532 }
533
534 fn get_first_byte_timeout(
535 &mut self,
536 opts: Resource<RequestOptions>,
537 ) -> wasmtime::Result<Option<Duration>> {
538 let opts = get_request_options(self.table, &opts)?;
539 let Some(first_byte_timeout) = opts.first_byte_timeout else {
540 return Ok(None);
541 };
542 let ns = first_byte_timeout.as_nanos();
543 let ns = Duration::try_from(ns)
544 .context("first byte timeout duration nanoseconds do not fit in u64")?;
545 Ok(Some(ns))
546 }
547
548 fn set_first_byte_timeout(
549 &mut self,
550 opts: Resource<RequestOptions>,
551 duration: Option<Duration>,
552 ) -> RequestOptionsResult<()> {
553 let opts = get_request_options_mut(self.table, &opts)?;
554 let opts = opts.get_mut().ok_or(RequestOptionsError::Immutable)?;
555 opts.first_byte_timeout = duration.map(core::time::Duration::from_nanos);
556 Ok(())
557 }
558
559 fn get_between_bytes_timeout(
560 &mut self,
561 opts: Resource<RequestOptions>,
562 ) -> wasmtime::Result<Option<Duration>> {
563 let opts = get_request_options(self.table, &opts)?;
564 let Some(between_bytes_timeout) = opts.between_bytes_timeout else {
565 return Ok(None);
566 };
567 let ns = between_bytes_timeout.as_nanos();
568 let ns = Duration::try_from(ns)
569 .context("between bytes timeout duration nanoseconds do not fit in u64")?;
570 Ok(Some(ns))
571 }
572
573 fn set_between_bytes_timeout(
574 &mut self,
575 opts: Resource<RequestOptions>,
576 duration: Option<Duration>,
577 ) -> RequestOptionsResult<()> {
578 let opts = get_request_options_mut(self.table, &opts)?;
579 let opts = opts.get_mut().ok_or(RequestOptionsError::Immutable)?;
580 opts.between_bytes_timeout = duration.map(core::time::Duration::from_nanos);
581 Ok(())
582 }
583
584 fn clone(
585 &mut self,
586 opts: Resource<RequestOptions>,
587 ) -> wasmtime::Result<Resource<RequestOptions>> {
588 let opts = get_request_options(self.table, &opts)?;
589 push_request_options(self.table, RequestOptions::new_mutable(Arc::clone(opts)))
590 }
591
592 fn drop(&mut self, opts: Resource<RequestOptions>) -> wasmtime::Result<()> {
593 delete_request_options(self.table, opts)?;
594 Ok(())
595 }
596}
597
598impl<T> HostResponseWithStore<T> for WasiHttp {
599 fn new(
600 mut store: Access<T, Self>,
601 headers: Resource<Headers>,
602 contents: Option<StreamReader<u8>>,
603 trailers: FutureReader<Result<Option<Resource<Trailers>>, ErrorCode>>,
604 ) -> wasmtime::Result<(Resource<Response>, FutureReader<Result<(), ErrorCode>>)> {
605 let (result_tx, result_rx) = oneshot::channel();
606 let body = match contents
607 .map(|rx| rx.try_into::<HostBodyStreamProducer<T>>(store.as_context_mut()))
608 {
609 Some(Ok(mut producer)) => Body::Host {
610 body: mem::take(&mut producer.body),
611 result_tx,
612 },
613 Some(Err(rx)) => Body::Guest {
614 contents_rx: Some(rx),
615 trailers_rx: trailers,
616 result_tx,
617 },
618 None => Body::Guest {
619 contents_rx: None,
620 trailers_rx: trailers,
621 result_tx,
622 },
623 };
624 let WasiHttpCtxView { table, .. } = store.get();
625 let headers = delete_fields(table, headers)?;
626 let res = Response {
627 status: http::StatusCode::OK,
628 headers,
629 body,
630 };
631 let res = table
632 .push(res)
633 .context("failed to push response to table")?;
634 Ok((
635 res,
636 FutureReader::new(&mut store, GuestBodyResultProducer::Receiver(result_rx))?,
637 ))
638 }
639
640 fn consume_body(
641 mut store: Access<T, Self>,
642 res: Resource<Response>,
643 fut: FutureReader<Result<(), ErrorCode>>,
644 ) -> wasmtime::Result<(
645 StreamReader<u8>,
646 FutureReader<Result<Option<Resource<Trailers>>, ErrorCode>>,
647 )> {
648 let getter = store.getter();
649 let Response { body, .. } = store
650 .get()
651 .table
652 .delete(res)
653 .context("failed to delete response from table")?;
654 body.consume(store, fut, getter)
655 }
656
657 fn drop(mut store: Access<'_, T, Self>, res: Resource<Response>) -> wasmtime::Result<()> {
658 let Response { body, .. } = store
659 .get()
660 .table
661 .delete(res)
662 .context("failed to delete response from table")?;
663 body.drop(store)?;
664 Ok(())
665 }
666}
667
668impl HostResponse for WasiHttpCtxView<'_> {
669 fn get_status_code(&mut self, res: Resource<Response>) -> wasmtime::Result<StatusCode> {
670 let res = get_response(self.table, &res)?;
671 Ok(res.status.into())
672 }
673
674 fn set_status_code(
675 &mut self,
676 res: Resource<Response>,
677 status_code: StatusCode,
678 ) -> wasmtime::Result<Result<(), ()>> {
679 let res = get_response_mut(self.table, &res)?;
680 match http::StatusCode::from_u16(status_code) {
681 Ok(status) if matches!(status_code, 100..=599) => {
682 res.status = status;
683 Ok(Ok(()))
684 }
685 _ => Ok(Err(())),
686 }
687 }
688
689 fn get_headers(&mut self, res: Resource<Response>) -> wasmtime::Result<Resource<Headers>> {
690 let Response { headers, .. } = get_response(self.table, &res)?;
691 push_fields(self.table, headers.clone())
692 }
693}
694
695impl Host for WasiHttpCtxView<'_> {
696 fn convert_error_code(&mut self, error: HttpError) -> wasmtime::Result<ErrorCode> {
697 error.downcast()
698 }
699
700 fn convert_header_error(
701 &mut self,
702 error: crate::p3::HeaderError,
703 ) -> wasmtime::Result<HeaderError> {
704 error.downcast()
705 }
706
707 fn convert_request_options_error(
708 &mut self,
709 error: crate::p3::RequestOptionsError,
710 ) -> wasmtime::Result<RequestOptionsError> {
711 error.downcast()
712 }
713}
714
715#[cfg(test)]
716mod tests {
717 use super::parse_header_value;
718 use http::header::{CONTENT_LENGTH, CONTENT_TYPE};
719
720 #[test]
721 fn content_length_rejects_non_digits() {
722 assert!(parse_header_value(&CONTENT_LENGTH, "0").is_ok());
723 assert!(parse_header_value(&CONTENT_LENGTH, "1234").is_ok());
724
725 assert!(parse_header_value(&CONTENT_LENGTH, "+5").is_err());
727 assert!(parse_header_value(&CONTENT_LENGTH, "-5").is_err());
728 assert!(parse_header_value(&CONTENT_LENGTH, " 5").is_err());
729 assert!(parse_header_value(&CONTENT_LENGTH, "").is_err());
730
731 assert!(parse_header_value(&CONTENT_TYPE, "text/plain").is_ok());
733 }
734}