tonic_web_wasm_client/
call.rs

1use http::{
2    header::{ACCEPT, CONTENT_TYPE},
3    response::Builder,
4    HeaderMap, HeaderValue, Request, Response,
5};
6use http_body_util::BodyExt;
7use js_sys::{Array, Uint8Array};
8use tonic::body::Body;
9use wasm_bindgen::JsValue;
10use web_sys::{Headers, RequestCredentials, RequestInit};
11
12use crate::{fetch::fetch, options::FetchOptions, Error, ResponseBody};
13
14pub async fn call(
15    mut base_url: String,
16    request: Request<Body>,
17    options: FetchOptions,
18) -> Result<Response<ResponseBody>, Error> {
19    base_url.push_str(&request.uri().to_string());
20
21    let headers = prepare_headers(request.headers())?;
22    let body = prepare_body(request).await?;
23
24    let request = prepare_request(&base_url, headers, body)?;
25    let (init, abort) = options.request_init()?;
26    let response = fetch(&request, &init).await?;
27
28    let result = Response::builder().status(response.status());
29    let (result, content_type) = set_response_headers(result, &response)?;
30
31    let content_type = content_type.ok_or(Error::MissingContentTypeHeader)?;
32    let body_stream = response.body().ok_or(Error::MissingResponseBody)?;
33
34    let body = ResponseBody::new(body_stream, &content_type, abort)?;
35
36    result.body(body).map_err(Into::into)
37}
38
39fn prepare_headers(header_map: &HeaderMap<HeaderValue>) -> Result<Headers, Error> {
40    // Construct default headers.
41    let headers = Headers::new().map_err(Error::js_error)?;
42    headers
43        .append(CONTENT_TYPE.as_str(), "application/grpc-web+proto")
44        .map_err(Error::js_error)?;
45    headers
46        .append(ACCEPT.as_str(), "application/grpc-web+proto")
47        .map_err(Error::js_error)?;
48    headers.append("x-grpc-web", "1").map_err(Error::js_error)?;
49
50    // Apply default headers.
51    for (header_name, header_value) in header_map.iter() {
52        // Allow default headers to be overridden except for `content-type`.
53        if header_name != CONTENT_TYPE {
54            headers
55                .set(header_name.as_str(), header_value.to_str()?)
56                .map_err(Error::js_error)?;
57        }
58    }
59
60    Ok(headers)
61}
62
63async fn prepare_body(request: Request<Body>) -> Result<Option<JsValue>, Error> {
64    let body = Some(request.collect().await?.to_bytes());
65    Ok(body.map(|bytes| Uint8Array::from(bytes.as_ref()).into()))
66}
67
68fn prepare_request(
69    url: &str,
70    headers: Headers,
71    body: Option<JsValue>,
72) -> Result<web_sys::Request, Error> {
73    let init = RequestInit::new();
74
75    init.set_method("POST");
76    init.set_headers(headers.as_ref());
77    if let Some(ref body) = body {
78        init.set_body(body);
79    }
80    init.set_credentials(RequestCredentials::SameOrigin);
81
82    web_sys::Request::new_with_str_and_init(url, &init).map_err(Error::js_error)
83}
84
85fn set_response_headers(
86    mut result: Builder,
87    response: &web_sys::Response,
88) -> Result<(Builder, Option<String>), Error> {
89    let headers = response.headers();
90
91    let header_iter = js_sys::try_iter(headers.as_ref()).map_err(Error::js_error)?;
92
93    let mut content_type = None;
94
95    if let Some(header_iter) = header_iter {
96        for header in header_iter {
97            let header = header.map_err(Error::js_error)?;
98            let pair: Array = header.into();
99
100            let header_name = pair.get(0).as_string();
101            let header_value = pair.get(1).as_string();
102
103            match (header_name, header_value) {
104                (Some(header_name), Some(header_value)) => {
105                    if header_name == CONTENT_TYPE.as_str() {
106                        content_type = Some(header_value.clone());
107                    }
108
109                    result = result.header(header_name, header_value);
110                }
111                _ => continue,
112            }
113        }
114    }
115
116    Ok((result, content_type))
117}