devashishdxt / tonic-web-wasm-client

Other
104 stars 28 forks source link

tonic::Status `.code()` "Unknown", `message()` "malformed response" for grpc-status: 16 or 5 #31

Closed isosphere closed 1 year ago

isosphere commented 1 year ago

Version Housekeeping

rustc 1.67.1 tonic-web-wasm-client ca7f5c9d1152000d862810accb2df9c60ba2d963

Backend: tonic 0.8 tonic-web 0.5

Report

I think that there might be an issue with error response handling.

I'm implementing an authentication system. If the user inputs a user that doesn't exist, my backend responds with a tonic::Status of Unauthenticated. This has a grpc-status code of 16.

In testing the client-side portion of this, I'm doing the following:

use tonic_web_wasm_client::Client;
use gloo_console::error;

let mut query_client: AuthenticationServiceClient<Client> = AuthenticationServiceClient::new(Client::new(BASE_URL.to_owned()));

let response = match query_client.get_authentication_token(request).await {
    Ok(response) => {
            ... login stuff ...
    },
    Err(e) => {
        error!(format!("Error: {:?}, {:?}", e.code(), e.message()));
        error!(format!("Error: {:?}", e));
    }
}

If I enter valid credentials authentication proceeds within the Ok(...) block as expected.

If I enter invalid credentials, my code does not have appropriate information to provide a good error message to the user. The error e always has the following debug print value:

Status { code: Unknown, message: "malformed response", metadata: MetadataMap { headers: {"content-length": "0", "content-type": "application/grpc-web+proto"} }, source: Some(MalformedResponse) }

However if I inspect the response in the Firefox development console, I see an appropriate grpc-status header code of 16 with an appropriately matching grpc-message header.

I have also tried responding with NotFound which is code 5, but that doesn't change what is exposed as e.

isosphere commented 1 year ago

Code Review

I'm not familiar with this codebase; here's my attempt at troubleshooting the above. I'll edit this post as I proceed.

The "malformed response" seems to be defined here: https://github.com/devashishdxt/tonic-web-wasm-client/blob/ca7f5c9d1152000d862810accb2df9c60ba2d963/src/error.rs#L34

This is referenced twice: https://github.com/devashishdxt/tonic-web-wasm-client/blob/ca7f5c9d1152000d862810accb2df9c60ba2d963/src/response_body.rs#L303-L306

https://github.com/devashishdxt/tonic-web-wasm-client/blob/ca7f5c9d1152000d862810accb2df9c60ba2d963/src/response_body.rs#L333-L336

Debug Prints

I changed the first usage to return a new error that I defined called PartyMode - this would at least let me identify which is responsible for the MalformedResponse error. Responses then print Error: Unknown, "party mode" in the console, so we know that this is responsible:

https://github.com/devashishdxt/tonic-web-wasm-client/blob/ca7f5c9d1152000d862810accb2df9c60ba2d963/src/response_body.rs#L303-L306

Is My Server's GRPC Response Valid?

The only way I can think of to validate this is by using a different client that I trust, so I'll use Kreya.

[22:17:52.114] KreyaCallStart
               disableServerCertificateValidation: false 
               mode: Grpc 
[22:17:52.116] CallStart
               method: /auth.AuthenticationService/GetAuthenticationToken 
[22:17:52.116] RequestStart
               host: 127.0.0.1 
               method: POST 
               pathAndQuery: /auth.AuthenticationService/GetAuthenticationToken 
               port: 5000 
               scheme: http 
[22:17:52.120] ConnectionEstablished
               versionMajor: 2 
               versionMinor: 0 
[22:17:52.184] MessageSent
[22:17:52.206] RequestStop 90ms
[22:17:52.209] CallFailed
               statusCode: 5 <--- "NotFound", which is what I'm returning
[22:17:52.209] CallStop 93ms
[22:17:52.211] KreyaCallStop

Seems fine.

Packet Dump

The following is the HTTP response from the GRPC server.

Printable

HTTP/1.1 200 OK
content-type: application/grpc-web+proto
grpc-status: 5
grpc-message: Not%20found
access-control-allow-origin: http://127.0.0.1:8080
vary: origin
vary: access-control-request-method
vary: access-control-request-headers
content-length: 0 <-- maybe this is the issue?
date: Fri, 10 Mar 2023 02:31:11 GMT

Hex Stream

485454502f312e3120323030204f4b0d0a636f6e74656e742d747970653a206170706c69636174696f6e2f677270632d7765622b70726f746f0d0a677270632d7374617475733a20350d0a677270632d6d6573736167653a204e6f74253230666f756e640d0a6163636573732d636f6e74726f6c2d616c6c6f772d6f726967696e3a20687474703a2f2f3132372e302e302e313a383038300d0a766172793a206f726967696e0d0a766172793a206163636573732d636f6e74726f6c2d726571756573742d6d6574686f640d0a766172793a206163636573732d636f6e74726f6c2d726571756573742d686561646572730d0a636f6e74656e742d6c656e6774683a20300d0a646174653a204672692c203130204d617220323032332030323a33313a313120474d540d0a0d0a
devashishdxt commented 1 year ago

Hi. Thanks for creating this issue.

I've created a PR to test error responses from server and the test seems to pass: #32.

Can you share your server code so that I have more context on what you're trying to do?

isosphere commented 1 year ago

Here's a very abbreviated pseudo-code snippet. This is using Tonic 0.8 with tonic_web::GrpcWebLayer 0.5.0:

Main

async fn main() -> Result<...> {
    let authentication_service_state = MyAuthTokenServer{some_parameter = true};
    let cors = CorsLayer::new()
        .allow_methods([Method::GET, Method::POST, Method::OPTIONS])
        .allow_headers(Any)
        .allow_origin(format!("http://{}:8080", matches.grpc_address).parse::<HeaderValue>().unwrap());

    Server::builder()
        .accept_http1(true) // required for tonic-web; as of this writing it presents as HTTP/1.1
        .layer(cors)
        .layer(CookieManagerLayer::new())
        .layer(GrpcWebLayer::new())
        .add_service(AuthenticationServiceServer::new(authentication_service_state))
        .serve(addr)
        .await?;
    Ok(())
}

Service Implementation


stuct MyAuthTokenServer{some_parameter: type}

#[tonic::async_trait]
impl AuthenticationService for MyAuthTokenServer {
    async fn get_authentication_token(&self, request: Request<AuthTokenRequest>) -> Result<Response<AuthTokenReponse>, Status> {
        let user_instance = match get_customer_by_email(&mut connection, &request_packet.username) {
            Ok(u) => u,
            Err(e) => {
                warn!("Failed to find user with email {:?}: {:?}", &request_packet.username, e);
                return Err(Status::not_found("Not found"));
            }
        };
    }
}
devashishdxt commented 1 year ago

This PR: #32 has almost the same test case which passes. I'm not sure if anything can be done on this crate to fix your issue.

devashishdxt commented 1 year ago

Maybe you can review your cors configuration. Here's a sample: https://github.com/devashishdxt/tonic-web-wasm-client/blob/ca7f5c9d1152000d862810accb2df9c60ba2d963/test-suite/simple/server/src/main.rs#L116.

devashishdxt commented 1 year ago

Closing this issue for now. If you still see the error, please create PR with a test case that fails and feel free to reopen.

isosphere commented 1 year ago

I copied and pasted your CorsLayer invocation and it resolved the issue, good call.

After some digging I discovered the exact problem was that my lack of a .expose_headers invocation implicitly did not include "grpc-status". How can my code report the status code if I have forbidden the header that provides it? Then presumably the tonic-web-wasm-client code goes looking for that code, doesn't find it, and so returns "malformed response", which makes sense.