Skip to main content

wasmtime_wasi_http/p3/host/
types.rs

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    // When fields are passed by ownership to the host that flags them as
50    // immutable within `wasi:http`, and this semantically means that putting
51    // fields in a request, then getting them back out, will return an immutable
52    // view of the headers rather than mutable for example.
53    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        // RFC 9110 defines `Content-Length` as `1*DIGIT`. `u64`'s `FromStr` is
133        // more lenient and also accepts a leading `+`, so reject anything that
134        // isn't a non-empty run of decimal digits.
135        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                        // poll the received future once and update state
177                        let res = poll_future(cx, fut.as_mut(), finish);
178                        *self = Self::Future(fut);
179                        res.map(Ok)
180                    }
181                    Some(Err(..)) => {
182                        // oneshot sender dropped, treat as success
183                        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        // `u64::from_str` accepts these but they are not `1*DIGIT` per RFC 9110.
726        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        // other header names are unaffected
732        assert!(parse_header_value(&CONTENT_TYPE, "text/plain").is_ok());
733    }
734}