tonic-web-wasm-client 0.3.3

grpc-web implementation for use by tonic clients in browsers via webassembly
Documentation
use http::{
    header::{ACCEPT, CONTENT_TYPE},
    response::Builder,
    HeaderMap, HeaderValue, Request, Response,
};
use http_body::Body;
use js_sys::{Array, Uint8Array};
use tonic::body::BoxBody;
use wasm_bindgen::JsValue;
use web_sys::{Headers, RequestCredentials, RequestInit};

use crate::{fetch::fetch, Error, ResponseBody};

pub async fn call(
    mut base_url: String,
    request: Request<BoxBody>,
) -> Result<Response<ResponseBody>, Error> {
    base_url.push_str(&request.uri().to_string());

    let headers = prepare_headers(request.headers())?;
    let body = prepare_body(request).await?;

    let request = prepare_request(&base_url, headers, body)?;
    let response = fetch(&request).await?;

    let result = Response::builder().status(response.status());
    let (result, content_type) = set_response_headers(result, &response)?;

    let content_type = content_type.ok_or(Error::MissingContentTypeHeader)?;
    let body_stream = response.body().ok_or(Error::MissingResponseBody)?;

    let body = ResponseBody::new(body_stream, &content_type)?;

    result.body(body).map_err(Into::into)
}

fn prepare_headers(header_map: &HeaderMap<HeaderValue>) -> Result<Headers, Error> {
    let headers = Headers::new().map_err(Error::js_error)?;

    headers
        .append(CONTENT_TYPE.as_str(), "application/grpc-web+proto")
        .map_err(Error::js_error)?;
    headers
        .append(ACCEPT.as_str(), "application/grpc-web+proto")
        .map_err(Error::js_error)?;
    headers.append("x-grpc-web", "1").map_err(Error::js_error)?;

    for (header_name, header_value) in header_map.iter() {
        if header_name != CONTENT_TYPE && header_name != ACCEPT {
            headers
                .append(header_name.as_str(), header_value.to_str()?)
                .map_err(Error::js_error)?;
        }
    }

    Ok(headers)
}

async fn prepare_body(request: Request<BoxBody>) -> Result<Option<JsValue>, Error> {
    let body = request.into_body().data().await.transpose()?;
    Ok(body.map(|bytes| Uint8Array::from(bytes.as_ref()).into()))
}

fn prepare_request(
    url: &str,
    headers: Headers,
    body: Option<JsValue>,
) -> Result<web_sys::Request, Error> {
    let mut init = RequestInit::new();

    init.method("POST")
        .headers(headers.as_ref())
        .body(body.as_ref())
        .credentials(RequestCredentials::SameOrigin);

    web_sys::Request::new_with_str_and_init(url, &init).map_err(Error::js_error)
}

fn set_response_headers(
    mut result: Builder,
    response: &web_sys::Response,
) -> Result<(Builder, Option<String>), Error> {
    let headers = response.headers();

    let header_iter = js_sys::try_iter(headers.as_ref()).map_err(Error::js_error)?;

    let mut content_type = None;

    if let Some(header_iter) = header_iter {
        for header in header_iter {
            let header = header.map_err(Error::js_error)?;
            let pair: Array = header.into();

            let header_name = pair.get(0).as_string();
            let header_value = pair.get(1).as_string();

            match (header_name, header_value) {
                (Some(header_name), Some(header_value)) => {
                    if header_name == CONTENT_TYPE.as_str() {
                        content_type = Some(header_value.clone());
                    }

                    result = result.header(header_name, header_value);
                }
                _ => continue,
            }
        }
    }

    Ok((result, content_type))
}