From 304c40de2b55f22190c8ac1ec6e54ad880056cfc Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 19 Jul 2026 19:08:46 +1200 Subject: [PATCH 1/5] Add stream error messages --- lib/protocol/http2/error.rb | 25 +++++++++++++++++++++++++ lib/protocol/http2/stream.rb | 2 +- test/protocol/http2/error.rb | 20 ++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/lib/protocol/http2/error.rb b/lib/protocol/http2/error.rb index ed0ee82..4f945ee 100644 --- a/lib/protocol/http2/error.rb +++ b/lib/protocol/http2/error.rb @@ -76,6 +76,31 @@ def initialize(message, code = PROTOCOL_ERROR) # Represents an error specific to stream operations. class StreamError < ProtocolError + MESSAGES = { + Error::NO_ERROR => "No error.", + Error::PROTOCOL_ERROR => "Protocol error!", + Error::INTERNAL_ERROR => "Internal error!", + Error::FLOW_CONTROL_ERROR => "Flow control error!", + Error::SETTINGS_TIMEOUT => "Settings timeout!", + Error::STREAM_CLOSED => "Stream closed!", + Error::FRAME_SIZE_ERROR => "Frame size error!", + Error::REFUSED_STREAM => "Stream refused.", + Error::CANCEL => "Stream cancelled.", + Error::COMPRESSION_ERROR => "Compression error!", + Error::CONNECT_ERROR => "Connect error!", + Error::ENHANCE_YOUR_CALM => "Enhance your calm!", + Error::INADEQUATE_SECURITY => "Inadequate security!", + Error::HTTP_1_1_REQUIRED => "HTTP/1.1 required.", + } + + def self.for(code) + message = MESSAGES.fetch(code) do + # Unknown error codes are allowed by the protocol, but don't have a static message. + "Unknown code: #{code}!" + end + + return self.new(message, code) + end end # Represents an error for operations on closed streams. diff --git a/lib/protocol/http2/stream.rb b/lib/protocol/http2/stream.rb index 5748964..dc13f8e 100644 --- a/lib/protocol/http2/stream.rb +++ b/lib/protocol/http2/stream.rb @@ -253,7 +253,7 @@ def close!(error_code = nil) if error_code == REFUSED_STREAM error = ::Protocol::HTTP::RefusedError.new("Stream refused.") else - error = StreamError.new("Stream closed!", error_code) + error = StreamError.for(error_code) end end diff --git a/test/protocol/http2/error.rb b/test/protocol/http2/error.rb index 950460e..f7282d5 100644 --- a/test/protocol/http2/error.rb +++ b/test/protocol/http2/error.rb @@ -5,6 +5,26 @@ require "protocol/http2/error" +describe Protocol::HTTP2::StreamError do + with ".for" do + it "builds a stream error for a known error code" do + error = subject.for(Protocol::HTTP2::Error::CANCEL) + + expect(error).to be_a(subject) + expect(error.message).to be == "Stream cancelled." + expect(error.code).to be == Protocol::HTTP2::Error::CANCEL + end + + it "builds a stream error for an unknown error code" do + error = subject.for(99) + + expect(error).to be_a(subject) + expect(error.message).to be == "Unknown code: 99!" + expect(error.code).to be == 99 + end + end +end + describe Protocol::HTTP2::HeaderError do let(:error) {subject.new("Invalid header key")} From 2d3c862480400094cf310fc81a214d3ed38e83d3 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 19 Jul 2026 19:11:55 +1200 Subject: [PATCH 2/5] Move error messages to HTTP2 error --- lib/protocol/http2/error.rb | 48 +++++++++++++++++++----------------- test/protocol/http2/error.rb | 12 +++++++++ 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/lib/protocol/http2/error.rb b/lib/protocol/http2/error.rb index 4f945ee..76d546b 100644 --- a/lib/protocol/http2/error.rb +++ b/lib/protocol/http2/error.rb @@ -50,6 +50,30 @@ class Error < HTTP::Error # The endpoint requires that HTTP/1.1 be used instead of HTTP/2. HTTP_1_1_REQUIRED = 0xD + + MESSAGES = { + NO_ERROR => "No error.", + PROTOCOL_ERROR => "Protocol error!", + INTERNAL_ERROR => "Internal error!", + FLOW_CONTROL_ERROR => "Flow control error!", + SETTINGS_TIMEOUT => "Settings timeout!", + STREAM_CLOSED => "Stream closed!", + FRAME_SIZE_ERROR => "Frame size error!", + REFUSED_STREAM => "Stream refused.", + CANCEL => "Stream cancelled.", + COMPRESSION_ERROR => "Compression error!", + CONNECT_ERROR => "Connect error!", + ENHANCE_YOUR_CALM => "Enhance your calm!", + INADEQUATE_SECURITY => "Inadequate security!", + HTTP_1_1_REQUIRED => "HTTP/1.1 required.", + } + + def self.message_for(code) + return MESSAGES.fetch(code) do + # Unknown error codes are allowed by the protocol, but don't have a static message. + "Unknown code: #{code}!" + end + end end # Raised if connection header is missing or invalid indicating that @@ -76,30 +100,8 @@ def initialize(message, code = PROTOCOL_ERROR) # Represents an error specific to stream operations. class StreamError < ProtocolError - MESSAGES = { - Error::NO_ERROR => "No error.", - Error::PROTOCOL_ERROR => "Protocol error!", - Error::INTERNAL_ERROR => "Internal error!", - Error::FLOW_CONTROL_ERROR => "Flow control error!", - Error::SETTINGS_TIMEOUT => "Settings timeout!", - Error::STREAM_CLOSED => "Stream closed!", - Error::FRAME_SIZE_ERROR => "Frame size error!", - Error::REFUSED_STREAM => "Stream refused.", - Error::CANCEL => "Stream cancelled.", - Error::COMPRESSION_ERROR => "Compression error!", - Error::CONNECT_ERROR => "Connect error!", - Error::ENHANCE_YOUR_CALM => "Enhance your calm!", - Error::INADEQUATE_SECURITY => "Inadequate security!", - Error::HTTP_1_1_REQUIRED => "HTTP/1.1 required.", - } - def self.for(code) - message = MESSAGES.fetch(code) do - # Unknown error codes are allowed by the protocol, but don't have a static message. - "Unknown code: #{code}!" - end - - return self.new(message, code) + return self.new(Error.message_for(code), code) end end diff --git a/test/protocol/http2/error.rb b/test/protocol/http2/error.rb index f7282d5..d833810 100644 --- a/test/protocol/http2/error.rb +++ b/test/protocol/http2/error.rb @@ -5,6 +5,18 @@ require "protocol/http2/error" +describe Protocol::HTTP2::Error do + with ".message_for" do + it "returns a static message for a known error code" do + expect(subject.message_for(subject::CANCEL)).to be == "Stream cancelled." + end + + it "returns a fallback message for an unknown error code" do + expect(subject.message_for(99)).to be == "Unknown code: 99!" + end + end +end + describe Protocol::HTTP2::StreamError do with ".for" do it "builds a stream error for a known error code" do From d3706990ca8fd988ceb153f9b0c5c59bcde7905e Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 19 Jul 2026 19:14:12 +1200 Subject: [PATCH 3/5] Add protocol error factory --- lib/protocol/http2/error.rb | 7 ++++--- test/protocol/http2/error.rb | 12 ++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/protocol/http2/error.rb b/lib/protocol/http2/error.rb index 76d546b..01ea9a8 100644 --- a/lib/protocol/http2/error.rb +++ b/lib/protocol/http2/error.rb @@ -86,6 +86,10 @@ class HandshakeError < Error # which signals termination of the current connection. You *cannot* # recover from this exception, or any exceptions subclassed from it. class ProtocolError < Error + def self.for(code) + return self.new(Error.message_for(code), code) + end + # Initialize a protocol error with message and error code. # @parameter message [String] The error message. # @parameter code [Integer] The HTTP/2 error code. @@ -100,9 +104,6 @@ def initialize(message, code = PROTOCOL_ERROR) # Represents an error specific to stream operations. class StreamError < ProtocolError - def self.for(code) - return self.new(Error.message_for(code), code) - end end # Represents an error for operations on closed streams. diff --git a/test/protocol/http2/error.rb b/test/protocol/http2/error.rb index d833810..1392eb2 100644 --- a/test/protocol/http2/error.rb +++ b/test/protocol/http2/error.rb @@ -17,6 +17,18 @@ end end +describe Protocol::HTTP2::ProtocolError do + with ".for" do + it "builds a protocol error for a known error code" do + error = subject.for(subject::PROTOCOL_ERROR) + + expect(error).to be_a(subject) + expect(error.message).to be == "Protocol error!" + expect(error.code).to be == subject::PROTOCOL_ERROR + end + end +end + describe Protocol::HTTP2::StreamError do with ".for" do it "builds a stream error for a known error code" do From 85be93a46786e65e8cf594c2cc578a046e05cb46 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 19 Jul 2026 19:15:17 +1200 Subject: [PATCH 4/5] Add stream error release note --- releases.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/releases.md b/releases.md index 5b79d33..5bbd93a 100644 --- a/releases.md +++ b/releases.md @@ -1,5 +1,9 @@ # Releases +## Unreleased + + - Improve `StreamError` messages for HTTP/2 stream reset error codes. + ## v0.26.0 - On RST\_STREAM with REFUSED\_STREAM, close the stream with `Protocol::HTTP::RefusedError` instead of `StreamError`. From 35fb346bfb7c9ee65971c023c9914501f087706c Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 19 Jul 2026 19:16:42 +1200 Subject: [PATCH 5/5] Document error factories --- lib/protocol/http2/error.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/protocol/http2/error.rb b/lib/protocol/http2/error.rb index 01ea9a8..492758d 100644 --- a/lib/protocol/http2/error.rb +++ b/lib/protocol/http2/error.rb @@ -68,6 +68,9 @@ class Error < HTTP::Error HTTP_1_1_REQUIRED => "HTTP/1.1 required.", } + # Get the message for a given HTTP/2 error code. + # @parameter code [Integer] The HTTP/2 error code. + # @returns [String] The error message. def self.message_for(code) return MESSAGES.fetch(code) do # Unknown error codes are allowed by the protocol, but don't have a static message. @@ -86,6 +89,9 @@ class HandshakeError < Error # which signals termination of the current connection. You *cannot* # recover from this exception, or any exceptions subclassed from it. class ProtocolError < Error + # Build a protocol error for a given HTTP/2 error code. + # @parameter code [Integer] The HTTP/2 error code. + # @returns [ProtocolError] The protocol error. def self.for(code) return self.new(Error.message_for(code), code) end