Skip to content

Http2 protocol violation #633

@erebe

Description

@erebe

Hello,

I am reporting an issue I have encountered while using tonic grpc server (v0.8.0).
We use nginx as a GRPC reverse proxy and hit the following corner case.
Full details here: https://trac.nginx.org/nginx/ticket/2376 you can also find a pcap capture of the issue in the ticket

With this setup
[client] === grpc stream (with ssl) ===> [nginx] === grpc stream (cleartext) ===> [backend]

and considering the following grpc service description

service Agent {
     rpc StreamToServer(stream Empty {}) returns (Empty) {}
}

If the http2 server generated by tonic early respond to the client without consuming fully the input stream

impl Agent {
    async fn stream_to_server (
        &self,
        request: Request<Streaming<Empty>>,
    ) -> Result<Response<Empty>, Status> {
        let _ = request.into_inner();
        // we don't care of the request at all
        // Just sending back our response before end of client stream
        Ok(Response::new(Empty {}))
    }
}

The following packets are going to be emitted by tonic/hyper/h2, in response to the call.

HEADERS[1],  DATA[1] (GRPC empty data),  HEADERS[1] (TRAILING with flag end_stream) , RST_STREAM(error: CANCEL)[1]

This specific sequence of packet is causing nginx to miss-behave and not forward the DATA & RST_STREAM back to the client.
After discussion with a maintainer of nginx, this is caused by the last RST_STREAM(error: CANCEL) which is invalid in regard of the spec, it should be a RST_STREAM(NO_ERROR)

As per the RFC

   ... A server can
   send a complete response prior to the client sending an entire
   request if the response does not depend on any portion of the request
   that has not been sent and received.  When this is true, a server MAY
   request that the client abort transmission of a request without error
   by sending a RST_STREAM with an error code of NO_ERROR after sending
   a complete response (i.e., a frame with the END_STREAM flag).
   Clients MUST NOT discard responses as a result of receiving such a
   RST_STREAM, though clients can always discard responses at their
   discretion for other reasons.

I tracked down where is coming this RST_STREAM(CANCEL), at first I thought it was in tonic but it ended-up being in the impl Drop of h2

h2::proto::streams::streams::maybe_cancel streams.rs:1467
h2::proto::streams::streams::drop_stream_ref::{closure#0} streams.rs:1443
h2::proto::streams::counts::Counts::transition<h2::proto::streams::streams::drop_stream_ref::{closure_env#0}, ()> counts.rs:127
h2::proto::streams::streams::drop_stream_ref streams.rs:1442
h2::proto::streams::streams::{impl#12}::drop streams.rs:1403

.schedule_implicit_reset(stream, Reason::CANCEL, counts, &mut actions.task);

So it seems a generic way of handling the end of stream for h2.
And now I am stuck in my investigation and require some guidance regarding where to go from here.

  • Replacing the Reason::CANCEL by Reason::NO_ERROR solves my issue with nginx (tested locally) and seem more adequate in regard of the RFC, but I don't know if there is un-expected side effect to that for other cases
  • I don't know if it is tonic or hyper that miss use h2 and require somewhere some special handling for this scenario

We really like tonic and nginx and would appreciate if we can go forward to make both happy to work together

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions