diff --git a/.github/workflows/edge.yml b/.github/workflows/edge.yml index c617da14e1..cfa629b0b8 100644 --- a/.github/workflows/edge.yml +++ b/.github/workflows/edge.yml @@ -7,7 +7,12 @@ jobs: fail-fast: false matrix: ruby: ['2.7', '3.0', '3.1', '3.2', '3.3', ruby-head, truffleruby-head, jruby-head] - gemfile: [rails_edge, rack_edge, rack_3_0] + gemfile: [rails_edge, rack_edge] + exclude: + - ruby: '2.7' + gemfile: rails_edge + - ruby: '3.0' + gemfile: rails_edge runs-on: ubuntu-latest continue-on-error: true env: diff --git a/CHANGELOG.md b/CHANGELOG.md index d6ba50c99a..ac5cb93f87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ * [#2373](https://github.com/ruby-grape/grape/pull/2373): Fix markdown files for following 1-line format - [@jcagarcia](https://github.com/jcagarcia). * [#2382](https://github.com/ruby-grape/grape/pull/2382): Fix values validator for params wrapped in `with` block - [@numbata](https://github.com/numbata). * [#2387](https://github.com/ruby-grape/grape/pull/2387): Fix rubygems version within workflows - [@ericproulx](https://github.com/ericproulx). +* [#2405](https://github.com/ruby-grape/grape/pull/2405): Fix edge workflow - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 2.0.0 (2023/11/11) diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index dc31094afc..97c2dbff07 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -688,7 +688,7 @@ class DummyFormatClass 'example' end put '/example' - expect(last_response.headers[Rack::CONTENT_TYPE]).to eql 'text/plain' + expect(last_response.content_type).to eql 'text/plain' end describe 'adds an OPTIONS route that' do @@ -1195,7 +1195,7 @@ class DummyFormatClass it 'sets content type for txt format' do get '/foo' - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('text/plain') + expect(last_response.content_type).to eq('text/plain') end it 'does not set Cache-Control' do @@ -1205,22 +1205,22 @@ class DummyFormatClass it 'sets content type for xml' do get '/foo.xml' - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('application/xml') + expect(last_response.content_type).to eq('application/xml') end it 'sets content type for json' do get '/foo.json' - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('application/json') + expect(last_response.content_type).to eq('application/json') end it 'sets content type for serializable hash format' do get '/foo.serializable_hash' - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('application/json') + expect(last_response.content_type).to eq('application/json') end it 'sets content type for binary format' do get '/foo.binary' - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('application/octet-stream') + expect(last_response.content_type).to eq('application/octet-stream') end it 'returns raw data when content type binary' do @@ -1229,7 +1229,7 @@ class DummyFormatClass subject.format :binary subject.get('/binary_file') { File.binread(image_filename) } get '/binary_file' - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('application/octet-stream') + expect(last_response.content_type).to eq('application/octet-stream') expect(last_response.body).to eq(file) end @@ -1241,8 +1241,8 @@ class DummyFormatClass subject.get('/file') { stream test_file } get '/file' - expect(last_response.headers[Rack::CONTENT_LENGTH]).to eq('25') - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('text/plain') + expect(last_response.content_length).to eq(25) + expect(last_response.content_type).to eq('text/plain') expect(last_response.body).to eq(file_content) end @@ -1252,12 +1252,12 @@ class DummyFormatClass blk.yield ' file content' end - subject.use Rack::Chunked + subject.use Gem::Version.new(Rack.release) < Gem::Version.new('3') ? Rack::Chunked : ChunkedResponse subject.get('/stream') { stream test_stream } get '/stream', {}, 'HTTP_VERSION' => 'HTTP/1.1', 'SERVER_PROTOCOL' => 'HTTP/1.1' - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq('text/plain') - expect(last_response.headers[Rack::CONTENT_LENGTH]).to be_nil + expect(last_response.content_type).to eq('text/plain') + expect(last_response.content_length).to be_nil expect(last_response.headers[Rack::CACHE_CONTROL]).to eq('no-cache') expect(last_response.headers[Grape::Http::Headers::TRANSFER_ENCODING]).to eq('chunked') @@ -1267,7 +1267,7 @@ class DummyFormatClass it 'sets content type for error' do subject.get('/error') { error!('error in plain text', 500) } get '/error' - expect(last_response.headers[Rack::CONTENT_TYPE]).to eql 'text/plain' + expect(last_response.content_type).to eql 'text/plain' end it 'sets content type for json error' do @@ -1275,7 +1275,7 @@ class DummyFormatClass subject.get('/error') { error!('error in json', 500) } get '/error.json' expect(last_response.status).to be 500 - expect(last_response.headers[Rack::CONTENT_TYPE]).to eql 'application/json' + expect(last_response.content_type).to eql 'application/json' end it 'sets content type for xml error' do @@ -1283,7 +1283,7 @@ class DummyFormatClass subject.get('/error') { error!('error in xml', 500) } get '/error' expect(last_response.status).to be 500 - expect(last_response.headers[Rack::CONTENT_TYPE]).to eql 'application/xml' + expect(last_response.content_type).to eql 'application/xml' end it 'includes extension in format' do @@ -1313,12 +1313,12 @@ class DummyFormatClass it 'sets content type' do get '/custom.custom' - expect(last_response.headers[Rack::CONTENT_TYPE]).to eql 'application/custom' + expect(last_response.content_type).to eql 'application/custom' end it 'sets content type for error' do get '/error.custom' - expect(last_response.headers[Rack::CONTENT_TYPE]).to eql 'application/custom' + expect(last_response.content_type).to eql 'application/custom' end end @@ -1342,7 +1342,7 @@ class DummyFormatClass image_filename = 'grape.png' post url, file: Rack::Test::UploadedFile.new(image_filename, content_type, true) expect(last_response.status).to eq(201) - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq(content_type) + expect(last_response.content_type).to eq(content_type) expect(last_response.headers['Content-Disposition']).to eq("attachment; filename*=UTF-8''grape.png") File.open(image_filename, 'rb') do |io| expect(last_response.body).to eq io.read @@ -1358,7 +1358,7 @@ class DummyFormatClass filename = __FILE__ post '/attachment.rb', file: Rack::Test::UploadedFile.new(filename, content_type, true) expect(last_response.status).to eq(201) - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq(content_type) + expect(last_response.content_type).to eq(content_type) expect(last_response.headers['Content-Disposition']).to eq("attachment; filename*=UTF-8''api_spec.rb") File.open(filename, 'rb') do |io| expect(last_response.body).to eq io.read diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 94d0443c37..77dd116c00 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -490,7 +490,7 @@ def app end it 'responses with given content type in headers' do - expect(last_response.headers[Rack::CONTENT_TYPE]).to eq 'application/json; charset=utf-8' + expect(last_response.content_type).to eq 'application/json; charset=utf-8' end end @@ -650,7 +650,7 @@ def app end get '/hey' expect(last_response.status).to eq 302 - expect(last_response.headers['Location']).to eq '/ha' + expect(last_response.location).to eq '/ha' expect(last_response.body).to eq 'This resource has been moved temporarily to /ha.' end @@ -660,7 +660,7 @@ def app end post '/hey', {}, 'HTTP_VERSION' => 'HTTP/1.1' expect(last_response.status).to eq 303 - expect(last_response.headers['Location']).to eq '/ha' + expect(last_response.location).to eq '/ha' expect(last_response.body).to eq 'An alternate resource is located at /ha.' end @@ -670,7 +670,7 @@ def app end get '/hey' expect(last_response.status).to eq 301 - expect(last_response.headers['Location']).to eq '/ha' + expect(last_response.location).to eq '/ha' expect(last_response.body).to eq 'This resource has been moved permanently to /ha.' end diff --git a/spec/grape/entity_spec.rb b/spec/grape/entity_spec.rb index 425edf0031..7eaa0fae87 100644 --- a/spec/grape/entity_spec.rb +++ b/spec/grape/entity_spec.rb @@ -236,7 +236,7 @@ def initialize(args) end get '/example' expect(last_response.status).to eq(200) - expect(last_response.headers['Content-type']).to eq('application/xml') + expect(last_response.content_type).to eq('application/xml') expect(last_response.body).to eq <<~XML @@ -266,7 +266,7 @@ def initialize(args) end get '/example' expect(last_response.status).to eq(200) - expect(last_response.headers['Content-type']).to eq('application/json') + expect(last_response.content_type).to eq('application/json') expect(last_response.body).to eq('{"example":{"name":"johnnyiller"}}') end @@ -298,7 +298,7 @@ def initialize(args) get '/example?callback=abcDef' expect(last_response.status).to eq(200) - expect(last_response.headers['Content-type']).to eq('application/javascript') + expect(last_response.content_type).to eq('application/javascript') expect(last_response.body).to include 'abcDef({"example":{"name":"johnnyiller"}})' end diff --git a/spec/grape/middleware/base_spec.rb b/spec/grape/middleware/base_spec.rb index cbba974e1a..3be95be1f5 100644 --- a/spec/grape/middleware/base_spec.rb +++ b/spec/grape/middleware/base_spec.rb @@ -93,7 +93,7 @@ end it 'header' do - expect(subject.response.header).to have_key(:abc) + expect(subject.response.headers).to have_key(:abc) end it 'returns the memoized Rack::Response instance' do @@ -115,7 +115,7 @@ end it 'header' do - expect(subject.response.header).to have_key(:abc) + expect(subject.response.headers).to have_key(:abc) end it 'returns the memoized Rack::Response instance' do diff --git a/spec/support/basic_auth_encode_helpers.rb b/spec/support/basic_auth_encode_helpers.rb index 78e21e6c80..00c3c61497 100644 --- a/spec/support/basic_auth_encode_helpers.rb +++ b/spec/support/basic_auth_encode_helpers.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'base64' + module Spec module Support module Helpers diff --git a/spec/support/chunked_response.rb b/spec/support/chunked_response.rb new file mode 100644 index 0000000000..41defe87e6 --- /dev/null +++ b/spec/support/chunked_response.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +# this is a copy of Rack::Chunked which has been removed in rack > 3.0 + +class ChunkedResponse + class Body + TERM = "\r\n" + TAIL = "0#{TERM}" + + # Store the response body to be chunked. + def initialize(body) + @body = body + end + + # For each element yielded by the response body, yield + # the element in chunked encoding. + def each(&block) + term = TERM + @body.each do |chunk| + size = chunk.bytesize + next if size == 0 + + yield [size.to_s(16), term, chunk.b, term].join + end + yield TAIL + yield_trailers(&block) + yield term + end + + # Close the response body if the response body supports it. + def close + @body.close if @body.respond_to?(:close) + end + + private + + # Do nothing as this class does not support trailer headers. + def yield_trailers; end + end + + class TrailerBody < Body + private + + # Yield strings for each trailer header. + def yield_trailers + @body.trailers.each_pair do |k, v| + yield "#{k}: #{v}\r\n" + end + end + end + + def initialize(app) + @app = app + end + + def call(env) + status, headers, body = response = @app.call(env) + + if !Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) && + !headers[Rack::CONTENT_LENGTH] && + !headers[Rack::TRANSFER_ENCODING] + + headers[Rack::TRANSFER_ENCODING] = 'chunked' + response[2] = if headers['trailer'] + TrailerBody.new(body) + else + Body.new(body) + end + end + + response + end +end