Skip to content

Commit

Permalink
Add Rack 3.0 support
Browse files Browse the repository at this point in the history
  • Loading branch information
marcotc committed Jun 7, 2024
1 parent 5f515ca commit ac0c865
Show file tree
Hide file tree
Showing 9 changed files with 321 additions and 173 deletions.
10 changes: 8 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-latest ]
# Due to https://github.com/actions/runner/issues/849, we have to use quotes for '3.0'
ruby: ['2.7', '3.0', '3.1', '3.2', head, truffleruby, truffleruby-head]
ruby: [
3.1,
3.2,
3.3,
head,
truffleruby,
truffleruby-head
]
gemfile: [ rack2, rack3 ]
runs-on: ${{ matrix.os }}
env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
Expand Down
2 changes: 1 addition & 1 deletion .ruby-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.2.0
3.1.2
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 2.0.0
## Changed
- Minimum supported version of Rack is 3.0.

## 1.1.0
## Changed
- Removed dependency on `git-version-bump` gem for versioning. `rack-brotli` now only depends on `rack` and `brotli`.
Expand Down
7 changes: 7 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
source 'https://rubygems.org'

gemspec

gem 'rack', '~> 3'

gem 'bundler'
gem 'minitest', '~> 5.6'
gem 'rake', '~> 12', '>= 12.3.3'
gem 'rdoc', '~> 3.12'
109 changes: 68 additions & 41 deletions lib/rack/brotli/deflater.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
require "brotli"
require 'rack/utils'
# frozen_string_literal: true

require 'brotli'
require "rack/utils"
require 'rack/request'
require 'rack/body_proxy'

module Rack::Brotli
# This middleware enables compression of http responses.
#
# Currently supported compression algorithms:
#
# * br
#
# The middleware automatically detects when compression is supported
# and allowed. For example no transformation is made when a cache
# directive of 'no-transform' is present, or when the response status
# code is one that doesn't allow an entity body.
# This middleware enables compression of http responses with the `br` encoding,
# when support is detected and allowed.
class Deflater
##
# Creates Rack::Brotli middleware.
Expand All @@ -21,80 +17,107 @@ class Deflater
# 'if' - a lambda enabling / disabling deflation based on returned boolean value
# e.g use Rack::Brotli, :if => lambda { |env, status, headers, body| body.map(&:bytesize).reduce(0, :+) > 512 }
# 'include' - a list of content types that should be compressed
# 'deflater' - Brotli compression options
# 'deflater' - Brotli compression options Hash (see https://brotli.org/encode.html#a4d4 and https://github.com/miyucy/brotli/blob/ea0e058031177e5cc42e361f7d2702a951048a31/ext/brotli/brotli.c#L119-L180)
# - 'mode'
# - 'quality'
# - 'lgwin'
# - 'lgblock'
def initialize(app, options = {})
@app = app

@condition = options[:if]
@compressible_types = options[:include]
@deflater_options = { quality: 5 }.merge(options[:deflater] || {})
@deflater_options = { quality: 5 }.merge(options.fetch(:deflater, {}))
@sync = options.fetch(:sync, true)
end

def call(env)
status, headers, body = @app.call(env)
headers = Rack::Utils::HeaderHash.new(headers)
status, headers, body = response = @app.call(env)

unless should_deflate?(env, status, headers, body)
return [status, headers, body]
return response
end

request = Rack::Request.new(env)

encoding = Rack::Utils.select_best_encoding(%w(br),
request.accept_encoding)

return [status, headers, body] unless encoding
encoding = Rack::Utils.select_best_encoding(%w(br identity), request.accept_encoding)

# Set the Vary HTTP header.
vary = headers["Vary"].to_s.split(",").map(&:strip)
unless vary.include?("*") || vary.include?("Accept-Encoding")
headers["Vary"] = vary.push("Accept-Encoding").join(",")
vary = headers["vary"].to_s.split(",").map(&:strip)
unless vary.include?("*") || vary.any?{|v| v.downcase == 'accept-encoding'}
headers["vary"] = vary.push("Accept-Encoding").join(",")
end

case encoding
when "br"
headers['Content-Encoding'] = "br"
headers['content-encoding'] = "br"
headers.delete(Rack::CONTENT_LENGTH)
[status, headers, BrotliStream.new(body, @deflater_options)]
when nil
response[2] = BrotliStream.new(body, @sync, @deflater_options)
response
when "identity"
response
else
# Only possible encoding values here are 'br', 'identity', and nil
message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
bp = Rack::BodyProxy.new([message]) { body.close if body.respond_to?(:close) }
[406, {Rack::CONTENT_TYPE => "text/plain", Rack::CONTENT_LENGTH => message.length.to_s}, bp]
[406, { Rack::CONTENT_TYPE => "text/plain", Rack::CONTENT_LENGTH => message.length.to_s }, bp]
end
end

# Body class used for encoded responses.
class BrotliStream
include Rack::Utils

def initialize(body, options)
BUFFER_LENGTH = 128 * 1_024

def initialize(body, sync, br_options)
@body = body
@options = options
@br_options = br_options
@sync = sync
end

# Yield compressed strings to the given block.
def each(&block)
@writer = block
buffer = ''
@body.each { |part|
buffer << part
}
yield ::Brotli.deflate(buffer, @options)
br = Brotli::Writer.new(self, @br_options)
# @body.each is equivalent to @body.gets (slow)
if @body.is_a? ::File # XXX: Should probably be ::IO
while part = @body.read(BUFFER_LENGTH)
br.write(part)
br.flush if @sync
end
else
@body.each { |part|
# Skip empty strings, as they would result in no output,
# and flushing empty parts could raise an IO error.
next if part.empty?
br.write(part)
br.flush if @sync
}
end
ensure
@writer = nil
br.finish
end

# Call the block passed to #each with the compressed data.
def write(data)
@writer.call(data)
end

# Close the original body if possible.
def close
@body.close if @body.respond_to?(:close)
end
end

private

# Whether the body should be compressed.
def should_deflate?(env, status, headers, body)
# Skip compressing empty entity body responses and responses with
# no-transform set.
if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
headers[Rack::CACHE_CONTROL].to_s =~ /\bno-transform\b/ ||
(headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/)
if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) ||
/\bno-transform\b/.match?(headers[Rack::CACHE_CONTROL].to_s) ||
headers['content-encoding']&.!~(/\bidentity\b/)
return false
end

Expand All @@ -104,6 +127,10 @@ def should_deflate?(env, status, headers, body)
# Skip if @condition lambda is given and evaluates to false
return false if @condition && !@condition.call(env, status, headers, body)

# No point in compressing empty body, also handles usage with
# Rack::Sendfile.
return false if headers[Rack::CONTENT_LENGTH] == '0'

true
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/rack/brotli/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module Rack
module Brotli
class Version
def self.to_s
'1.1.0'
'2.0.0'
end
end
end
Expand Down
9 changes: 2 additions & 7 deletions rack-brotli.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,8 @@ Gem::Specification.new do |s|

s.extra_rdoc_files = %w[README.md COPYING]

s.add_runtime_dependency 'rack', '>= 1.4', '< 3'
s.add_runtime_dependency 'brotli', '>= 0.1.7'

s.add_development_dependency 'bundler'
s.add_development_dependency 'minitest', '~> 5.6'
s.add_development_dependency 'rake', '~> 12', '>= 12.3.3'
s.add_development_dependency 'rdoc', '~> 3.12'
s.add_runtime_dependency 'rack', '>= 3'
s.add_runtime_dependency 'brotli', '>= 0.3'

s.homepage = "http://github.com/marcotc/rack-brotli/"
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "rack-brotli", "--main", "README"]
Expand Down
Loading

0 comments on commit ac0c865

Please sign in to comment.