hypercorn icon indicating copy to clipboard operation
hypercorn copied to clipboard

HTTP2 Trailers not being sent

Open adambudziak opened this issue 1 year ago • 7 comments

Hi, I know that it's probably a stretch since it's likely covered by a bunch of tests, but I tried everything and I can't get it to work. So I'm playing with connecting gRPC to ASGI using hypercorn, and I made this very simple app:

import asyncio
from hypercorn.config import Config
from hypercorn.asyncio import serve

async def grpc_echo(scope, receive, send):
    if scope['type'] == 'http' and scope['method'] == 'POST':
        message = await receive()
        request_body = message.get('body', b'')

        # Send headers
        await send({
            'type': 'http.response.start',
            'status': 200,
            'headers': [
                (b'content-type', b'application/grpc'),
            ],
            'trailers': True
        })

        await send({
            'type': 'http.response.body',
            'body': request_body,
        })

        await send({
            'type': 'http.response.trailers',
            'headers': [
                (b'grpc-status', b'0'),
                (b'grpc-message', b'OK'),
            ],
        })
    else:
        raise NotImplementedError


async def main():
    config = Config()
    config.bind = ["localhost:50051"]
    await serve(grpc_echo, config)

if __name__ == "__main__":
    asyncio.run(main())

here's the proto (though I doubt it matters)

syntax = "proto3";

package helloworld;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloRequest) {}
}

message HelloRequest {
  string name = 1;
}

I run the client by grpcurl -proto helloworld.proto -plaintext -d '{"name": "World"}' localhost:50051 helloworld.Greeter/SayHello

and the error I get is

  Code: Internal
  Message: server closed the stream without sending trailers

This area is completely new to me so there might be some rookie mistake (maybe not related to trailers at all), but I checked the ASGI spec, and my trailers look ok, I ran it through debugger and it looks like it's being sent, however I don't see them in Wireshark and the clients (I tried Postman and grpcurl) both fail.

In the debugger I saw that the response body is sent (I see the "World" string), I also see some stuff being sent afterwards, but nothing looking like the trailers that I set.

Any help is appreciated, and congrats for an amazing lib, it's been a long time since I saw one that's so readable inside.

adambudziak avatar Aug 30 '24 19:08 adambudziak

Ok, so I did more digging, and it looks like some issue with end_stream value in H2Stream.send_headers.

https://github.com/python-hyper/h2/blob/master/src/h2/stream.py#L863the function taken from the map here sets trailers_sent in the state machine to true.

Then this check https://github.com/python-hyper/h2/blob/master/src/h2/stream.py#L877 always raises an exception because trailers_sent is True, but end_stream is always False (hypercorn doesn't set it).

(it looks correct since according to the spec "The HEADERS frame starting the trailers header block has the END_STREAM flag set.")

I tried setting end_stream to True, but then I get the following error in grpcurl.

Error invoking method "helloworld.Greeter/SayHello": grpc call for "helloworld.Greeter.SayHello" failed: EOF

however it works better in Postman (I see the status code, but no response body).

I'm gonna keep investigating, it's likely that it's a different issue.

adambudziak avatar Aug 31 '24 08:08 adambudziak

It looks like because trailers now set end_stream=True, the response body isn't sent. I made a "fix" (quotes because I really have no idea what I'm doing). I'll check if tox passes and if so, I'll make a PR.

adambudziak avatar Aug 31 '24 09:08 adambudziak

It looks like because trailers now set end_stream=True, the response body isn't sent. I made a "fix" (quotes because I really have no idea what I'm doing). I'll check if tox passes and if so, I'll make a PR.

Tried your fix, and it worked for me! Although I don't understand the intention of other lines except one with end_stream=True. Also, unfortunately your MR remains open for half of a year, so it can be applied only locally yet.

drevoborod avatar Feb 17 '25 15:02 drevoborod

TBH I just saw them in the other places where end_stream=True was used. I tried to understand more if I need to call these things, but it was too deep for me. But it worked!

adambudziak avatar Feb 17 '25 20:02 adambudziak

Tried your fix, and it worked for me! Although I don't understand the intention of other lines except one with end_stream=True. Also, unfortunately your MR remains open for half of a year, so it can be applied only locally yet.

@pgjones Hypercorn needs some due inputs from you.

XChikuX avatar Mar 07 '25 15:03 XChikuX

I'm facing the same issue and I can confirm that the proposed modification also works in my use case.

JRial95 avatar Jun 11 '25 11:06 JRial95

I have the same problem, the modification also works

oktaydavid avatar Jun 12 '25 07:06 oktaydavid