Skip to content

Ensure wire always contains a full H/2 frame - #15

Merged
ioquatix merged 7 commits into
socketry:frame-header-peekfrom
maruth-stripe:main
Jun 10, 2024
Merged

Ensure wire always contains a full H/2 frame#15
ioquatix merged 7 commits into
socketry:frame-header-peekfrom
maruth-stripe:main

Conversation

@maruth-stripe

@maruth-stripemaruth-stripe commented Nov 6, 2023

Copy link
Copy Markdown

Follow-up to socketry/async-io#72 to fix#14
Updates read_header to use Stream#peek instead of `Stream#read. We maintain the invariant that the read buffer / wire is always frame-aligned. Instead of reading data off the wire when reading the header for an H/2 frame, we just peek the header and then read the full frame.

Types of Changes

  • Bug fix.

Contribution

@maruth-stripe
maruth-stripe marked this pull request as ready for review November 9, 2023 15:00
@maruth-stripe

maruth-stripe commented Nov 9, 2023

Copy link
Copy Markdown
Author

Verified the fix works with the following script (modification to the repro in #14)

# requiresrequire"stringio"require"async/reactor"classFunkyIOdefinitialize@f=Protocol::HTTP2::DataFrame.new(401,0,Protocol::HTTP2::DataFrame::TYPE,13,"a" * 13)@sio=StringIO.new@f.write_header(@sio)@sio.rewind@state=:read_headerenddefpeek(size,buf=nil)if@state == :read_header || @state == :try_header_againres=@sio.read(size,buf)case@statewhen:read_header@state=:now_timeoutwhen:try_header_again@state=:read_frameend@sio=StringIO.new@f.write_header(@sio)@f.write_payload(@sio)@sio.rewindreturnresendenddefread(size,buf=nil)case@statewhen:now_timeout@state=:try_header_againraiseAsync::TimeoutErrorwhen:read_frameres=@sio.read(size,buf)resendendendf=Protocol::HTTP2::Framer.new(FunkyIO.new)Async::Reactor.rundobeginp(f.read_frame)rescueAsync::TimeoutError# try againendp(f.read_frame)end

outputs
#<Protocol::HTTP2::DataFrame stream_id=401 flags=0 13b>

@ioquatix

Copy link
Copy Markdown
Member

I released async-io v1.37.0 which I assume we can use here?

@ioquatixioquatix self-assigned this Nov 10, 2023
@ioquatixioquatix added the bug Something isn't working label Nov 10, 2023
@maruth-stripe

Copy link
Copy Markdown
Author

Hi @ioquatix really sorry for disappearing on this PR for a bit, got caught up with other stuff!

I've updated it a bit, and tested it with the follwing script:

# requiresrequire"stringio"require"async"require"async/reactor"require"protocol/http2/framer"require"protocol/http2/data_frame"classFunkyIOdefinitialize@f=Protocol::HTTP2::DataFrame.new(401,0,Protocol::HTTP2::DataFrame::TYPE,13,"a" * 13)@sio=StringIO.new@f.write_header(@sio)@sio.rewind@state=:read_headerenddefsync=(x)enddefread_nonblock(size,buf=nil,exception)puts"read_nonblock(#{size}) state: #{@state}"if@state == :read_header || @state == :try_header_againres=@sio.read(size,buf)case@statewhen:read_header@state=:now_timeoutwhen:try_header_again@state=:read_frameend@sio=StringIO.new@f.write_payload(@sio)@sio.rewindreturnreselsif@state == :now_timeout@state=:try_header_againraiseAsync::TimeoutErrorelsif@state == :read_framereturn@sio.read(size,buf)endendendf=Protocol::HTTP2::Framer.new(FunkyIO.new)Async::Reactor.rundobeginp(f.read_frame)rescueAsync::TimeoutError# try againendp(f.read_frame)end

which outputs

read_nonblock(65536) state: read_header
read_nonblock(65536) state: now_timeout
read_nonblock(65536) state: try_header_again
#<Protocol::HTTP2::DataFrame stream_id=401 flags=0 13b>

I'm not sure why but bundle exec bake test seems to be taking quite a while. Unsure if it's something messed up with my local setup.

@maruth-stripe

Copy link
Copy Markdown
Author

One note: this PR now implicitly adds a requirement that the stream passed into Framer must implement sync= and read_nonblock (or be an `Async::IO::Stream)

I believe StringIO, Socket do implement this.

@maruth-stripe

Copy link
Copy Markdown
Author

@ioquatix do you know why the test suite might be timing out here?

@ioquatix

Copy link
Copy Markdown
Member

The next step for this code is to work with standard IO instances. However, I don't think a method like peek exists for IO. Do you have any ideas?

@ioquatix

Copy link
Copy Markdown
Member

After considering this further, I believe we should adopt the peek(n) model in this code. We will have to use an appropriate wrapper.

@ioquatix
ioquatix changed the base branch from main to frame-header-peekJune 10, 2024 07:55
@ioquatix
ioquatix merged commit cb304c8 into socketry:frame-header-peekJun 10, 2024
@ioquatix

Copy link
Copy Markdown
Member

I'm going to merge this into another branch so I can finish it off.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugSomething isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Framer's sequential reads of frame header then payload can leave underlying async stream's @read_buffer in a corrupted state

2 participants

@maruth-stripe@ioquatix