Skip to content

Commit ea4b387

Browse files
authored
Introduce PRIORITY_UPDATE frame and priority tracking per stream. (#25)
1 parent 4b680eb commit ea4b387

File tree

12 files changed

+225
-75
lines changed

12 files changed

+225
-75
lines changed

fixtures/protocol/http2/a_frame.rb

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Released under the MIT License.
44
# Copyright, 2019-2024, by Samuel Williams.
55

6+
require "socket"
67
require "protocol/http2/framer"
78

89
module Protocol

lib/protocol/http2/client.rb

+13-3
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,25 @@ def send_connection_preface(settings = [])
2929
@framer.write_connection_preface
3030

3131
# We don't support RFC7540 priorities:
32-
settings = settings.to_a
33-
settings << [Settings::NO_RFC7540_PRIORITIES, 1]
32+
if settings.is_a?(Hash)
33+
settings = settings.dup
34+
else
35+
settings = settings.to_h
36+
end
37+
38+
unless settings.key?(Settings::NO_RFC7540_PRIORITIES)
39+
settings = settings.dup
40+
settings[Settings::NO_RFC7540_PRIORITIES] = 1
41+
end
3442

3543
send_settings(settings)
3644

3745
yield if block_given?
3846

3947
read_frame do |frame|
40-
raise ProtocolError, "First frame must be #{SettingsFrame}, but got #{frame.class}" unless frame.is_a? SettingsFrame
48+
unless frame.is_a? SettingsFrame
49+
raise ProtocolError, "First frame must be #{SettingsFrame}, but got #{frame.class}"
50+
end
4151
end
4252
else
4353
raise ProtocolError, "Cannot send connection preface in state #{@state}"

lib/protocol/http2/connection.rb

+14
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
require_relative "flow_controlled"
99

1010
require "protocol/hpack"
11+
require "protocol/http/header/priority"
1112

1213
module Protocol
1314
module HTTP2
@@ -400,6 +401,19 @@ def receive_push_promise(frame)
400401
raise ProtocolError, "Unable to receive push promise!"
401402
end
402403

404+
def receive_priority_update(frame)
405+
if frame.stream_id != 0
406+
raise ProtocolError, "Invalid stream id: #{frame.stream_id}"
407+
end
408+
409+
stream_id, value = frame.unpack
410+
411+
# Apparently you can set the priority of idle streams, but I'm not sure why that makes sense, so for now let's ignore it.
412+
if stream = @streams[stream_id]
413+
stream.priority = Protocol::HTTP::Header::Priority.new(value)
414+
end
415+
end
416+
403417
def client_stream_id?(id)
404418
id.odd?
405419
end

lib/protocol/http2/framer.rb

+8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
require_relative "goaway_frame"
1515
require_relative "window_update_frame"
1616
require_relative "continuation_frame"
17+
require_relative "priority_update_frame"
1718

1819
module Protocol
1920
module HTTP2
@@ -29,6 +30,13 @@ module HTTP2
2930
GoawayFrame,
3031
WindowUpdateFrame,
3132
ContinuationFrame,
33+
nil,
34+
nil,
35+
nil,
36+
nil,
37+
nil,
38+
nil,
39+
PriorityUpdateFrame,
3240
].freeze
3341

3442
# Default connection "fast-fail" preamble string as defined by the spec.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2019-2024, by Samuel Williams.
5+
6+
require_relative "frame"
7+
require_relative "padded"
8+
require_relative "continuation_frame"
9+
10+
module Protocol
11+
module HTTP2
12+
# The PRIORITY_UPDATE frame is used by clients to signal the initial priority of a response, or to reprioritize a response or push stream. It carries the stream ID of the response and the priority in ASCII text, using the same representation as the Priority header field value.
13+
#
14+
# +-+-------------+-----------------------------------------------+
15+
# |R| Prioritized Stream ID (31) |
16+
# +-+-----------------------------+-------------------------------+
17+
# | Priority Field Value (*) ...
18+
# +---------------------------------------------------------------+
19+
#
20+
class PriorityUpdateFrame < Frame
21+
TYPE = 0x10
22+
FORMAT = "N".freeze
23+
24+
def unpack
25+
data = super
26+
27+
prioritized_stream_id = data.unpack1(FORMAT)
28+
29+
return prioritized_stream_id, data.byteslice(4, data.bytesize - 4)
30+
end
31+
32+
def pack(prioritized_stream_id, data, **options)
33+
super([prioritized_stream_id].pack(FORMAT) + data, **options)
34+
end
35+
36+
def apply(connection)
37+
connection.receive_priority_update(self)
38+
end
39+
end
40+
end
41+
end

lib/protocol/http2/server.rb

+10-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,16 @@ def read_connection_preface(settings = [])
2929
@framer.read_connection_preface
3030

3131
# We don't support RFC7540 priorities:
32-
settings = settings.to_a
33-
settings << [Settings::NO_RFC7540_PRIORITIES, 1]
32+
if settings.is_a?(Hash)
33+
settings = settings.dup
34+
else
35+
settings = settings.to_h
36+
end
37+
38+
unless settings.key?(Settings::NO_RFC7540_PRIORITIES)
39+
settings = settings.dup
40+
settings[Settings::NO_RFC7540_PRIORITIES] = 1
41+
end
3442

3543
send_settings(settings)
3644

lib/protocol/http2/settings_frame.rb

+26-44
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,22 @@ class Settings
2727
:maximum_header_list_size=,
2828
nil,
2929
:enable_connect_protocol=,
30+
:no_rfc7540_priorities=,
3031
]
3132

33+
def initialize
34+
# These limits are taken from the RFC:
35+
# https://tools.ietf.org/html/rfc7540#section-6.5.2
36+
@header_table_size = 4096
37+
@enable_push = 1
38+
@maximum_concurrent_streams = 0xFFFFFFFF
39+
@initial_window_size = 0xFFFF # 2**16 - 1
40+
@maximum_frame_size = 0x4000 # 2**14
41+
@maximum_header_list_size = 0xFFFFFFFF
42+
@enable_connect_protocol = 0
43+
@no_rfc7540_priorities = 0
44+
end
45+
3246
# Allows the sender to inform the remote endpoint of the maximum size of the header compression table used to decode header blocks, in octets.
3347
attr_accessor :header_table_size
3448

@@ -91,16 +105,18 @@ def enable_connect_protocol?
91105
@enable_connect_protocol == 1
92106
end
93107

94-
def initialize
95-
# These limits are taken from the RFC:
96-
# https://tools.ietf.org/html/rfc7540#section-6.5.2
97-
@header_table_size = 4096
98-
@enable_push = 1
99-
@maximum_concurrent_streams = 0xFFFFFFFF
100-
@initial_window_size = 0xFFFF # 2**16 - 1
101-
@maximum_frame_size = 0x4000 # 2**14
102-
@maximum_header_list_size = 0xFFFFFFFF
103-
@enable_connect_protocol = 0
108+
attr :no_rfc7540_priorities
109+
110+
def no_rfc7540_priorities= value
111+
if value == 0 or value == 1
112+
@no_rfc7540_priorities = value
113+
else
114+
raise ProtocolError, "Invalid value for no_rfc7540_priorities: #{value}"
115+
end
116+
end
117+
118+
def no_rfc7540_priorities?
119+
@no_rfc7540_priorities == 1
104120
end
105121

106122
def update(changes)
@@ -110,40 +126,6 @@ def update(changes)
110126
end
111127
end
112128
end
113-
114-
def difference(other)
115-
changes = []
116-
117-
if @header_table_size != other.header_table_size
118-
changes << [HEADER_TABLE_SIZE, @header_table_size]
119-
end
120-
121-
if @enable_push != other.enable_push
122-
changes << [ENABLE_PUSH, @enable_push]
123-
end
124-
125-
if @maximum_concurrent_streams != other.maximum_concurrent_streams
126-
changes << [MAXIMUM_CONCURRENT_STREAMS, @maximum_concurrent_streams]
127-
end
128-
129-
if @initial_window_size != other.initial_window_size
130-
changes << [INITIAL_WINDOW_SIZE, @initial_window_size]
131-
end
132-
133-
if @maximum_frame_size != other.maximum_frame_size
134-
changes << [MAXIMUM_FRAME_SIZE, @maximum_frame_size]
135-
end
136-
137-
if @maximum_header_list_size != other.maximum_header_list_size
138-
changes << [MAXIMUM_HEADER_LIST_SIZE, @maximum_header_list_size]
139-
end
140-
141-
if @enable_connect_protocol != other.enable_connect_protocol
142-
changes << [ENABLE_CONNECT_PROTOCOL, @enable_connect_protocol]
143-
end
144-
145-
return changes
146-
end
147129
end
148130

149131
class PendingSettings

lib/protocol/http2/stream.rb

+5
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ def initialize(connection, id, state = :idle)
7676

7777
@local_window = Window.new(@connection.local_settings.initial_window_size)
7878
@remote_window = Window.new(@connection.remote_settings.initial_window_size)
79+
80+
@priority = nil
7981
end
8082

8183
# The connection this stream belongs to.
@@ -90,6 +92,9 @@ def initialize(connection, id, state = :idle)
9092
attr :local_window
9193
attr :remote_window
9294

95+
# @attribute [Protocol::HTTP::Header::Priority | Nil] the priority of the stream.
96+
attr_accessor :priority
97+
9398
def maximum_frame_size
9499
@connection.available_frame_size
95100
end

test/protocol/http2/client.rb

+22-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535
client_settings_frame = framer.read_frame
3636
expect(client_settings_frame).to be_a Protocol::HTTP2::SettingsFrame
37-
expect(client_settings_frame.unpack).to be == settings
37+
expect(client_settings_frame.unpack).to be == settings + [[Protocol::HTTP2::Settings::NO_RFC7540_PRIORITIES, 1]]
3838

3939
# Fake (empty) server settings:
4040
server_settings_frame = Protocol::HTTP2::SettingsFrame.new
@@ -52,6 +52,27 @@
5252
expect(client.local_settings.header_table_size).to be == 1024
5353
end
5454

55+
it "should fail if the server does not reply with settings frame" do
56+
data_frame = Protocol::HTTP2::DataFrame.new
57+
data_frame.pack("Hello, World!")
58+
59+
expect do
60+
client.send_connection_preface(settings) do
61+
framer.write_frame(data_frame)
62+
end
63+
end.to raise_exception(Protocol::HTTP2::ProtocolError, message: be =~ /First frame must be Protocol::HTTP2::SettingsFrame/)
64+
end
65+
66+
it "should send connection preface with no RFC7540 priorities" do
67+
server_settings_frame = client.send_connection_preface({}) do
68+
client_settings_frame = server.read_connection_preface({})
69+
70+
expect(client_settings_frame.unpack).to be == [[Protocol::HTTP2::Settings::NO_RFC7540_PRIORITIES, 1]]
71+
end
72+
73+
expect(server_settings_frame.unpack).to be == [[Protocol::HTTP2::Settings::NO_RFC7540_PRIORITIES, 1]]
74+
end
75+
5576
it "can generate a stream id" do
5677
id = client.next_stream_id
5778
expect(id).to be == 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# frozen_string_literal: true
2+
# Released under the MIT License.
3+
# Copyright, 2019-2024, by Samuel Williams.
4+
5+
require "protocol/http2/priority_update_frame"
6+
7+
require "protocol/http2/a_frame"
8+
require "protocol/http2/connection_context"
9+
10+
describe Protocol::HTTP2::PriorityUpdateFrame do
11+
let(:frame) {subject.new}
12+
13+
it_behaves_like Protocol::HTTP2::AFrame do
14+
before do
15+
frame.pack 1, "u=1, i"
16+
end
17+
18+
it "applies to the connection" do
19+
expect(frame).to be(:connection?)
20+
end
21+
end
22+
23+
with "client/server connection" do
24+
include_context Protocol::HTTP2::ConnectionContext
25+
26+
def before
27+
client.open!
28+
server.open!
29+
30+
super
31+
end
32+
33+
it "fails with protocol error if stream id is not zero" do
34+
# This isn't a valid for the frame stream_id:
35+
frame.stream_id = 1
36+
37+
# This is a valid stream payload:
38+
frame.pack stream.id, "u=1, i"
39+
40+
expect do
41+
frame.apply(server)
42+
end.to raise_exception(Protocol::HTTP2::ProtocolError)
43+
end
44+
45+
let(:stream) {client.create_stream}
46+
47+
it "updates the priority of a stream" do
48+
stream.send_headers [["content-type", "text/plain"]]
49+
server.read_frame
50+
51+
expect(server).to receive(:receive_priority_update)
52+
expect(stream.priority).to be_nil
53+
54+
frame.pack stream.id, "u=1, i"
55+
client.write_frame(frame)
56+
57+
inform server.read_frame
58+
59+
server_stream = server.streams[stream.id]
60+
expect(server_stream).not.to be_nil
61+
62+
expect(server_stream.priority).to be == ["u=1", "i"]
63+
end
64+
end
65+
end

test/protocol/http2/server.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
# The server immediately sends its own settings frame...
7979
frame = framer.read_frame
8080
expect(frame).to be_a Protocol::HTTP2::SettingsFrame
81-
expect(frame.unpack).to be == server_settings
81+
expect(frame.unpack).to be == server_settings + [[Protocol::HTTP2::Settings::NO_RFC7540_PRIORITIES, 1]]
8282

8383
# And then it acknowledges the client settings:
8484
frame = framer.read_frame

0 commit comments

Comments
 (0)