Skip to content

Commit 3bace72

Browse files
authored
Merge pull request #652 from adrianodennanni/main
Add Zstd support (optional dependency, just like Brotli)
2 parents 752e4d2 + 1956295 commit 3bace72

File tree

3 files changed

+79
-2
lines changed

3 files changed

+79
-2
lines changed

Gemfile

+4-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@ gem "minitest", "~> 5.14"
66
gem "rake", "~> 13.0"
77
gem "rdoc", "~> 6.3"
88
gem "rubocop", "~> 1.12"
9-
gem "brotli", ">= 0.5" unless RUBY_PLATFORM == "java"
9+
unless RUBY_PLATFORM == 'java'
10+
gem 'brotli', '>= 0.5'
11+
gem 'zstd-ruby', '~> 1.5'
12+
end

lib/mechanize/http/agent.rb

+31
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,35 @@ def content_encoding_brotli(body_io)
523523
body_io.close
524524
end
525525

526+
##
527+
# Decodes a Zstd-encoded +body_io+
528+
#
529+
# (Experimental, CRuby only) Although Mechanize will never request a zstd-encoded response via
530+
# `accept-encoding`, buggy servers may return zstd-encoded responses, or you might need to
531+
# inform the zstd keyword on your Accept-Encoding headers. Let's try to handle those cases if
532+
# the Zstd gem is loaded.
533+
#
534+
# If you need to handle Zstd-encoded responses, install the 'zstd-ruby' gem and require it in your
535+
# application. If the `Zstd` constant is defined, Mechanize will attempt to use it to inflate
536+
# the response.
537+
#
538+
def content_encoding_zstd(body_io)
539+
log.debug('deflate zstd body') if log
540+
541+
unless defined?(::Zstd)
542+
raise Mechanize::Error, "cannot deflate zstd-encoded response. Please install and require the 'zstd-ruby' gem."
543+
end
544+
545+
begin
546+
return StringIO.new(Zstd.decompress(body_io.read))
547+
rescue StandardError
548+
log.error("unable to zstd#decompress response") if log
549+
raise Mechanize::Error, "error decompressing zstd-encoded response."
550+
end
551+
ensure
552+
body_io.close
553+
end
554+
526555
def disable_keep_alive request
527556
request['connection'] = 'close' unless @keep_alive
528557
end
@@ -861,6 +890,8 @@ def response_content_encoding response, body_io
861890
content_encoding_gunzip body_io
862891
when 'br' then
863892
content_encoding_brotli body_io
893+
when 'zstd' then
894+
content_encoding_zstd body_io
864895
else
865896
raise Mechanize::Error,
866897
"unsupported content-encoding: #{response['Content-Encoding']}"

test/test_mechanize_http_agent.rb

+44-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
# frozen_string_literal: true
33

44
require 'mechanize/test_case'
5-
require "brotli" unless RUBY_PLATFORM == "java"
5+
unless RUBY_PLATFORM == 'java'
6+
require 'brotli'
7+
require 'zstd-ruby'
8+
end
69

710
class TestMechanizeHttpAgent < Mechanize::TestCase
811

@@ -965,6 +968,46 @@ def test_response_content_encoding_brotli_corrupt
965968
assert(body_io.closed?)
966969
end
967970

971+
def test_response_content_encoding_zstd_when_zstd_not_loaded
972+
skip("only test this on jruby which doesn't have zstd support") unless RUBY_ENGINE == 'jruby'
973+
974+
@res.instance_variable_set :@header, 'content-encoding' => %w[zstd]
975+
body_io = StringIO.new("content doesn't matter for this test")
976+
977+
e = assert_raises(Mechanize::Error) do
978+
@agent.response_content_encoding(@res, body_io)
979+
end
980+
assert_includes(e.message, 'cannot deflate zstd-encoded response')
981+
982+
assert(body_io.closed?)
983+
end
984+
985+
def test_response_content_encoding_zstd
986+
skip('jruby does not have zstd support') if RUBY_ENGINE == 'jruby'
987+
988+
@res.instance_variable_set :@header, 'content-encoding' => %w[zstd]
989+
body_io = StringIO.new(Zstd.compress('this is compressed by zstd'))
990+
991+
body = @agent.response_content_encoding(@res, body_io)
992+
993+
assert_equal('this is compressed by zstd', body.read)
994+
assert(body_io.closed?)
995+
end
996+
997+
def test_response_content_encoding_zstd_corrupt
998+
skip('jruby does not have zstd support') if RUBY_ENGINE == 'jruby'
999+
1000+
@res.instance_variable_set :@header, 'content-encoding' => %w[zstd]
1001+
body_io = StringIO.new('not a zstd payload')
1002+
1003+
e = assert_raises(Mechanize::Error) do
1004+
@agent.response_content_encoding(@res, body_io)
1005+
end
1006+
assert_includes(e.message, 'error decompressing zstd-encoded response')
1007+
assert_kind_of(RuntimeError, e.cause)
1008+
assert(body_io.closed?)
1009+
end
1010+
9681011
def test_response_content_encoding_gzip_corrupt
9691012
log = StringIO.new
9701013
logger = Logger.new log

0 commit comments

Comments
 (0)