diff --git a/lib/protocol/http/error.rb b/lib/protocol/http/error.rb index 69dc2c6..5e5911f 100644 --- a/lib/protocol/http/error.rb +++ b/lib/protocol/http/error.rb @@ -22,15 +22,17 @@ class RemoteError < Error # @deprecated Use {RefusedError} instead. RequestRefusedError = RefusedError - # Represents a bad request error (as opposed to a server error). - # This is used to indicate that the request was malformed or invalid. + # Marks errors which may indicate a malformed or invalid request when raised while processing an incoming request. module BadRequest end - # Raised when a singleton (e.g. `content-length`) header is duplicated in a request or response. - class DuplicateHeaderError < Error + # Raised when an HTTP header is malformed or invalid. When raised while processing an incoming request, it may be treated as a bad request. + class InvalidHeaderError < Error include BadRequest - + end + + # Raised when a singleton (e.g. `content-length`) header is duplicated in a request or response. + class DuplicateHeaderError < InvalidHeaderError # @parameter key [String] The header key that was duplicated. def initialize(key, existing_value, new_value) super("Duplicate singleton header key: #{key.inspect}") @@ -62,9 +64,7 @@ def detailed_message(highlight: false) end # Raised when an invalid trailer header is encountered in headers. - class InvalidTrailerError < Error - include BadRequest - + class InvalidTrailerError < InvalidHeaderError # @parameter key [String] The trailer key that is invalid. def initialize(key) super("Invalid trailer key: #{key.inspect}") diff --git a/lib/protocol/http/header/accept.rb b/lib/protocol/http/header/accept.rb index 358a27b..eb3dd8c 100644 --- a/lib/protocol/http/header/accept.rb +++ b/lib/protocol/http/header/accept.rb @@ -23,7 +23,7 @@ class Accept < Split (?=,|\z) # Match until a comma or end of string /x - ParseError = Class.new(Error) + ParseError = Class.new(InvalidHeaderError) MEDIA_RANGE = /\A(?#{TOKEN})\/(?#{TOKEN})(?.*)\z/ diff --git a/lib/protocol/http/header/accept_charset.rb b/lib/protocol/http/header/accept_charset.rb index 890d719..ac9eeb3 100644 --- a/lib/protocol/http/header/accept_charset.rb +++ b/lib/protocol/http/header/accept_charset.rb @@ -12,7 +12,7 @@ module HTTP module Header # The `accept-charset` header represents a list of character sets that the client can accept. class AcceptCharset < Split - ParseError = Class.new(Error) + ParseError = Class.new(InvalidHeaderError) # https://tools.ietf.org/html/rfc7231#section-5.3.3 CHARSET = /\A(?#{TOKEN})(;q=(?#{QVALUE}))?\z/ diff --git a/lib/protocol/http/header/accept_encoding.rb b/lib/protocol/http/header/accept_encoding.rb index d9fa98f..32e21c1 100644 --- a/lib/protocol/http/header/accept_encoding.rb +++ b/lib/protocol/http/header/accept_encoding.rb @@ -12,7 +12,7 @@ module HTTP module Header # The `accept-encoding` header represents a list of encodings that the client can accept. class AcceptEncoding < Split - ParseError = Class.new(Error) + ParseError = Class.new(InvalidHeaderError) # https://tools.ietf.org/html/rfc7231#section-5.3.1 QVALUE = /0(\.[0-9]{0,3})?|1(\.[0]{0,3})?/ diff --git a/lib/protocol/http/header/accept_language.rb b/lib/protocol/http/header/accept_language.rb index ee47bfd..2767bc6 100644 --- a/lib/protocol/http/header/accept_language.rb +++ b/lib/protocol/http/header/accept_language.rb @@ -12,7 +12,7 @@ module HTTP module Header # The `accept-language` header represents a list of languages that the client can accept. class AcceptLanguage < Split - ParseError = Class.new(Error) + ParseError = Class.new(InvalidHeaderError) # https://tools.ietf.org/html/rfc3066#section-2.1 NAME = /\*|[A-Z]{1,8}(-[A-Z0-9]{1,8})*/i diff --git a/lib/protocol/http/header/digest.rb b/lib/protocol/http/header/digest.rb index 7a6b63d..10851e9 100644 --- a/lib/protocol/http/header/digest.rb +++ b/lib/protocol/http/header/digest.rb @@ -23,7 +23,7 @@ module Header # # => "sha-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=, md5=9bb58f26192e4ba00f01e2e7b136bbd8" # ``` class Digest < Split - ParseError = Class.new(Error) + ParseError = Class.new(InvalidHeaderError) # https://tools.ietf.org/html/rfc3230#section-4.3.2 ENTRY = /\A(?[a-zA-Z0-9][a-zA-Z0-9\-]*)\s*=\s*(?.*)\z/ diff --git a/lib/protocol/http/header/range.rb b/lib/protocol/http/header/range.rb index c017668..7fa3e58 100644 --- a/lib/protocol/http/header/range.rb +++ b/lib/protocol/http/header/range.rb @@ -10,7 +10,7 @@ module HTTP module Header # Represents a `range` request header. class Range - ParseError = Class.new(Error) + ParseError = Class.new(InvalidHeaderError) TOKEN = /[!#$%&'*+\-.0-9A-Z^_`a-z|~]+/ HEADER = /\A(?#{TOKEN})=(?.*)\z/ diff --git a/lib/protocol/http/header/server_timing.rb b/lib/protocol/http/header/server_timing.rb index 74444aa..a5e5c4f 100644 --- a/lib/protocol/http/header/server_timing.rb +++ b/lib/protocol/http/header/server_timing.rb @@ -23,7 +23,7 @@ module Header # # => "db;dur=53.2, cache;dur=12.1;desc=\"Redis lookup\"" # ``` class ServerTiming < Split - ParseError = Class.new(Error) + ParseError = Class.new(InvalidHeaderError) # https://www.w3.org/TR/server-timing/ METRIC = /\A(?[a-zA-Z0-9][a-zA-Z0-9_\-]*)(;(?.*))?\z/ diff --git a/lib/protocol/http/header/te.rb b/lib/protocol/http/header/te.rb index 17c36a3..552d6f3 100644 --- a/lib/protocol/http/header/te.rb +++ b/lib/protocol/http/header/te.rb @@ -14,7 +14,7 @@ module Header # # The `te` header allows a client to indicate which transfer encodings it can handle, and in what order of preference using quality factors. class TE < Split - ParseError = Class.new(Error) + ParseError = Class.new(InvalidHeaderError) # Transfer encoding token pattern TOKEN = /[!#$%&'*+\-.0-9A-Z^_`a-z|~]+/ diff --git a/readme.md b/readme.md index a91bd86..7ca6c33 100644 --- a/readme.md +++ b/readme.md @@ -30,6 +30,10 @@ Please see the [project documentation](https://socketry.github.io/protocol-http/ Please see the [project releases](https://socketry.github.io/protocol-http/releases/index) for all releases. +### Unreleased + + - Introduce `Protocol::HTTP::InvalidHeaderError` for malformed or invalid headers, which can be treated as bad requests. + ### v0.71.0 - Parse all cookie pairs from `Cookie` header fields, including multiple semicolon-separated pairs within each field. diff --git a/releases.md b/releases.md index d9e5b16..fd69e31 100644 --- a/releases.md +++ b/releases.md @@ -3,6 +3,7 @@ ## Unreleased - Clarified body stream lifecycle and ownership, including the directional semantics of `Protocol::HTTP::Body::Stream#close_read`, `#close_write`, and `#close`, how premature input closure affects the associated HTTP exchange, and ownership of streams passed to `Streamable#call`. + - Introduce `Protocol::HTTP::InvalidHeaderError` for malformed or invalid headers, which can be treated as bad requests. ## v0.71.0 diff --git a/test/protocol/http/error.rb b/test/protocol/http/error.rb index f4bcd19..a5929a8 100644 --- a/test/protocol/http/error.rb +++ b/test/protocol/http/error.rb @@ -37,6 +37,18 @@ end end +describe Protocol::HTTP::InvalidHeaderError do + let(:error) {subject.new("Invalid header.")} + + it "is an HTTP error" do + expect(error).to be_a(Protocol::HTTP::Error) + end + + it "can be treated as a bad request" do + expect(error).to be_a(Protocol::HTTP::BadRequest) + end +end + describe Protocol::HTTP::DuplicateHeaderError do let(:key) {"content-length"} let(:existing_value) {"100"} @@ -44,6 +56,10 @@ let(:error) {subject.new(key, existing_value, new_value)} with "#initialize" do + it "is an invalid header error" do + expect(error).to be_a(Protocol::HTTP::InvalidHeaderError) + end + it "should set the key and values" do expect(error.key).to be == key expect(error.existing_value).to be == existing_value @@ -73,3 +89,11 @@ end end end + +describe Protocol::HTTP::InvalidTrailerError do + let(:error) {subject.new("content-length")} + + it "is an invalid header error" do + expect(error).to be_a(Protocol::HTTP::InvalidHeaderError) + end +end diff --git a/test/protocol/http/header/accept.rb b/test/protocol/http/header/accept.rb index d650b9f..5b6c8c7 100644 --- a/test/protocol/http/header/accept.rb +++ b/test/protocol/http/header/accept.rb @@ -26,6 +26,10 @@ end describe Protocol::HTTP::Header::Accept do + it "classifies parse errors as invalid headers" do + expect(subject::ParseError.new).to be_a(Protocol::HTTP::InvalidHeaderError) + end + let(:header) {subject.parse(description)} let(:media_ranges) {header.preferred_media_ranges} diff --git a/test/protocol/http/header/accept_charset.rb b/test/protocol/http/header/accept_charset.rb index b89d8e1..64738c1 100644 --- a/test/protocol/http/header/accept_charset.rb +++ b/test/protocol/http/header/accept_charset.rb @@ -19,6 +19,10 @@ end describe Protocol::HTTP::Header::AcceptCharset do + it "classifies parse errors as invalid headers" do + expect(subject::ParseError.new).to be_a(Protocol::HTTP::InvalidHeaderError) + end + let(:header) {subject.parse(description)} let(:charsets) {header.preferred_charsets} diff --git a/test/protocol/http/header/accept_encoding.rb b/test/protocol/http/header/accept_encoding.rb index 836e569..85096be 100644 --- a/test/protocol/http/header/accept_encoding.rb +++ b/test/protocol/http/header/accept_encoding.rb @@ -19,6 +19,10 @@ end describe Protocol::HTTP::Header::AcceptEncoding do + it "classifies parse errors as invalid headers" do + expect(subject::ParseError.new).to be_a(Protocol::HTTP::InvalidHeaderError) + end + let(:header) {subject.parse(description)} let(:encodings) {header.preferred_encodings} diff --git a/test/protocol/http/header/accept_language.rb b/test/protocol/http/header/accept_language.rb index 6bb3e11..183befd 100644 --- a/test/protocol/http/header/accept_language.rb +++ b/test/protocol/http/header/accept_language.rb @@ -19,6 +19,10 @@ end describe Protocol::HTTP::Header::AcceptLanguage do + it "classifies parse errors as invalid headers" do + expect(subject::ParseError.new).to be_a(Protocol::HTTP::InvalidHeaderError) + end + let(:header) {subject.parse(description)} let(:languages) {header.preferred_languages} diff --git a/test/protocol/http/header/digest.rb b/test/protocol/http/header/digest.rb index 1ebef76..ad55c7d 100644 --- a/test/protocol/http/header/digest.rb +++ b/test/protocol/http/header/digest.rb @@ -7,6 +7,10 @@ require "sus" describe Protocol::HTTP::Header::Digest do + it "classifies parse errors as invalid headers" do + expect(subject::ParseError.new).to be_a(Protocol::HTTP::InvalidHeaderError) + end + let(:header) {subject.parse(description)} with "empty header" do diff --git a/test/protocol/http/header/range.rb b/test/protocol/http/header/range.rb index 4ec8220..b36973e 100644 --- a/test/protocol/http/header/range.rb +++ b/test/protocol/http/header/range.rb @@ -6,6 +6,10 @@ require "protocol/http/header/range" describe Protocol::HTTP::Header::Range do + it "classifies parse errors as invalid headers" do + expect(subject::ParseError.new).to be_a(Protocol::HTTP::InvalidHeaderError) + end + with ".parse" do it "parses byte ranges" do header = subject.parse("bytes=0-4, 10-, -5") diff --git a/test/protocol/http/header/server_timing.rb b/test/protocol/http/header/server_timing.rb index 6aa336b..fd5d0d1 100644 --- a/test/protocol/http/header/server_timing.rb +++ b/test/protocol/http/header/server_timing.rb @@ -7,6 +7,10 @@ require "sus" describe Protocol::HTTP::Header::ServerTiming do + it "classifies parse errors as invalid headers" do + expect(subject::ParseError.new).to be_a(Protocol::HTTP::InvalidHeaderError) + end + let(:header) {subject.parse(description)} with "empty header" do diff --git a/test/protocol/http/header/te.rb b/test/protocol/http/header/te.rb index 5dde573..3c5303c 100644 --- a/test/protocol/http/header/te.rb +++ b/test/protocol/http/header/te.rb @@ -6,6 +6,10 @@ require "protocol/http/header/te" describe Protocol::HTTP::Header::TE do + it "classifies parse errors as invalid headers" do + expect(subject::ParseError.new).to be_a(Protocol::HTTP::InvalidHeaderError) + end + let(:header) {subject.parse(description)} with "chunked" do