Skip to content

Commit 2f62365

Browse files
authored
Set response headers based on Rack version (#2355)
* Set response headers based on Rack version * Use Rack.release instead of Rack::RELEASE * Bumped to 1.9.0 and updated UPGRADING.md * Added .rack3? method * Removed headers_helper * Updated README and UPGRADING
1 parent 51b081c commit 2f62365

21 files changed

+187
-82
lines changed

.github/workflows/test.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,16 @@ jobs:
6363
if: ${{ matrix.gemfile == 'multi_xml' }}
6464
run: bundle exec rspec spec/integration/multi_xml
6565

66+
- name: Run tests (spec/integration/rack/v2)
67+
# rack_2_0.gemfile is equals to Gemfile
68+
if: ${{ matrix.gemfile == 'rack_2_0' }}
69+
run: bundle exec rspec spec/integration/rack/v2
70+
71+
- name: Run tests (spec/integration/rack/v3)
72+
# rack_2_0.gemfile is equals to Gemfile
73+
if: ${{ matrix.gemfile == 'rack_3_0' }}
74+
run: bundle exec rspec spec/integration/rack/v3
75+
6676
- name: Coveralls
6777
uses: coverallsapp/github-action@master
6878
with:

.rubocop_todo.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,8 @@ RSpec/FilePath:
272272
- 'spec/integration/eager_load/eager_load_spec.rb'
273273
- 'spec/integration/multi_json/json_spec.rb'
274274
- 'spec/integration/multi_xml/xml_spec.rb'
275+
- 'spec/integration/rack/v2/headers_spec.rb'
276+
- 'spec/integration/rack/v3/headers_spec.rb'
275277

276278
# Offense count: 12
277279
# Configuration parameters: Max.

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
### 1.8.1 (Next)
1+
### 1.9.0 (Next)
22

33
#### Features
44

55
* [#2353](https://github.com/ruby-grape/grape/pull/2353): Added Rails 7.1 support - [@ericproulx](https://github.com/ericproulx).
6+
* [#2355](https://github.com/ruby-grape/grape/pull/2355): Set response headers based on Rack version - [@schinery](https://github.com/schinery).
67
* [#2360](https://github.com/ruby-grape/grape/pull/2360): Reduce gem size by removing specs - [@ericproulx](https://github.com/ericproulx).
78
* Your contribution here.
8-
9+
910
#### Fixes
1011

1112
* Your contribution here.

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ content negotiation, versioning and much more.
160160

161161
## Stable Release
162162

163-
You're reading the documentation for the next release of Grape, which should be **1.8.1**.
163+
You're reading the documentation for the next release of Grape, which should be **1.9.0**.
164164
Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version.
165165
The current stable release is [1.8.0](https://github.com/ruby-grape/grape/blob/v1.8.0/README.md).
166166

@@ -2130,8 +2130,9 @@ curl -H "secret_PassWord: swordfish" ...
21302130

21312131
The header name will have been normalized for you.
21322132

2133-
- In the `header` helper names will be coerced into a capitalized kebab case.
2134-
- In the `env` collection they appear in all uppercase, in snake case, and prefixed with 'HTTP_'.
2133+
- In the `header` helper names will be coerced into a downcased kebab case as `secret-password` if using Rack 3.
2134+
- In the `header` helper names will be coerced into a capitalized kebab case as `Secret-PassWord` if using Rack < 3.
2135+
- In the `env` collection they appear in all uppercase, in snake case, and prefixed with 'HTTP_' as `HTTP_SECRET_PASSWORD`
21352136

21362137
The header name will have been normalized per HTTP standards defined in [RFC2616 Section 4.2](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2) regardless of what is being sent by a client.
21372138

UPGRADING.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,35 @@
11
Upgrading Grape
22
===============
33

4+
### Upgrading to >= 1.9.0
5+
6+
#### Headers
7+
8+
As per [rack/rack#1592](https://github.com/rack/rack/issues/1592) Rack 3.0 is enforcing the HTTP/2 semantics, and thus treats all headers as lowercase. Starting with Grape 1.9.0, headers will be cased based on what version of Rack you are using.
9+
10+
Given this request:
11+
12+
```shell
13+
curl -H "Content-Type: application/json" -H "Secret-Password: foo" ...
14+
```
15+
16+
If you are using Rack 3 in your application then the headers will be set to:
17+
18+
```ruby
19+
{ "content-type" => "application/json", "secret-password" => "foo"}
20+
```
21+
22+
This means if you are checking for header values in your application, you would need to change your code to use downcased keys.
23+
24+
```ruby
25+
get do
26+
# This would use headers['Secret-Password'] in Rack < 3
27+
error!('Unauthorized', 401) unless headers['secret-password'] == 'swordfish'
28+
end
29+
```
30+
31+
See [#2355](https://github.com/ruby-grape/grape/pull/2355) for more information.
32+
433
### Upgrading to >= 1.7.0
534

635
#### Exceptions renaming

lib/grape.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ def self.deprecator
4242
@deprecator ||= ActiveSupport::Deprecation.new('2.0', 'Grape')
4343
end
4444

45+
def self.rack3?
46+
Gem::Version.new(::Rack.release) >= Gem::Version.new('3')
47+
end
48+
4549
eager_autoload do
4650
autoload :API
4751
autoload :Endpoint

lib/grape/dsl/inside_route.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ def redirect(url, permanent: false, body: nil, **_options)
185185
status 302
186186
body_message ||= "This resource has been moved temporarily to #{url}."
187187
end
188-
header 'Location', url
188+
header Grape::Http::Headers::LOCATION, url
189189
content_type 'text/plain'
190190
body body_message
191191
end
@@ -328,9 +328,9 @@ def sendfile(value = nil)
328328
def stream(value = nil)
329329
return if value.nil? && @stream.nil?
330330

331-
header 'Content-Length', nil
332-
header 'Transfer-Encoding', nil
333-
header 'Cache-Control', 'no-cache' # Skips ETag generation (reading the response up front)
331+
header Grape::Http::Headers::CONTENT_LENGTH, nil
332+
header Grape::Http::Headers::TRANSFER_ENCODING, nil
333+
header Grape::Http::Headers::CACHE_CONTROL, 'no-cache' # Skips ETag generation (reading the response up front)
334334
if value.is_a?(String)
335335
file_body = Grape::ServeStream::FileBody.new(value)
336336
@stream = Grape::ServeStream::StreamResponse.new(file_body)

lib/grape/endpoint.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ def run
250250
if (allowed_methods = env[Grape::Env::GRAPE_ALLOWED_METHODS])
251251
raise Grape::Exceptions::MethodNotAllowed.new(header.merge('Allow' => allowed_methods)) unless options?
252252

253-
header 'Allow', allowed_methods
253+
header Grape::Http::Headers::ALLOW, allowed_methods
254254
response_object = ''
255255
status 204
256256
else

lib/grape/http/headers.rb

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,24 @@ module Headers
1010
PATH_INFO = 'PATH_INFO'
1111
REQUEST_METHOD = 'REQUEST_METHOD'
1212
QUERY_STRING = 'QUERY_STRING'
13-
CONTENT_TYPE = 'Content-Type'
13+
14+
if Grape.rack3?
15+
ALLOW = 'allow'
16+
CACHE_CONTROL = 'cache-control'
17+
CONTENT_LENGTH = 'content-length'
18+
CONTENT_TYPE = 'content-type'
19+
LOCATION = 'location'
20+
TRANSFER_ENCODING = 'transfer-encoding'
21+
X_CASCADE = 'x-cascade'
22+
else
23+
ALLOW = 'Allow'
24+
CACHE_CONTROL = 'Cache-Control'
25+
CONTENT_LENGTH = 'Content-Length'
26+
CONTENT_TYPE = 'Content-Type'
27+
LOCATION = 'Location'
28+
TRANSFER_ENCODING = 'Transfer-Encoding'
29+
X_CASCADE = 'X-Cascade'
30+
end
1431

1532
GET = 'GET'
1633
POST = 'POST'
@@ -24,7 +41,6 @@ module Headers
2441
SUPPORTED_METHODS_WITHOUT_OPTIONS = Grape::Util::LazyObject.new { [GET, POST, PUT, PATCH, DELETE, HEAD].freeze }
2542

2643
HTTP_ACCEPT_VERSION = 'HTTP_ACCEPT_VERSION'
27-
X_CASCADE = 'X-Cascade'
2844
HTTP_TRANSFER_ENCODING = 'HTTP_TRANSFER_ENCODING'
2945
HTTP_ACCEPT = 'HTTP_ACCEPT'
3046

lib/grape/request.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,14 @@ def build_headers
4646
end
4747
end
4848

49-
def transform_header(header)
50-
-header[5..].split('_').each(&:capitalize!).join('-')
49+
if Grape.rack3?
50+
def transform_header(header)
51+
-header[5..].tr('_', '-').downcase
52+
end
53+
else
54+
def transform_header(header)
55+
-header[5..].split('_').map(&:capitalize).join('-')
56+
end
5157
end
5258
end
5359
end

lib/grape/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33
module Grape
44
# The current version of Grape.
5-
VERSION = '1.8.1'
5+
VERSION = '1.9.0'
66
end

spec/grape/api/custom_validations_spec.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,19 @@ def validate(request)
162162
return unless request.params.key? @attrs.first
163163
# check if admin flag is set to true
164164
return unless @option
165+
165166
# check if user is admin or not
166167
# as an example get a token from request and check if it's admin or not
167-
raise Grape::Exceptions::Validation.new(params: @attrs, message: 'Can not set Admin only field.') unless request.headers['X-Access-Token'] == 'admin'
168+
raise Grape::Exceptions::Validation.new(params: @attrs, message: 'Can not set Admin only field.') unless request.headers[access_header] == 'admin'
169+
end
170+
171+
def access_header
172+
Grape.rack3? ? 'x-access-token' : 'X-Access-Token'
168173
end
169174
end
170175
end
171176
let(:app) { Rack::Builder.new(subject) }
177+
let(:x_access_token_header) { Grape.rack3? ? 'x-access-token' : 'X-Access-Token' }
172178

173179
before do
174180
described_class.register_validator('admin', admin_validator)
@@ -197,14 +203,14 @@ def validate(request)
197203
end
198204

199205
it 'does not fail when we send admin fields and we are admin' do
200-
header 'X-Access-Token', 'admin'
206+
header x_access_token_header, 'admin'
201207
get '/', admin_field: 'tester', non_admin_field: 'toaster', admin_false_field: 'test'
202208
expect(last_response.status).to eq 200
203209
expect(last_response.body).to eq 'bacon'
204210
end
205211

206212
it 'fails when we send admin fields and we are not admin' do
207-
header 'X-Access-Token', 'user'
213+
header x_access_token_header, 'user'
208214
get '/', admin_field: 'tester', non_admin_field: 'toaster', admin_false_field: 'test'
209215
expect(last_response.status).to eq 400
210216
expect(last_response.body).to include 'Can not set Admin only field.'

0 commit comments

Comments
 (0)