hypercorn icon indicating copy to clipboard operation
hypercorn copied to clipboard

h2/h3 trailing header support. Fixes #147

Open jeffsawatzky opened this issue 1 year ago • 1 comments

This is an initial implementation of trailing header suppport for http2/http3. This fixes #147, in theory.

Here's the thing though...I have no clue what I am doing. I would like some tips on how to do the following:

  1. Ensure that I am following the asgi spec properly
  2. Ensure that I am following the HTTP/2 HTTP/3 spec properly

I assume the underlying h2 and h3 libraries take care of (2) for me, but I'm not sure.

jeffsawatzky avatar Nov 22 '23 21:11 jeffsawatzky

@pgjones can you provide feedback/comments on my stuff so far? Don't want to get to far into this if I'm going in the wrong direction. Thanks.

jeffsawatzky avatar Nov 23 '23 19:11 jeffsawatzky

Thanks for this, I've adapted it a bit and merged in d8de5f28adc99b1bc9b47a6c557fe25972ab966f

pgjones avatar May 27 '24 13:05 pgjones

I did a quick test with this implementation with python-grpc-client. The asgi app just works like a grpc server, however, the client is not happy with the server and lost the connection half way. It seems that the server send trailers after EndBody been send, which closed the connection. maybe we should avoid this. I'll dig deeper tomorrow.

synodriver avatar May 27 '24 17:05 synodriver

Thanks both, hopefully fixed with d16b50398aaedc509f83fe2b5c6c83a0ffbfc991

pgjones avatar May 27 '24 19:05 pgjones

Emmmm... That didn't work either. Here is my asgi app which pretend to be a grpc server(A simple grpc echo server)

async def grpcapp(scope, receive, send):
    if scope["type"] == "http":
        body = (await receive())["body"]
        await send(
            {
                "type": "http.response.start",
                "status": 200,
                "headers": [
                    (b"Content-Type", b"application/grpc+proto"),
                    (b"Cache-Control", b"no-cache"),
                    (b"Trailer", b"grpc-status")
                ],
                "trailers": True,
            }
        )

        await send({"type": "http.response.body", "body": body})
        await send({"type": "http.response.trailers", "headers": [(b"grpc-status", b"0")]})

A real grpc client would expect the same response type and value when talk to the server, but with grpclib in python, I just got

Traceback (most recent call last):
  File "D:\conda\envs\py310\lib\site-packages\grpclib\client.py", line 468, in recv_trailing_metadata
    trailers = await self._stream.recv_trailers()
  File "D:\conda\envs\py310\lib\site-packages\grpclib\protocol.py", line 351, in recv_trailers
    await self.trailers_received.wait()
  File "D:\conda\envs\py310\lib\asyncio\locks.py", line 214, in wait
    await fut
asyncio.exceptions.CancelledError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "E:\pyproject\nonecorn\src\pclient.py", line 28, in <module>
    asyncio.run(main())
  File "D:\conda\envs\py310\lib\asyncio\runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "D:\conda\envs\py310\lib\asyncio\base_events.py", line 649, in run_until_complete
    return future.result()
  File "E:\pyproject\nonecorn\src\pclient.py", line 23, in main
    reply = await greeter.SayHello(Test(id=2, data="xsndjasndsa"))
  File "D:\conda\envs\py310\lib\site-packages\grpclib\client.py", line 902, in __call__
    async with self.open(timeout=timeout, metadata=metadata) as stream:
  File "D:\conda\envs\py310\lib\site-packages\grpclib\client.py", line 563, in __aexit__
    raise exc_val
  File "D:\conda\envs\py310\lib\site-packages\grpclib\client.py", line 553, in __aexit__
    await self._maybe_finish()
  File "D:\conda\envs\py310\lib\site-packages\grpclib\client.py", line 523, in _maybe_finish
    await self.recv_trailing_metadata()
  File "D:\conda\envs\py310\lib\site-packages\grpclib\client.py", line 467, in recv_trailing_metadata
    with self._wrapper:
  File "D:\conda\envs\py310\lib\site-packages\grpclib\utils.py", line 70, in __exit__
    raise self._error
grpclib.exceptions.StreamTerminatedError: Connection lost

And here is the client side:

import asyncio
import os
import ssl
from datetime import datetime

os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python"
from grpclib.client import Channel

from test_grpc import TestServerStub

# generated by protoc, it doens't matter since you can generate your own
from test_pb2 import Test


async def main():
    async with Channel("127.0.0.1", 9001, ssl=False) as channel:
        greeter = TestServerStub(channel)

        reply = await greeter.SayHello(Test(id=2, data="xsndjasndsa"))
        print(reply)


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

Besides, grpcurl also failed with ERROR: Code: Internal Message: server closed the stream without sending trailers

synodriver avatar May 28 '24 11:05 synodriver