Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/protocol/multipart.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# Copyright, 2025, by Samuel Williams.

require_relative "multipart/version"
require_relative "multipart/error"
require_relative "multipart/byte_limit"
require_relative "multipart/headers"
require_relative "multipart/parser"
Expand Down
4 changes: 3 additions & 1 deletion lib/protocol/multipart/byte_limit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
# Released under the MIT License.
# Copyright, 2026, by Samuel Williams.

require_relative "error"

module Protocol
module Multipart
# Tracks consumed bytes against an optional maximum.
Expand Down Expand Up @@ -30,7 +32,7 @@ def consume(size)
@size += size

if @maximum and @size > @maximum
raise RangeError, "Multipart #{@name} exceeded limit of #{@maximum}!"
raise LimitError, "Multipart #{@name} exceeded limit of #{@maximum}!"
end

return @size
Expand Down
12 changes: 12 additions & 0 deletions lib/protocol/multipart/error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2026, by Samuel Williams.

module Protocol
module Multipart
# Raised when multipart processing exceeds a configured limit.
class LimitError < StandardError
end
end
end
3 changes: 2 additions & 1 deletion lib/protocol/multipart/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# Copyright, 2025, by Samuel Williams.

require "io/stream"
require_relative "error"
require_relative "headers"

module Protocol
Expand Down Expand Up @@ -245,7 +246,7 @@ def read_line(size, limit, allowance: 0, chomp:)

def check_limit(name, value, limit)
if limit and value > limit
raise RangeError, "Multipart #{name} exceeded limit of #{limit}!"
raise LimitError, "Multipart #{name} exceeded limit of #{limit}!"
end
end

Expand Down
2 changes: 1 addition & 1 deletion protocol-multipart.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ Gem::Specification.new do |spec|

spec.add_dependency "io-stream", "~> 0.8"
spec.add_dependency "protocol-http", "~> 0.67"
spec.add_dependency "protocol-url", "~> 0.5"
spec.add_dependency "protocol-url", "~> 0.9"
end
4 changes: 4 additions & 0 deletions releases.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Releases

## Unreleased

- Add `Protocol::Multipart::LimitError` for configured processing limits.

## v0.4.0

- Use consistent limit naming for multipart parser constraints.
Expand Down
2 changes: 1 addition & 1 deletion test/protocol/multipart/byte_limit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
it "raises when the maximum is exceeded" do
limit = subject.new(1, name: :field_size)

expect{limit.consume(2)}.to raise_exception(RangeError, message: be =~ /field_size exceeded limit of 1/)
expect{limit.consume(2)}.to raise_exception(Protocol::Multipart::LimitError, message: be =~ /field_size exceeded limit of 1/)
end

it "can be unlimited" do
Expand Down
10 changes: 5 additions & 5 deletions test/protocol/multipart/form_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ def to_h

expect do
parse_field("12345", field_size_limit: 4)
end.to raise_exception(RangeError, message: be =~ /field_size exceeded limit of 4/)
end.to raise_exception(Protocol::Multipart::LimitError, message: be =~ /field_size exceeded limit of 4/)
end

it "applies the upload size limit at its boundary" do
Expand All @@ -213,7 +213,7 @@ def to_h

expect do
parse_upload("12345", upload_size_limit: 4)
end.to raise_exception(RangeError, message: be =~ /upload_size exceeded limit of 4/)
end.to raise_exception(Protocol::Multipart::LimitError, message: be =~ /upload_size exceeded limit of 4/)
end

it "applies the upload size limit while discarding unread content" do
Expand All @@ -224,7 +224,7 @@ def to_h

expect do
subject::Parser.new(upload_size_limit: 3).each(StringIO.new(serialize(form_data)), boundary: form_data.boundary).to_a
end.to raise_exception(RangeError, message: be =~ /upload_size exceeded/)
end.to raise_exception(Protocol::Multipart::LimitError, message: be =~ /upload_size exceeded/)
end

it "applies the total size limit at its boundary" do
Expand All @@ -233,7 +233,7 @@ def to_h

expect do
parse_fields({"first" => "12", "second" => "345"}, total_size_limit: 4)
end.to raise_exception(RangeError, message: be =~ /total_size exceeded limit of 4/)
end.to raise_exception(Protocol::Multipart::LimitError, message: be =~ /total_size exceeded limit of 4/)
end

it "allows content limits to be disabled" do
Expand Down Expand Up @@ -267,7 +267,7 @@ def to_h

expect do
parse_field("1", name: "a[b][c]", depth_limit: 2)
end.to raise_exception(RangeError, message: be =~ /depth exceeded limit of 2/)
end.to raise_exception(Protocol::URL::LimitError, message: be =~ /depth exceeded limit of 2/)
end

with "invalid form metadata" do
Expand Down
12 changes: 6 additions & 6 deletions test/protocol/multipart/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,13 @@
data = "preamble\r\n--#{boundary}\r\n\r\n--#{boundary}--\r\n"
parser = Protocol::Multipart::Parser.new(StringIO.new(data), boundary, preamble_size_limit: 4)

expect{parser.each.to_a}.to raise_exception(RangeError, message: be =~ /preamble_size exceeded/)
expect{parser.each.to_a}.to raise_exception(Protocol::Multipart::LimitError, message: be =~ /preamble_size exceeded/)
end

it "limits an unterminated preamble" do
parser = Protocol::Multipart::Parser.new(StringIO.new("x" * 1024), boundary, preamble_size_limit: 16)

expect{parser.each.to_a}.to raise_exception(RangeError, message: be =~ /preamble_size exceeded/)
expect{parser.each.to_a}.to raise_exception(Protocol::Multipart::LimitError, message: be =~ /preamble_size exceeded/)
end

it "allows a boundary after the maximum preamble size" do
Expand All @@ -76,14 +76,14 @@
data = "--#{boundary}\r\nContent-Type: text/plain\r\n\r\nvalue\r\n--#{boundary}--\r\n"
parser = Protocol::Multipart::Parser.new(StringIO.new(data), boundary, header_size_limit: 16)

expect{parser.each.to_a}.to raise_exception(RangeError, message: be =~ /header_size exceeded/)
expect{parser.each.to_a}.to raise_exception(Protocol::Multipart::LimitError, message: be =~ /header_size exceeded/)
end

it "limits an unterminated header" do
data = "--#{boundary}\r\nX-Test: #{'x' * 1024}"
parser = Protocol::Multipart::Parser.new(StringIO.new(data), boundary, header_size_limit: 16)

expect{parser.each.to_a}.to raise_exception(RangeError, message: be =~ /header_size exceeded/)
expect{parser.each.to_a}.to raise_exception(Protocol::Multipart::LimitError, message: be =~ /header_size exceeded/)
end

it "allows the header terminator after the maximum header size" do
Expand All @@ -97,14 +97,14 @@
data = "--#{boundary}\r\nContent-Type: text/plain\r\nX-Test: true\r\n\r\nvalue\r\n--#{boundary}--\r\n"
parser = Protocol::Multipart::Parser.new(StringIO.new(data), boundary, header_count_limit: 1)

expect{parser.each.to_a}.to raise_exception(RangeError, message: be =~ /header_count exceeded/)
expect{parser.each.to_a}.to raise_exception(Protocol::Multipart::LimitError, message: be =~ /header_count exceeded/)
end

it "limits the part count" do
data = "--#{boundary}\r\n\r\none\r\n--#{boundary}\r\n\r\ntwo\r\n--#{boundary}--\r\n"
parser = Protocol::Multipart::Parser.new(StringIO.new(data), boundary, part_count_limit: 1)

expect{parser.each.to_a}.to raise_exception(RangeError, message: be =~ /part_count exceeded/)
expect{parser.each.to_a}.to raise_exception(Protocol::Multipart::LimitError, message: be =~ /part_count exceeded/)
end

it "allows limits to be disabled" do
Expand Down
Loading