spin_sdk/
http.rs

1/// Traits for converting between the various types
2pub mod conversions;
3
4use std::collections::HashMap;
5
6#[doc(inline)]
7pub use conversions::IntoResponse;
8#[doc(inline)]
9pub use types::{
10    ErrorCode, Fields, Headers, IncomingRequest, IncomingResponse, Method, OutgoingBody,
11    OutgoingRequest, OutgoingResponse, Scheme, StatusCode, Trailers,
12};
13
14use self::conversions::{TryFromIncomingResponse, TryIntoOutgoingRequest};
15use super::wit::wasi::http0_2_0::types;
16use futures::SinkExt;
17use spin_executor::bindings::wasi::io::streams::{self, StreamError};
18
19/// A unified request object that can represent both incoming and outgoing requests.
20///
21/// This should be used in favor of `IncomingRequest` and `OutgoingRequest` when there
22/// is no need for streaming bodies.
23pub struct Request {
24    /// The method of the request
25    method: Method,
26    /// The uri for the request
27    ///
28    /// The first item is set to `None` if the supplied uri is malformed
29    uri: (Option<hyperium::Uri>, String),
30    /// The request headers
31    headers: HashMap<String, HeaderValue>,
32    /// The request body as bytes
33    body: Vec<u8>,
34}
35
36impl Request {
37    /// Creates a new request from a method and uri
38    pub fn new(method: Method, uri: impl Into<String>) -> Self {
39        Self {
40            method,
41            uri: Self::parse_uri(uri.into()),
42            headers: HashMap::new(),
43            body: Vec::new(),
44        }
45    }
46
47    /// Creates a [`RequestBuilder`]
48    pub fn builder() -> RequestBuilder {
49        RequestBuilder::new(Method::Get, "/")
50    }
51
52    /// Creates a [`RequestBuilder`] to GET the given `uri`
53    pub fn get(uri: impl Into<String>) -> RequestBuilder {
54        RequestBuilder::new(Method::Get, uri)
55    }
56
57    /// Creates a [`RequestBuilder`] to POST the given `body` to `uri`
58    pub fn post(uri: impl Into<String>, body: impl conversions::IntoBody) -> RequestBuilder {
59        let mut builder = RequestBuilder::new(Method::Post, uri);
60        builder.body(body);
61        builder
62    }
63
64    /// Creates a [`RequestBuilder`] to PUT the given `body` to `uri`
65    pub fn put(uri: impl Into<String>, body: impl conversions::IntoBody) -> RequestBuilder {
66        let mut builder = RequestBuilder::new(Method::Put, uri);
67        builder.body(body);
68        builder
69    }
70
71    /// Creates a [`RequestBuilder`] to PATCH the resource specified by `uri`
72    pub fn patch(uri: impl Into<String>, body: impl conversions::IntoBody) -> RequestBuilder {
73        let mut builder = RequestBuilder::new(Method::Patch, uri);
74        builder.body(body);
75        builder
76    }
77
78    /// Creates a [`RequestBuilder`] to DELETE the resource specified by `uri`
79    pub fn delete(uri: impl Into<String>) -> RequestBuilder {
80        RequestBuilder::new(Method::Delete, uri)
81    }
82
83    /// The request method
84    pub fn method(&self) -> &Method {
85        &self.method
86    }
87
88    /// The request uri
89    pub fn uri(&self) -> &str {
90        &self.uri.1
91    }
92
93    /// The request uri path
94    pub fn path(&self) -> &str {
95        self.uri.0.as_ref().map(|u| u.path()).unwrap_or_default()
96    }
97
98    /// The request uri query
99    pub fn query(&self) -> &str {
100        self.uri
101            .0
102            .as_ref()
103            .and_then(|u| u.query())
104            .unwrap_or_default()
105    }
106
107    /// The request headers
108    pub fn headers(&self) -> impl Iterator<Item = (&str, &HeaderValue)> {
109        self.headers.iter().map(|(k, v)| (k.as_str(), v))
110    }
111
112    /// Return a header value
113    ///
114    /// Will return `None` if the header does not exist.
115    pub fn header(&self, name: &str) -> Option<&HeaderValue> {
116        self.headers.get(&name.to_lowercase())
117    }
118
119    /// Set a header
120    pub fn set_header(&mut self, name: impl Into<String>, value: impl Into<String>) {
121        self.headers.insert(
122            name.into(),
123            HeaderValue {
124                inner: HeaderValueRep::String(value.into()),
125            },
126        );
127    }
128
129    /// The request body
130    pub fn body(&self) -> &[u8] {
131        &self.body
132    }
133
134    /// The request body
135    pub fn body_mut(&mut self) -> &mut Vec<u8> {
136        &mut self.body
137    }
138
139    /// Consume this type and return its body
140    pub fn into_body(self) -> Vec<u8> {
141        self.body
142    }
143
144    fn parse_uri(uri: String) -> (Option<hyperium::Uri>, String) {
145        (
146            hyperium::Uri::try_from(&uri)
147                .or_else(|_| hyperium::Uri::try_from(&format!("http://{uri}")))
148                .ok(),
149            uri,
150        )
151    }
152
153    /// Whether the request is an HTTPS request
154    fn is_https(&self) -> bool {
155        self.uri
156            .0
157            .as_ref()
158            .and_then(|u| u.scheme())
159            .map(|s| s == &hyperium::uri::Scheme::HTTPS)
160            .unwrap_or(true)
161    }
162
163    /// The URI's authority
164    fn authority(&self) -> Option<&str> {
165        self.uri
166            .0
167            .as_ref()
168            .and_then(|u| u.authority())
169            .map(|a| a.as_str())
170    }
171
172    /// The request path and query combined
173    pub fn path_and_query(&self) -> Option<&str> {
174        self.uri
175            .0
176            .as_ref()
177            .and_then(|u| u.path_and_query())
178            .map(|s| s.as_str())
179    }
180}
181
182/// A request builder
183pub struct RequestBuilder {
184    request: Request,
185}
186
187impl RequestBuilder {
188    /// Create a new `RequestBuilder`
189    pub fn new(method: Method, uri: impl Into<String>) -> Self {
190        Self {
191            request: Request::new(method, uri.into()),
192        }
193    }
194
195    /// Set the method
196    pub fn method(&mut self, method: Method) -> &mut Self {
197        self.request.method = method;
198        self
199    }
200
201    /// Set the uri
202    pub fn uri(&mut self, uri: impl Into<String>) -> &mut Self {
203        self.request.uri = Request::parse_uri(uri.into());
204        self
205    }
206
207    /// Set the headers
208    pub fn headers(&mut self, headers: impl conversions::IntoHeaders) -> &mut Self {
209        self.request.headers = into_header_rep(headers);
210        self
211    }
212
213    /// Set a header
214    pub fn header(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
215        self.request
216            .headers
217            .insert(key.into().to_lowercase(), HeaderValue::string(value.into()));
218        self
219    }
220
221    /// Set the body
222    pub fn body(&mut self, body: impl conversions::IntoBody) -> &mut Self {
223        self.request.body = body.into_body();
224        self
225    }
226
227    /// Build the `Request`
228    pub fn build(&mut self) -> Request {
229        std::mem::replace(&mut self.request, Request::new(Method::Get, "/"))
230    }
231}
232
233/// A unified response object that can represent both outgoing and incoming responses.
234///
235/// This should be used in favor of `OutgoingResponse` and `IncomingResponse` when there
236/// is no need for streaming bodies.
237pub struct Response {
238    /// The status of the response
239    status: StatusCode,
240    /// The response headers
241    headers: HashMap<String, HeaderValue>,
242    /// The body of the response as bytes
243    body: Vec<u8>,
244}
245
246impl Response {
247    /// Create a new response from a status and body
248    pub fn new(status: impl conversions::IntoStatusCode, body: impl conversions::IntoBody) -> Self {
249        Self {
250            status: status.into_status_code(),
251            headers: HashMap::new(),
252            body: body.into_body(),
253        }
254    }
255
256    /// The response status
257    pub fn status(&self) -> &StatusCode {
258        &self.status
259    }
260
261    /// The request headers
262    pub fn headers(&self) -> impl Iterator<Item = (&str, &HeaderValue)> {
263        self.headers.iter().map(|(k, v)| (k.as_str(), v))
264    }
265
266    /// Return a header value
267    ///
268    /// Will return `None` if the header does not exist.
269    pub fn header(&self, name: &str) -> Option<&HeaderValue> {
270        self.headers.get(&name.to_lowercase())
271    }
272
273    /// Set a response header
274    pub fn set_header(&mut self, name: impl Into<String>, value: impl Into<String>) {
275        self.headers.insert(
276            name.into(),
277            HeaderValue {
278                inner: HeaderValueRep::String(value.into()),
279            },
280        );
281    }
282
283    /// The response body
284    pub fn body(&self) -> &[u8] {
285        &self.body
286    }
287
288    /// The response body
289    pub fn body_mut(&mut self) -> &mut Vec<u8> {
290        &mut self.body
291    }
292
293    /// Consume this type and return its body
294    pub fn into_body(self) -> Vec<u8> {
295        self.body
296    }
297
298    /// Converts this response into a [`ResponseBuilder`]. This can be used to
299    /// update a response before passing it on.
300    pub fn into_builder(self) -> ResponseBuilder {
301        ResponseBuilder { response: self }
302    }
303
304    /// Creates a [`ResponseBuilder`]
305    pub fn builder() -> ResponseBuilder {
306        ResponseBuilder::new(200)
307    }
308}
309
310impl std::fmt::Debug for Response {
311    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312        f.debug_struct("Response")
313            .field("status", &self.status)
314            .field("headers", &self.headers)
315            .field("body.len()", &self.body.len())
316            .finish()
317    }
318}
319
320/// A builder for `Response``
321pub struct ResponseBuilder {
322    response: Response,
323}
324
325impl ResponseBuilder {
326    /// Create a new `ResponseBuilder`
327    pub fn new(status: impl conversions::IntoStatusCode) -> Self {
328        ResponseBuilder {
329            response: Response::new(status, Vec::new()),
330        }
331    }
332
333    /// Set the status
334    pub fn status(&mut self, status: impl conversions::IntoStatusCode) -> &mut Self {
335        self.response.status = status.into_status_code();
336        self
337    }
338
339    /// Set the headers
340    pub fn headers(&mut self, headers: impl conversions::IntoHeaders) -> &mut Self {
341        self.response.headers = into_header_rep(headers.into_headers());
342        self
343    }
344
345    /// Set a header
346    pub fn header(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
347        self.response
348            .headers
349            .insert(key.into().to_lowercase(), HeaderValue::string(value.into()));
350        self
351    }
352
353    /// Set the body
354    pub fn body(&mut self, body: impl conversions::IntoBody) -> &mut Self {
355        self.response.body = body.into_body();
356        self
357    }
358
359    /// Build the `Response`
360    pub fn build(&mut self) -> Response {
361        std::mem::replace(&mut self.response, Response::new(200, Vec::new()))
362    }
363}
364
365/// A header value.
366///
367/// Since header values do not have to be valid utf8, this allows for
368/// both utf8 strings and bags of bytes.
369#[derive(Debug, PartialEq, Eq, Clone)]
370pub struct HeaderValue {
371    inner: HeaderValueRep,
372}
373
374#[derive(Debug, PartialEq, Eq, Clone)]
375enum HeaderValueRep {
376    /// Header value encoded as a utf8 string
377    String(String),
378    /// Header value as a bag of bytes
379    Bytes(Vec<u8>),
380}
381
382impl HeaderValue {
383    /// Construct a `HeaderValue` from a string
384    pub fn string(str: String) -> HeaderValue {
385        HeaderValue {
386            inner: HeaderValueRep::String(str),
387        }
388    }
389
390    /// Construct a `HeaderValue` from a bag of bytes
391    pub fn bytes(bytes: Vec<u8>) -> HeaderValue {
392        HeaderValue {
393            inner: String::from_utf8(bytes)
394                .map(HeaderValueRep::String)
395                .unwrap_or_else(|e| HeaderValueRep::Bytes(e.into_bytes())),
396        }
397    }
398
399    /// Get the `HeaderValue` as a utf8 encoded string
400    ///
401    /// Returns `None` if the value is a non utf8 encoded header value
402    pub fn as_str(&self) -> Option<&str> {
403        match &self.inner {
404            HeaderValueRep::String(s) => Some(s),
405            HeaderValueRep::Bytes(b) => std::str::from_utf8(b).ok(),
406        }
407    }
408
409    /// Get the `HeaderValue` as bytes
410    pub fn as_bytes(&self) -> &[u8] {
411        self.as_ref()
412    }
413
414    /// Turn the `HeaderValue` into a String (in a lossy way if the `HeaderValue` is a bag of bytes)
415    pub fn into_utf8_lossy(self) -> String {
416        match self.inner {
417            HeaderValueRep::String(s) => s,
418            HeaderValueRep::Bytes(b) => String::from_utf8_lossy(&b).into_owned(),
419        }
420    }
421
422    /// Turn the `HeaderValue` into bytes
423    pub fn into_bytes(self) -> Vec<u8> {
424        match self.inner {
425            HeaderValueRep::String(s) => s.into_bytes(),
426            HeaderValueRep::Bytes(b) => b,
427        }
428    }
429}
430
431impl AsRef<[u8]> for HeaderValue {
432    fn as_ref(&self) -> &[u8] {
433        match &self.inner {
434            HeaderValueRep::String(s) => s.as_bytes(),
435            HeaderValueRep::Bytes(b) => b,
436        }
437    }
438}
439
440fn into_header_rep(headers: impl conversions::IntoHeaders) -> HashMap<String, HeaderValue> {
441    headers
442        .into_headers()
443        .into_iter()
444        .map(|(k, v)| {
445            let v = String::from_utf8(v)
446                .map(HeaderValueRep::String)
447                .unwrap_or_else(|e| HeaderValueRep::Bytes(e.into_bytes()));
448            (k.to_lowercase(), HeaderValue { inner: v })
449        })
450        .collect()
451}
452
453impl std::hash::Hash for Method {
454    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
455        core::mem::discriminant(self).hash(state);
456    }
457}
458
459impl Eq for Method {}
460
461impl PartialEq for Method {
462    fn eq(&self, other: &Self) -> bool {
463        match (self, other) {
464            (Self::Other(l), Self::Other(r)) => l == r,
465            _ => core::mem::discriminant(self) == core::mem::discriminant(other),
466        }
467    }
468}
469
470impl std::fmt::Display for Method {
471    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
472        f.write_str(match self {
473            Method::Get => "GET",
474            Method::Post => "POST",
475            Method::Put => "PUT",
476            Method::Delete => "DELETE",
477            Method::Patch => "PATCH",
478            Method::Head => "HEAD",
479            Method::Options => "OPTIONS",
480            Method::Connect => "CONNECT",
481            Method::Trace => "TRACE",
482            Method::Other(o) => o,
483        })
484    }
485}
486
487impl IncomingRequest {
488    /// The incoming request Uri
489    pub fn uri(&self) -> String {
490        let scheme_and_authority =
491            if let (Some(scheme), Some(authority)) = (self.scheme(), self.authority()) {
492                let scheme = match &scheme {
493                    Scheme::Http => "http://",
494                    Scheme::Https => "https://",
495                    Scheme::Other(s) => s.as_str(),
496                };
497                format!("{scheme}{authority}")
498            } else {
499                String::new()
500            };
501        let path_and_query = self.path_with_query().unwrap_or_default();
502        format!("{scheme_and_authority}{path_and_query}")
503    }
504
505    /// Return a `Stream` from which the body of the specified request may be read.
506    ///
507    /// # Panics
508    ///
509    /// Panics if the body was already consumed.
510    pub fn into_body_stream(self) -> impl futures::Stream<Item = Result<Vec<u8>, streams::Error>> {
511        executor::incoming_body(self.consume().expect("request body was already consumed"))
512    }
513
514    /// Return a `Vec<u8>` of the body or fails
515    pub async fn into_body(self) -> Result<Vec<u8>, streams::Error> {
516        use futures::TryStreamExt;
517        let mut stream = self.into_body_stream();
518        let mut body = Vec::new();
519        while let Some(chunk) = stream.try_next().await? {
520            body.extend(chunk);
521        }
522        Ok(body)
523    }
524}
525
526impl IncomingResponse {
527    /// Return a `Stream` from which the body of the specified response may be read.
528    ///
529    /// # Panics
530    ///
531    /// Panics if the body was already consumed.
532    // TODO: This should ideally take ownership of `self` and be called `into_body_stream` (i.e. symmetric with
533    // `IncomingRequest::into_body_stream`).  However, as of this writing, `wasmtime-wasi-http` is implemented in
534    // such a way that dropping an `IncomingResponse` will cause the request to be cancelled, meaning the caller
535    // won't necessarily have a chance to send the request body if they haven't started doing so yet (or, if they
536    // have started, they might not be able to finish before the connection is closed).  See
537    // https://github.com/bytecodealliance/wasmtime/issues/7413 for details.
538    pub fn take_body_stream(&self) -> impl futures::Stream<Item = Result<Vec<u8>, streams::Error>> {
539        executor::incoming_body(self.consume().expect("response body was already consumed"))
540    }
541
542    /// Return a `Vec<u8>` of the body or fails
543    ///
544    /// # Panics
545    ///
546    /// Panics if the body was already consumed.
547    pub async fn into_body(self) -> Result<Vec<u8>, streams::Error> {
548        use futures::TryStreamExt;
549        let mut stream = self.take_body_stream();
550        let mut body = Vec::new();
551        while let Some(chunk) = stream.try_next().await? {
552            body.extend(chunk);
553        }
554        Ok(body)
555    }
556}
557
558impl OutgoingResponse {
559    /// Construct a `Sink` which writes chunks to the body of the specified response.
560    ///
561    /// # Panics
562    ///
563    /// Panics if the body was already taken.
564    pub fn take_body(&self) -> impl futures::Sink<Vec<u8>, Error = StreamError> {
565        executor::outgoing_body(self.body().expect("response body was already taken"))
566    }
567}
568
569impl OutgoingRequest {
570    /// Construct a `Sink` which writes chunks to the body of the specified response.
571    ///
572    /// # Panics
573    ///
574    /// Panics if the body was already taken.
575    pub fn take_body(&self) -> impl futures::Sink<Vec<u8>, Error = StreamError> {
576        executor::outgoing_body(self.body().expect("request body was already taken"))
577    }
578}
579
580/// The out param for setting an `OutgoingResponse`
581pub struct ResponseOutparam(types::ResponseOutparam);
582
583impl ResponseOutparam {
584    #[doc(hidden)]
585    // This is needed for the macro so we can transfrom the macro's
586    // `ResponseOutparam` to this `ResponseOutparam`
587    pub unsafe fn from_handle(handle: u32) -> Self {
588        Self(types::ResponseOutparam::from_handle(handle))
589    }
590
591    /// Set the outgoing response
592    pub fn set(self, response: OutgoingResponse) {
593        types::ResponseOutparam::set(self.0, Ok(response));
594    }
595
596    /// Set with the outgoing response and the supplied buffer
597    ///
598    /// Will panic if response body has already been taken
599    pub async fn set_with_body(
600        self,
601        response: OutgoingResponse,
602        buffer: Vec<u8>,
603    ) -> Result<(), StreamError> {
604        let mut body = response.take_body();
605        self.set(response);
606        body.send(buffer).await
607    }
608
609    /// Return the inner, `wit-bindgen`-generated instance
610    pub fn into_inner(self) -> types::ResponseOutparam {
611        self.0
612    }
613}
614
615/// Send an outgoing request
616pub async fn send<I, O>(request: I) -> Result<O, SendError>
617where
618    I: TryIntoOutgoingRequest,
619    I::Error: Into<Box<dyn std::error::Error + Send + Sync>> + 'static,
620    O: TryFromIncomingResponse,
621    O::Error: Into<Box<dyn std::error::Error + Send + Sync>> + 'static,
622{
623    let (request, body_buffer) = I::try_into_outgoing_request(request)
624        .map_err(|e| SendError::RequestConversion(e.into()))?;
625    let response = if let Some(body_buffer) = body_buffer {
626        // It is part of the contract of the trait that implementors of `TryIntoOutgoingRequest`
627        // do not call `OutgoingRequest::write`` if they return a buffered body.
628        let mut body_sink = request.take_body();
629        let response = executor::outgoing_request_send(request);
630        body_sink.send(body_buffer).await.map_err(SendError::Io)?;
631        drop(body_sink);
632        response.await.map_err(SendError::Http)?
633    } else {
634        executor::outgoing_request_send(request)
635            .await
636            .map_err(SendError::Http)?
637    };
638
639    TryFromIncomingResponse::try_from_incoming_response(response)
640        .await
641        .map_err(|e: O::Error| SendError::ResponseConversion(e.into()))
642}
643
644/// An error encountered when performing an HTTP request
645#[derive(thiserror::Error, Debug)]
646pub enum SendError {
647    /// Error converting to a request
648    #[error(transparent)]
649    RequestConversion(Box<dyn std::error::Error + Send + Sync>),
650    /// Error converting from a response
651    #[error(transparent)]
652    ResponseConversion(Box<dyn std::error::Error + Send + Sync>),
653    /// An I/O error
654    #[error(transparent)]
655    Io(StreamError),
656    /// An HTTP error
657    #[error(transparent)]
658    Http(ErrorCode),
659}
660
661#[doc(hidden)]
662/// The executor for driving wasi-http futures to completion
663mod executor;
664#[doc(hidden)]
665pub use executor::run;
666
667/// An error parsing a JSON body
668#[cfg(feature = "json")]
669#[derive(Debug)]
670pub struct JsonBodyError(serde_json::Error);
671
672#[cfg(feature = "json")]
673impl std::error::Error for JsonBodyError {}
674
675#[cfg(feature = "json")]
676impl std::fmt::Display for JsonBodyError {
677    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
678        f.write_str("could not convert body to json")
679    }
680}
681
682/// An error when the body is not UTF-8
683#[derive(Debug)]
684pub struct NonUtf8BodyError;
685
686impl std::error::Error for NonUtf8BodyError {}
687
688impl std::fmt::Display for NonUtf8BodyError {
689    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
690        f.write_str("body was expected to be utf8 but was not")
691    }
692}
693
694mod router;
695/// Exports HTTP Router items.
696pub use router::*;
697
698/// A Body extractor
699#[derive(Debug)]
700pub struct Body<T>(pub T);
701
702impl<T> std::ops::Deref for Body<T> {
703    type Target = T;
704
705    fn deref(&self) -> &Self::Target {
706        &self.0
707    }
708}
709
710/// A Json extractor
711#[derive(Debug)]
712pub struct Json<T>(pub T);
713
714impl<T> std::ops::Deref for Json<T> {
715    type Target = T;
716
717    fn deref(&self) -> &Self::Target {
718        &self.0
719    }
720}
721
722/// Helper functions for creating responses
723pub mod responses {
724    use super::Response;
725
726    /// Helper function to return a 404 Not Found response.
727    pub fn not_found() -> Response {
728        Response::new(404, "Not Found")
729    }
730
731    /// Helper function to return a 500 Internal Server Error response.
732    pub fn internal_server_error() -> Response {
733        Response::new(500, "Internal Server Error")
734    }
735
736    /// Helper function to return a 405 Method Not Allowed response.
737    pub fn method_not_allowed() -> Response {
738        Response::new(405, "Method Not Allowed")
739    }
740
741    pub(crate) fn bad_request(msg: Option<String>) -> Response {
742        Response::new(400, msg.map(|m| m.into_bytes()))
743    }
744}
745
746#[cfg(test)]
747mod tests {
748    use super::*;
749
750    #[test]
751    fn request_uri_parses() {
752        let uri = "/hello?world=1";
753        let req = Request::new(Method::Get, uri);
754        assert_eq!(req.uri(), uri);
755        assert_eq!(req.path(), "/hello");
756        assert_eq!(req.query(), "world=1");
757
758        let uri = "http://localhost:3000/hello?world=1";
759        let req = Request::new(Method::Get, uri);
760        assert_eq!(req.uri(), uri);
761        assert_eq!(req.path(), "/hello");
762        assert_eq!(req.query(), "world=1");
763
764        let uri = "localhost:3000/hello?world=1";
765        let req = Request::new(Method::Get, uri);
766        assert_eq!(req.uri(), uri);
767        assert_eq!(req.path(), "/hello");
768        assert_eq!(req.query(), "world=1");
769    }
770}