grape 1.8.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +517 -1
- data/CONTRIBUTING.md +19 -11
- data/README.md +841 -545
- data/UPGRADING.md +1553 -7
- data/grape.gemspec +11 -14
- data/lib/grape/api/instance.rb +77 -160
- data/lib/grape/api.rb +75 -107
- data/lib/grape/content_types.rb +67 -9
- data/lib/grape/cookies.rb +28 -25
- data/lib/grape/declared_params_handler.rb +116 -0
- data/lib/grape/dry_types.rb +48 -6
- data/lib/grape/dsl/callbacks.rb +8 -58
- data/lib/grape/dsl/declared.rb +35 -0
- data/lib/grape/dsl/desc.rb +25 -63
- data/lib/grape/dsl/entity.rb +106 -0
- data/lib/grape/dsl/headers.rb +2 -2
- data/lib/grape/dsl/helpers.rb +83 -64
- data/lib/grape/dsl/inside_route.rb +69 -296
- data/lib/grape/dsl/logger.rb +4 -9
- data/lib/grape/dsl/middleware.rb +22 -40
- data/lib/grape/dsl/parameters.rb +88 -94
- data/lib/grape/dsl/request_response.rb +143 -147
- data/lib/grape/dsl/rescue_options.rb +25 -0
- data/lib/grape/dsl/routing.rb +333 -191
- data/lib/grape/dsl/settings.rb +29 -135
- data/lib/grape/dsl/validations.rb +39 -32
- data/lib/grape/dsl/version_options.rb +23 -0
- data/lib/grape/endpoint/options.rb +25 -0
- data/lib/grape/endpoint.rb +319 -269
- data/lib/grape/{util/env.rb → env.rb} +8 -8
- data/lib/grape/error_formatter/base.rb +57 -21
- data/lib/grape/error_formatter/json.rb +13 -15
- data/lib/grape/error_formatter/serializable_hash.rb +7 -0
- data/lib/grape/error_formatter/txt.rb +12 -18
- data/lib/grape/error_formatter/xml.rb +3 -13
- data/lib/grape/error_formatter.rb +9 -25
- data/lib/grape/exceptions/base.rb +22 -58
- data/lib/grape/exceptions/error_response.rb +48 -0
- data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
- data/lib/grape/exceptions/internal_server_error.rb +16 -0
- data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
- data/lib/grape/exceptions/invalid_formatter.rb +1 -1
- data/lib/grape/exceptions/invalid_message_body.rb +1 -1
- data/lib/grape/exceptions/invalid_version_header.rb +1 -1
- data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
- data/lib/grape/exceptions/method_not_allowed.rb +1 -1
- data/lib/grape/exceptions/missing_group_type.rb +0 -2
- data/lib/grape/exceptions/missing_mime_type.rb +1 -1
- data/lib/grape/exceptions/request_error.rb +11 -0
- data/lib/grape/exceptions/unknown_auth_strategy.rb +11 -0
- data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
- data/lib/grape/exceptions/unknown_parameter.rb +1 -1
- data/lib/grape/exceptions/unknown_params_builder.rb +11 -0
- data/lib/grape/exceptions/unknown_validator.rb +1 -1
- data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
- data/lib/grape/exceptions/validation.rb +28 -11
- data/lib/grape/exceptions/validation_array_errors.rb +5 -0
- data/lib/grape/exceptions/validation_errors.rb +22 -26
- data/lib/grape/formatter/base.rb +16 -0
- data/lib/grape/formatter/json.rb +5 -6
- data/lib/grape/formatter/serializable_hash.rb +7 -10
- data/lib/grape/formatter/txt.rb +3 -5
- data/lib/grape/formatter/xml.rb +4 -6
- data/lib/grape/formatter.rb +7 -25
- data/lib/grape/json.rb +46 -0
- data/lib/grape/locale/en.yml +44 -42
- data/lib/grape/middleware/auth/base.rb +11 -33
- data/lib/grape/middleware/auth/dsl.rb +12 -34
- data/lib/grape/middleware/auth/strategies.rb +1 -2
- data/lib/grape/middleware/base.rb +56 -33
- data/lib/grape/middleware/error.rb +290 -91
- data/lib/grape/middleware/formatter.rb +164 -106
- data/lib/grape/middleware/precomputed_content_types.rb +51 -0
- data/lib/grape/middleware/stack.rb +38 -40
- data/lib/grape/middleware/versioner/accept_version_header.rb +6 -33
- data/lib/grape/middleware/versioner/base.rb +66 -0
- data/lib/grape/middleware/versioner/header.rb +44 -129
- data/lib/grape/middleware/versioner/param.rb +4 -25
- data/lib/grape/middleware/versioner/path.rb +48 -25
- data/lib/grape/middleware/versioner.rb +7 -14
- data/lib/grape/mountable.rb +22 -0
- data/lib/grape/namespace.rb +21 -14
- data/lib/grape/params_builder/base.rb +20 -0
- data/lib/grape/params_builder/hash.rb +11 -0
- data/lib/grape/params_builder/hash_with_indifferent_access.rb +11 -0
- data/lib/grape/params_builder/hashie_mash.rb +11 -0
- data/lib/grape/params_builder.rb +15 -0
- data/lib/grape/parser/base.rb +16 -0
- data/lib/grape/parser/json.rb +6 -8
- data/lib/grape/parser/xml.rb +6 -8
- data/lib/grape/parser.rb +5 -23
- data/lib/grape/path.rb +8 -94
- data/lib/grape/precompiled_json.rb +50 -0
- data/lib/grape/railtie.rb +9 -0
- data/lib/grape/request.rb +200 -26
- data/lib/grape/router/base_route.rb +85 -0
- data/lib/grape/router/greedy_route.rb +30 -0
- data/lib/grape/router/mustermann_pattern.rb +44 -0
- data/lib/grape/router/pattern/path.rb +78 -0
- data/lib/grape/router/pattern.rb +77 -35
- data/lib/grape/router/route.rb +79 -60
- data/lib/grape/router.rb +128 -104
- data/lib/grape/serve_stream/file_body.rb +7 -0
- data/lib/grape/serve_stream/sendfile_response.rb +3 -5
- data/lib/grape/serve_stream/stream_response.rb +7 -0
- data/lib/grape/testing.rb +33 -0
- data/lib/grape/util/api_description.rb +67 -0
- data/lib/grape/util/cache.rb +22 -5
- data/lib/grape/util/deep_freeze.rb +34 -0
- data/lib/grape/util/endpoint_configuration.rb +1 -1
- data/lib/grape/util/freeze_on_new.rb +20 -0
- data/lib/grape/util/header.rb +13 -0
- data/lib/grape/util/inheritable_setting.rb +777 -39
- data/lib/grape/util/lazy/base.rb +16 -0
- data/lib/grape/util/lazy/block.rb +22 -0
- data/lib/grape/util/lazy/value.rb +31 -0
- data/lib/grape/util/lazy/value_array.rb +21 -0
- data/lib/grape/util/lazy/value_enumerable.rb +31 -0
- data/lib/grape/util/lazy/value_hash.rb +21 -0
- data/lib/grape/util/media_type.rb +74 -0
- data/lib/grape/util/path_normalizer.rb +37 -0
- data/lib/grape/util/registry.rb +37 -0
- data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
- data/lib/grape/util/stackable_values.rb +40 -16
- data/lib/grape/util/translation.rb +42 -0
- data/lib/grape/validations/attributes_iterator.rb +61 -28
- data/lib/grape/validations/coerce_options.rb +21 -0
- data/lib/grape/validations/contract_scope.rb +29 -0
- data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
- data/lib/grape/validations/oneof_collector.rb +35 -0
- data/lib/grape/validations/param_scope_tracker.rb +62 -0
- data/lib/grape/validations/params_documentation.rb +52 -0
- data/lib/grape/validations/params_scope.rb +223 -306
- data/lib/grape/validations/shared_options.rb +19 -0
- data/lib/grape/validations/single_attribute_iterator.rb +6 -4
- data/lib/grape/validations/types/array_coercer.rb +7 -12
- data/lib/grape/validations/types/custom_type_coercer.rb +47 -85
- data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +17 -28
- data/lib/grape/validations/types/json.rb +1 -5
- data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
- data/lib/grape/validations/types/primitive_coercer.rb +14 -35
- data/lib/grape/validations/types/set_coercer.rb +1 -4
- data/lib/grape/validations/types/variant_collection_coercer.rb +16 -3
- data/lib/grape/validations/types.rb +29 -54
- data/lib/grape/validations/validations_spec.rb +164 -0
- data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
- data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
- data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
- data/lib/grape/validations/validators/base.rb +118 -37
- data/lib/grape/validations/validators/coerce_validator.rb +26 -38
- data/lib/grape/validations/validators/contract_scope_validator.rb +46 -0
- data/lib/grape/validations/validators/default_validator.rb +13 -16
- data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
- data/lib/grape/validations/validators/except_values_validator.rb +15 -5
- data/lib/grape/validations/validators/length_validator.rb +50 -0
- data/lib/grape/validations/validators/multiple_params_base.rb +12 -9
- data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
- data/lib/grape/validations/validators/oneof_validator.rb +51 -0
- data/lib/grape/validations/validators/presence_validator.rb +4 -2
- data/lib/grape/validations/validators/regexp_validator.rb +11 -3
- data/lib/grape/validations/validators/same_as_validator.rb +7 -15
- data/lib/grape/validations/validators/values_validator.rb +36 -65
- data/lib/grape/validations.rb +8 -21
- data/lib/grape/version.rb +1 -2
- data/lib/grape/xml.rb +17 -0
- data/lib/grape.rb +96 -288
- metadata +83 -294
- data/lib/grape/api/helpers.rb +0 -9
- data/lib/grape/dsl/api.rb +0 -19
- data/lib/grape/dsl/configuration.rb +0 -15
- data/lib/grape/eager_load.rb +0 -20
- data/lib/grape/exceptions/empty_message_body.rb +0 -11
- data/lib/grape/exceptions/missing_option.rb +0 -11
- data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
- data/lib/grape/exceptions/unknown_options.rb +0 -11
- data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -27
- data/lib/grape/extensions/hash.rb +0 -22
- data/lib/grape/extensions/hashie/mash.rb +0 -26
- data/lib/grape/http/headers.rb +0 -61
- data/lib/grape/middleware/globals.rb +0 -16
- data/lib/grape/middleware/helpers.rb +0 -12
- data/lib/grape/middleware/versioner/parse_media_type_patch.rb +0 -24
- data/lib/grape/router/attribute_translator.rb +0 -63
- data/lib/grape/types/invalid_value.rb +0 -8
- data/lib/grape/util/base_inheritable.rb +0 -43
- data/lib/grape/util/inheritable_values.rb +0 -31
- data/lib/grape/util/json.rb +0 -12
- data/lib/grape/util/lazy_block.rb +0 -27
- data/lib/grape/util/lazy_object.rb +0 -43
- data/lib/grape/util/lazy_value.rb +0 -91
- data/lib/grape/util/registrable.rb +0 -15
- data/lib/grape/util/reverse_stackable_values.rb +0 -20
- data/lib/grape/util/strict_hash_configuration.rb +0 -108
- data/lib/grape/util/xml.rb +0 -10
- data/lib/grape/validations/attributes_doc.rb +0 -58
- data/lib/grape/validations/types/build_coercer.rb +0 -94
- data/lib/grape/validations/validator_factory.rb +0 -15
- data/spec/grape/api/custom_validations_spec.rb +0 -213
- data/spec/grape/api/deeply_included_options_spec.rb +0 -56
- data/spec/grape/api/defines_boolean_in_params_spec.rb +0 -38
- data/spec/grape/api/documentation_spec.rb +0 -59
- data/spec/grape/api/inherited_helpers_spec.rb +0 -114
- data/spec/grape/api/instance_spec.rb +0 -103
- data/spec/grape/api/invalid_format_spec.rb +0 -45
- data/spec/grape/api/namespace_parameters_in_route_spec.rb +0 -38
- data/spec/grape/api/nested_helpers_spec.rb +0 -50
- data/spec/grape/api/optional_parameters_in_route_spec.rb +0 -43
- data/spec/grape/api/parameters_modification_spec.rb +0 -41
- data/spec/grape/api/patch_method_helpers_spec.rb +0 -79
- data/spec/grape/api/recognize_path_spec.rb +0 -21
- data/spec/grape/api/required_parameters_in_route_spec.rb +0 -37
- data/spec/grape/api/required_parameters_with_invalid_method_spec.rb +0 -26
- data/spec/grape/api/routes_with_requirements_spec.rb +0 -59
- data/spec/grape/api/shared_helpers_exactly_one_of_spec.rb +0 -41
- data/spec/grape/api/shared_helpers_spec.rb +0 -36
- data/spec/grape/api_remount_spec.rb +0 -509
- data/spec/grape/api_spec.rb +0 -4356
- data/spec/grape/dsl/callbacks_spec.rb +0 -45
- data/spec/grape/dsl/desc_spec.rb +0 -98
- data/spec/grape/dsl/headers_spec.rb +0 -62
- data/spec/grape/dsl/helpers_spec.rb +0 -100
- data/spec/grape/dsl/inside_route_spec.rb +0 -531
- data/spec/grape/dsl/logger_spec.rb +0 -24
- data/spec/grape/dsl/middleware_spec.rb +0 -60
- data/spec/grape/dsl/parameters_spec.rb +0 -180
- data/spec/grape/dsl/request_response_spec.rb +0 -225
- data/spec/grape/dsl/routing_spec.rb +0 -275
- data/spec/grape/dsl/settings_spec.rb +0 -261
- data/spec/grape/dsl/validations_spec.rb +0 -55
- data/spec/grape/endpoint/declared_spec.rb +0 -846
- data/spec/grape/endpoint_spec.rb +0 -1085
- data/spec/grape/entity_spec.rb +0 -336
- data/spec/grape/exceptions/base_spec.rb +0 -81
- data/spec/grape/exceptions/body_parse_errors_spec.rb +0 -185
- data/spec/grape/exceptions/invalid_accept_header_spec.rb +0 -358
- data/spec/grape/exceptions/invalid_formatter_spec.rb +0 -15
- data/spec/grape/exceptions/invalid_response_spec.rb +0 -11
- data/spec/grape/exceptions/invalid_versioner_option_spec.rb +0 -15
- data/spec/grape/exceptions/missing_group_type_spec.rb +0 -17
- data/spec/grape/exceptions/missing_mime_type_spec.rb +0 -17
- data/spec/grape/exceptions/missing_option_spec.rb +0 -15
- data/spec/grape/exceptions/unknown_options_spec.rb +0 -15
- data/spec/grape/exceptions/unknown_validator_spec.rb +0 -15
- data/spec/grape/exceptions/unsupported_group_type_spec.rb +0 -19
- data/spec/grape/exceptions/validation_errors_spec.rb +0 -92
- data/spec/grape/exceptions/validation_spec.rb +0 -19
- data/spec/grape/extensions/param_builders/hash_spec.rb +0 -83
- data/spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb +0 -105
- data/spec/grape/extensions/param_builders/hashie/mash_spec.rb +0 -79
- data/spec/grape/grape_spec.rb +0 -9
- data/spec/grape/integration/global_namespace_function_spec.rb +0 -29
- data/spec/grape/integration/rack_sendfile_spec.rb +0 -48
- data/spec/grape/integration/rack_spec.rb +0 -51
- data/spec/grape/loading_spec.rb +0 -44
- data/spec/grape/middleware/auth/base_spec.rb +0 -31
- data/spec/grape/middleware/auth/dsl_spec.rb +0 -60
- data/spec/grape/middleware/auth/strategies_spec.rb +0 -120
- data/spec/grape/middleware/base_spec.rb +0 -221
- data/spec/grape/middleware/error_spec.rb +0 -85
- data/spec/grape/middleware/exception_spec.rb +0 -294
- data/spec/grape/middleware/formatter_spec.rb +0 -461
- data/spec/grape/middleware/globals_spec.rb +0 -30
- data/spec/grape/middleware/stack_spec.rb +0 -155
- data/spec/grape/middleware/versioner/accept_version_header_spec.rb +0 -122
- data/spec/grape/middleware/versioner/header_spec.rb +0 -345
- data/spec/grape/middleware/versioner/param_spec.rb +0 -171
- data/spec/grape/middleware/versioner/path_spec.rb +0 -62
- data/spec/grape/middleware/versioner_spec.rb +0 -21
- data/spec/grape/named_api_spec.rb +0 -19
- data/spec/grape/parser_spec.rb +0 -86
- data/spec/grape/path_spec.rb +0 -252
- data/spec/grape/presenters/presenter_spec.rb +0 -71
- data/spec/grape/request_spec.rb +0 -126
- data/spec/grape/util/inheritable_setting_spec.rb +0 -242
- data/spec/grape/util/inheritable_values_spec.rb +0 -79
- data/spec/grape/util/reverse_stackable_values_spec.rb +0 -134
- data/spec/grape/util/stackable_values_spec.rb +0 -128
- data/spec/grape/util/strict_hash_configuration_spec.rb +0 -38
- data/spec/grape/validations/attributes_doc_spec.rb +0 -153
- data/spec/grape/validations/instance_behaivour_spec.rb +0 -43
- data/spec/grape/validations/multiple_attributes_iterator_spec.rb +0 -38
- data/spec/grape/validations/params_scope_spec.rb +0 -1420
- data/spec/grape/validations/single_attribute_iterator_spec.rb +0 -56
- data/spec/grape/validations/types/array_coercer_spec.rb +0 -33
- data/spec/grape/validations/types/primitive_coercer_spec.rb +0 -150
- data/spec/grape/validations/types/set_coercer_spec.rb +0 -32
- data/spec/grape/validations/types_spec.rb +0 -111
- data/spec/grape/validations/validators/all_or_none_spec.rb +0 -162
- data/spec/grape/validations/validators/allow_blank_spec.rb +0 -575
- data/spec/grape/validations/validators/at_least_one_of_spec.rb +0 -205
- data/spec/grape/validations/validators/base_spec.rb +0 -38
- data/spec/grape/validations/validators/coerce_spec.rb +0 -1261
- data/spec/grape/validations/validators/default_spec.rb +0 -463
- data/spec/grape/validations/validators/exactly_one_of_spec.rb +0 -233
- data/spec/grape/validations/validators/except_values_spec.rb +0 -192
- data/spec/grape/validations/validators/mutual_exclusion_spec.rb +0 -214
- data/spec/grape/validations/validators/presence_spec.rb +0 -315
- data/spec/grape/validations/validators/regexp_spec.rb +0 -161
- data/spec/grape/validations/validators/same_as_spec.rb +0 -57
- data/spec/grape/validations/validators/values_spec.rb +0 -733
- data/spec/grape/validations/validators/zh-CN.yml +0 -10
- data/spec/grape/validations_spec.rb +0 -2030
- data/spec/integration/eager_load/eager_load_spec.rb +0 -15
- data/spec/integration/multi_json/json_spec.rb +0 -7
- data/spec/integration/multi_xml/xml_spec.rb +0 -7
- data/spec/shared/deprecated_class_examples.rb +0 -16
- data/spec/shared/versioning_examples.rb +0 -215
- data/spec/spec_helper.rb +0 -52
- data/spec/support/basic_auth_encode_helpers.rb +0 -11
- data/spec/support/chunks.rb +0 -14
- data/spec/support/content_type_helpers.rb +0 -15
- data/spec/support/endpoint_faker.rb +0 -25
- data/spec/support/file_streamer.rb +0 -13
- data/spec/support/integer_helpers.rb +0 -13
- data/spec/support/versioned_helpers.rb +0 -55
data/UPGRADING.md
CHANGED
|
@@ -1,6 +1,1555 @@
|
|
|
1
1
|
Upgrading Grape
|
|
2
2
|
===============
|
|
3
3
|
|
|
4
|
+
### Upgrading to >= 4.0.0
|
|
5
|
+
|
|
6
|
+
#### A positional options Hash is no longer accepted by `auth`, `http_basic` or `desc`
|
|
7
|
+
|
|
8
|
+
Deprecated in 3.3 ([#2723](https://github.com/ruby-grape/grape/pull/2723), [#2728](https://github.com/ruby-grape/grape/pull/2728)). These take keyword arguments:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
# Before
|
|
12
|
+
auth :custom, { realm: 'r', opaque: 'o' }
|
|
13
|
+
http_basic({ realm: 'API' })
|
|
14
|
+
desc 'Get users', { detail: 'Returns every user' }
|
|
15
|
+
|
|
16
|
+
# After
|
|
17
|
+
auth :custom, realm: 'r', opaque: 'o'
|
|
18
|
+
http_basic realm: 'API'
|
|
19
|
+
desc 'Get users', detail: 'Returns every user'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
A leftover positional Hash now raises `ArgumentError` rather than warning. A call that already used bare keyword syntax, or a block, is unaffected.
|
|
23
|
+
|
|
24
|
+
#### `http_digest` is removed
|
|
25
|
+
|
|
26
|
+
`Grape::Middleware::Auth::DSL#http_digest` is gone. Calling it now raises `NoMethodError` while the API class is being defined.
|
|
27
|
+
|
|
28
|
+
Nothing it could reach has existed since **2.0.0**, which removed `Rack::Auth::Digest` along with Grape's `:http_digest` strategy ([#2361](https://github.com/ruby-grape/grape/pull/2361)) after Rack 3 dropped digest authentication. The method survived that removal and kept recording its settings happily, so an API declaring `http_digest` still booted — and then raised `Grape::Exceptions::UnknownAuthStrategy` on the *first request*, from inside the middleware build, as an uncaught exception rather than a response. Failing while the class is defined is the point of removing it.
|
|
29
|
+
|
|
30
|
+
**If you registered your own `:http_digest` strategy**, it still works; call `auth` directly:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
Grape::Middleware::Auth::Strategies.add(:http_digest, MyDigestStrategy, ->(settings) { [settings[:realm]] })
|
|
34
|
+
|
|
35
|
+
class API < Grape::API
|
|
36
|
+
auth :http_digest, realm: 'API Authorization', opaque: 'secret' do |username|
|
|
37
|
+
# ...
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The removed method supplied two defaults that `auth` does not, so pass them explicitly if you were relying on them: `realm` defaulted to `'API Authorization'`, and `opaque` to `'secret'` (nested inside `realm` when `realm` was itself a Hash).
|
|
43
|
+
|
|
44
|
+
#### A `rescue_from` handler can no longer return or throw a Hash
|
|
45
|
+
|
|
46
|
+
Deprecated in 3.3. A handler that returned `{ message:, status:, headers: }` was read as an error response. Say what you mean instead:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
# Before
|
|
50
|
+
rescue_from :all do |e|
|
|
51
|
+
{ message: e.message, status: 500, headers: {} }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# After
|
|
55
|
+
rescue_from :all do |e|
|
|
56
|
+
error!(e.message, 500)
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
A handler that still returns such a Hash no longer has it interpreted: the response is an invalid one, which Grape answers with its framework default rather than with the status the Hash carried.
|
|
61
|
+
|
|
62
|
+
#### Middleware `Options` no longer answer `[]`, and the `DEFAULT_OPTIONS` constants are gone
|
|
63
|
+
|
|
64
|
+
Deprecated in 3.3, when middleware options moved to per-class `Options` `Data` value objects. `Grape::Middleware::Error::DEFAULT_OPTIONS`, `Grape::Middleware::Formatter::DEFAULT_OPTIONS` and `Grape::Middleware::Versioner::Base::DEFAULT_OPTIONS` are removed, as is Hash-style access on the `Options` objects themselves:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
# Before
|
|
68
|
+
middleware.config[:default_format]
|
|
69
|
+
Grape::Middleware::Formatter::DEFAULT_OPTIONS[:default_format]
|
|
70
|
+
|
|
71
|
+
# After
|
|
72
|
+
middleware.config.default_format
|
|
73
|
+
Grape::Middleware::Formatter::Options.new.default_format
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`Grape::Middleware::Base` still supports a plain `DEFAULT_OPTIONS` Hash on middleware that declares no `Options` class, so third-party middleware written against that path is unaffected.
|
|
77
|
+
|
|
78
|
+
#### `Grape::Router.normalize_path` is removed
|
|
79
|
+
|
|
80
|
+
Deprecated in 3.3. Use `Grape::Util::PathNormalizer.call`, which is where the implementation has lived since.
|
|
81
|
+
|
|
82
|
+
#### Custom validators read `@options`, not `@option`
|
|
83
|
+
|
|
84
|
+
`Grape::Validations::Validators::Base` kept `@option` as an alias of `@options` from 3.2 with a note to drop it at the next major. A custom validator reading `@option` now sees `nil`:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
# Before
|
|
88
|
+
def validate_param!(attr_name, params)
|
|
89
|
+
return if params[attr_name].length <= @option
|
|
90
|
+
# ...
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# After
|
|
94
|
+
def validate_param!(attr_name, params)
|
|
95
|
+
return if params[attr_name].length <= @options
|
|
96
|
+
# ...
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`@options`, `option_value` and the rest of the documented custom-validator surface are unchanged.
|
|
101
|
+
#### `Grape::ErrorFormatter.formatter_for` answers `nil` for an unregistered format
|
|
102
|
+
|
|
103
|
+
It used to take the API's `default_error_formatter` as a third argument and end in `default_error_formatter || Grape::ErrorFormatter::Txt` — a global registry implementing the caller's fallback policy. It is now a lookup, the same shape as `Grape::Parser.parser_for`:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
Grape::ErrorFormatter.formatter_for(:json) # => Grape::ErrorFormatter::Json
|
|
107
|
+
Grape::ErrorFormatter.formatter_for(:unknown) # => nil, was Grape::ErrorFormatter::Txt
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Passing a third argument now raises `ArgumentError`. The fallback moved to `Grape::Middleware::Error`, which applies the API's `default_error_formatter` and resolves that to `Grape::ErrorFormatter::Txt` when the API set none.
|
|
111
|
+
|
|
112
|
+
Error responses are unchanged, including the precedence: a formatter registered for the requested format still wins over the API's `default_error_formatter`, which applies to formats that have none of their own.
|
|
113
|
+
|
|
114
|
+
Two things follow at definition time, where the lookup's fallback used to hide them.
|
|
115
|
+
|
|
116
|
+
`default_error_formatter` names an error formatter, so a name nothing is registered under is now an error rather than a silent `Txt`:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
default_error_formatter :jsonn
|
|
120
|
+
# => Grape::Exceptions::UnknownErrorFormatter: unknown error formatter: jsonn
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`format` names a *format*, and a format with no error formatter of its own is ordinary, so it still succeeds — but the stored setting is now `nil` rather than `Grape::ErrorFormatter::Txt`:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
content_type :xls, 'application/vnd.ms-excel'
|
|
127
|
+
format :xls
|
|
128
|
+
MyAPI.default_error_formatter # => nil, was Grape::ErrorFormatter::Txt
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The middleware supplies `Txt` at render time, so error responses are unchanged; the setting is only visible if you read it back.
|
|
132
|
+
|
|
133
|
+
#### `error_formatter` now requires a formatter
|
|
134
|
+
|
|
135
|
+
`error_formatter` takes the formatter positionally or as `with:`. Called with neither, it used to register `nil` for that format:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
error_formatter :json # no formatter given
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`Grape::ErrorFormatter.formatter_for` hands a registered value back as-is, so the format resolved to no formatter at all — and every error response in that format then failed to render and came back as the failsafe `500 Internal Server Error` in `text/plain`, whatever status and message the API had asked for.
|
|
142
|
+
|
|
143
|
+
That call now raises `ArgumentError` when the API is defined, rather than on the first error the API tries to render. Pass the formatter:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
error_formatter :json, with: MyErrorFormatter
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
An API that already passes one is unaffected, and a format with no `error_formatter` of its own keeps falling back to `default_error_formatter` and then to `Grape::ErrorFormatter::Txt`.
|
|
150
|
+
|
|
151
|
+
#### `grape/testing` is no longer loaded unless you require it
|
|
152
|
+
|
|
153
|
+
`Grape::Testing` has been documented as opt-in since 3.3 — "intended for test environments only and is not loaded by default" — but it was never excluded from Grape's eager load, so `require 'grape'` installed it anyway. It extends `Grape::Endpoint` with `before_each` / `reset_before_each` and prepends a wrapper around `Grape::Endpoint#run` that every request goes through.
|
|
154
|
+
|
|
155
|
+
It is now excluded, so the documented behaviour is the actual one. If your suite calls `Grape::Endpoint.before_each` without requiring the module first, it will now raise `NoMethodError`. Add the require the 3.3 note already asked for to your test helper:
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
require 'grape/testing'
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Nothing else changes: the module's API is the same, and referencing the `Grape::Testing` constant still autoloads it.
|
|
162
|
+
|
|
163
|
+
#### The versioner's `pattern` option has been removed
|
|
164
|
+
|
|
165
|
+
`Grape::Middleware::Versioner::Base` accepted a `pattern` option, which `Versioner::Path` matched the candidate version segment against before recording it. It dates from the original versioner (2010), where it was the only way to decide whether the first path segment was a version — there was no declared version list yet.
|
|
166
|
+
|
|
167
|
+
`versions:` took that job over: a segment is checked against the versions the API declared, and an unrecognized one answers 404. `pattern` was never connected to the `version` DSL that arrived later — `DSL::Routing#version` does not accept a `pattern:` keyword and `Endpoint#build_stack` never passed one — so it could only be reached by inserting the middleware yourself:
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
use Grape::Middleware::Versioner::Path, versions: %w[v1], pattern: /v.+/
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
which also meant forgoing what `version` does for routing. That now raises `ArgumentError: unknown keyword: :pattern`, at the point the middleware is built rather than on a request.
|
|
174
|
+
|
|
175
|
+
`Grape::Middleware::Versioner::Base::DEFAULT_OPTIONS` no longer carries a `:pattern` key, and `Versioner::Base#pattern` is gone. An API that declares its version through `version 'v1', using: :path` is unaffected — it never had a pattern.
|
|
176
|
+
|
|
177
|
+
The one behaviour that disappears with it: a hand-wired versioner could use `pattern` to say "this segment is not a version at all", leaving `api.version` unset and letting the request through, as distinct from "unknown version", which answers 404. Nothing in the `version` DSL exposed that distinction, and `versions:` cannot express it. If you relied on it, match the segment yourself in a small middleware ahead of the versioner rather than through this option.
|
|
178
|
+
|
|
179
|
+
#### An error response carries a backtrace only when one was asked for
|
|
180
|
+
|
|
181
|
+
Every error response used to materialize the raised exception's backtrace, even though the built-in error formatters render it only under `rescue_from ..., backtrace: true`. It is now assembled only when that option is set, since `Exception#backtrace` builds an Array of location Strings on every call and a Grape error is raised deep inside the request stack.
|
|
182
|
+
|
|
183
|
+
`Grape::Exceptions::ErrorResponse#backtrace` is therefore an empty Array on the payload handed to an error formatter unless the API asked for backtraces. The built-in formatters are unaffected — they already guarded on the `include_backtrace:` argument they are passed.
|
|
184
|
+
|
|
185
|
+
A custom error formatter that reads `error.backtrace` while ignoring that argument will now see an empty Array. Read it from the exception instead, which still travels on the payload:
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
# before
|
|
189
|
+
error.backtrace
|
|
190
|
+
|
|
191
|
+
# after — equivalent regardless of the rescue options
|
|
192
|
+
error.original_exception&.backtrace || []
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`Grape::Exceptions::ErrorResponse.from_exception` no longer stores a backtrace either, for the same reason; `original_exception` is set, so nothing is lost.
|
|
196
|
+
|
|
197
|
+
#### An endpoint that declares helpers is served from a subclass of `Grape::Endpoint`
|
|
198
|
+
|
|
199
|
+
Helpers were included into the singleton class of the per-request copy of an endpoint, which meant every request built a fresh singleton class and re-ran `Module#include`. They are now included once, when the API is compiled, into a subclass owned by that endpoint, and each request copies an instance of it.
|
|
200
|
+
|
|
201
|
+
The endpoint exposed as `env['api.endpoint']` (and handed to `before`/`after` filters, `rescue_from` blocks and middleware) is therefore an instance of that subclass rather than of `Grape::Endpoint` itself, for any endpoint whose scope declares `helpers`. Endpoints with no helpers are unaffected.
|
|
202
|
+
|
|
203
|
+
Inheritance-based checks are unchanged:
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
endpoint.is_a?(Grape::Endpoint) # => true, as before
|
|
207
|
+
endpoint.kind_of?(Grape::Endpoint) # => true, as before
|
|
208
|
+
Grape::Endpoint === endpoint # => true, as before
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Exact-class checks are not, and have no equivalent — use `is_a?`:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
# before => true, now => false
|
|
215
|
+
endpoint.instance_of?(Grape::Endpoint)
|
|
216
|
+
endpoint.class == Grape::Endpoint
|
|
217
|
+
|
|
218
|
+
# before => "Grape::Endpoint", now => "Grape::Endpoint(helpers)"
|
|
219
|
+
endpoint.class.name
|
|
220
|
+
endpoint.class.to_s
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The subclass carries a temporary name (`Module#set_temporary_name`), so it never answers `nil` to `#name` or renders as a bare `#<Class:0x...>` address. Code that logs or groups by `endpoint.class.name` — an error tracker keying on it, say — will see `Grape::Endpoint(helpers)` for these endpoints, and should match on `is_a?` if it needs to treat both kinds alike.
|
|
224
|
+
#### The `desc` `default` key is renamed to `default_response`
|
|
225
|
+
|
|
226
|
+
grape-swagger reads a route's default response as `route.default_response`, a name Grape never defined — the `desc` DSL wrote the key as `default`, and the reader for it resolved to `Hash#default` on the route's options bag, so it answered `nil` for every route. A default response declared the way the README documented it therefore never reached the generated OpenAPI document.
|
|
227
|
+
|
|
228
|
+
The key, the `desc` block method and the route reader are all named `default_response` now. `default` still works everywhere it did, and warns:
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
# deprecated
|
|
232
|
+
desc 'Delete a widget' do
|
|
233
|
+
default code: 'default', message: 'unexpected error'
|
|
234
|
+
end
|
|
235
|
+
desc 'Delete a widget', default: { code: 'default' }
|
|
236
|
+
route.default
|
|
237
|
+
|
|
238
|
+
# preferred
|
|
239
|
+
desc 'Delete a widget' do
|
|
240
|
+
default_response code: 'default', message: 'unexpected error'
|
|
241
|
+
end
|
|
242
|
+
desc 'Delete a widget', default_response: { code: 'default' }
|
|
243
|
+
route.default_response
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
A `desc 'x', default: ...` keyword is stored under `:default_response`, so a route carries one key under one name. Code reading `route.options[:default]` directly — rather than through the reader — has to read `route.options[:default_response]` instead.
|
|
247
|
+
#### A group's type check is now the same for `requires` and `optional`
|
|
248
|
+
|
|
249
|
+
The rule that a group declaration needs a type (`Array`, `Hash`, `JSON` or `Array[JSON]`) was implemented twice — once in `ParamsScope#new_scope` for `requires`, once inline in `optional` — and the two copies had drifted. `optional` read the type before merging in the attributes of an enclosing `with`, so a group type supplied by `with` was invisible to it:
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
params do
|
|
253
|
+
with(type: Hash) do
|
|
254
|
+
requires(:a) { requires :b, type: String } # accepted
|
|
255
|
+
optional(:a) { optional :b, type: String } # raised MissingGroupType
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Both are accepted now. A group with no type from either the declaration or an enclosing `with` still raises `Grape::Exceptions::MissingGroupType`, and an unsupported type still raises `Grape::Exceptions::UnsupportedGroupType`.
|
|
261
|
+
|
|
262
|
+
One case stops raising. `optional :a, using: SomeEntity do ... end` — a block *and* `using:` — raised `MissingGroupType` before; it now ignores the block and documents the entity's params, which is what `requires` has always done with that combination. If you have such a declaration, the block was never going to be applied; delete it or drop `using:`.
|
|
263
|
+
#### The `presence` option of `requires` and `optional` is deprecated
|
|
264
|
+
|
|
265
|
+
`presence` is how the params DSL tells the validation pipeline that a parameter is required. It travelled as a key written into the same options Hash that carries an API's own options, so an API could set it — and got opposite results depending on which method it was passed to, because `requires` overwrote it while nothing overwrote it for `optional`:
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
requires :a, presence: false # ignored: :a is still required
|
|
269
|
+
optional :a, presence: true # honoured: :a is actually required
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Both still resolve exactly as they did, and now emit a deprecation warning. Declare the parameter with `requires` to make it required and `optional` to make it optional; the option will be ignored outright in a future release.
|
|
273
|
+
|
|
274
|
+
#### A `with` block's `message` now reaches the presence validator
|
|
275
|
+
|
|
276
|
+
The presence entry for a `requires` was built before the attributes of an enclosing `with` were merged in, so a group-level `message:` never reached it. It does now:
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
params do
|
|
280
|
+
with(message: 'custom missing') do
|
|
281
|
+
requires :a
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# before: "a is missing"
|
|
286
|
+
# now: "a custom missing"
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
An API that sets `message:` on a `with` block and relies on the generic wording for missing parameters will see its own message instead.
|
|
290
|
+
|
|
291
|
+
#### `refresh_already_mounted` is no longer a `mount` option
|
|
292
|
+
|
|
293
|
+
`mount` accepted an undocumented `refresh_already_mounted` option that replaced any endpoint already mounted for the same base API instead of adding a second one. It exists for Grape's own re-mounting machinery: when a class-level method runs after a `mount`, `Grape::API.refresh_mount_step` replays that mount, and replaying it naively would duplicate the endpoint.
|
|
294
|
+
|
|
295
|
+
That decision belongs to the replay, not to the call, so the endpoint replacement moved into the private `refresh_mounted_api` which is the only caller that ever wanted it. Nothing changes for the re-mounting itself.
|
|
296
|
+
|
|
297
|
+
Passing the option to `mount` still works and now warns; it will be ignored in a future release. There is no replacement, because an API has no reason to ask for it — mounting the same app twice on purpose is still two endpoints.
|
|
298
|
+
|
|
299
|
+
#### A `mount` target is recognised as a mapping only when it is a `Hash`
|
|
300
|
+
|
|
301
|
+
`mount` decided between "a Hash of app => path" and "a bare app mounted at /" with `mounts.respond_to?(:each_pair)`. A `Struct` and an `OpenStruct` answer that too, and neither can express an app => path mapping — their keys are member names — so a Rack app that happened to be one was read as a mapping and silently mounted its own field value as a path:
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
RackStruct = Struct.new(:name) do
|
|
305
|
+
def call(env) = [200, {}, ['ok']]
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
mount RackStruct.new('x')
|
|
309
|
+
|
|
310
|
+
# before: mounted at "/x", because :name => 'x' was read as app => path
|
|
311
|
+
# now: mounted at "/", as the Rack app it is
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
The test is now `mounts.is_a?(Hash)`. Every Hash-like people pass — `HashWithIndifferentAccess`, `Hashie::Mash`, `ActiveSupport::OrderedOptions` — subclasses `Hash` and is unaffected.
|
|
315
|
+
|
|
316
|
+
#### `use`, `helpers`, `rescue_from` and other registrations no longer reach routes defined above them
|
|
317
|
+
|
|
318
|
+
A route captures the middleware, helpers, callbacks and rescue handlers registered above it. That was already true most of the time, but not always: `Grape::Util::InheritableSetting#point_in_time_copy` copied a scope's stackable store and its rescue-handler maps shallowly, so the nested Arrays and Hashes stayed shared with the scope. A registration added *after* an endpoint was defined therefore still reached that endpoint — but only when the key already held at least one registration when the endpoint was defined, since otherwise the scope allocated a fresh store only for itself.
|
|
319
|
+
|
|
320
|
+
The outcome depended on something the API never expressed:
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
class A < Grape::API
|
|
324
|
+
use Middleware1
|
|
325
|
+
get('/x') { } # endpoint defined here
|
|
326
|
+
use Middleware2 # applied to GET /x
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
class B < Grape::API
|
|
330
|
+
get('/x') { } # endpoint defined here
|
|
331
|
+
use Middleware2 # NOT applied to GET /x
|
|
332
|
+
end
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
`A` and `B` state the same thing and behaved differently. Both now behave like `B`. The same held for `rescue_from` declared below a route.
|
|
336
|
+
|
|
337
|
+
**What can break.** An API that declares `use` (or `helpers`, a filter such as `before`, or `rescue_from`) below its routes and relies on it applying to them. That arrangement only ever worked when an earlier registration for the same key happened to seed the stack, so it was never dependable, but code written against it will now see the middleware or helper silently not run.
|
|
338
|
+
|
|
339
|
+
**The fix is to move the registration above the routes it should cover**, which is where Grape's documentation has always placed it:
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
class A < Grape::API
|
|
343
|
+
use Middleware1
|
|
344
|
+
use Middleware2
|
|
345
|
+
get('/x') { }
|
|
346
|
+
end
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Nothing changes for the ordinary arrangement — registrations declared before a route, or inherited from an enclosing namespace or a mounting API, still apply exactly as before, including values an enclosing scope gains after the nested scope was created.
|
|
350
|
+
#### `rescue_from :grape_exceptions` now outranks a catch-all class handler
|
|
351
|
+
|
|
352
|
+
`rescue_from :grape_exceptions` is an opt-in to keep Grape's own errors rendering with their own status — a validation failure answers `400` rather than whatever the application's catch-all returns.
|
|
353
|
+
|
|
354
|
+
It only ever worked against `rescue_from :all`. Written as a class instead, a catch-all is a *registered* handler, which `Grape::Middleware::Error` consults first, and Grape's exceptions are `StandardError`s — so the opt-in was silently inert:
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
rescue_from StandardError do
|
|
358
|
+
error!('server error', 500)
|
|
359
|
+
end
|
|
360
|
+
rescue_from :grape_exceptions
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
| request | before | 4.0 |
|
|
364
|
+
| --- | --- | --- |
|
|
365
|
+
| fails parameter validation | **500** `server error` | `400` |
|
|
366
|
+
| raises an application error | `500` `server error` | unchanged |
|
|
367
|
+
|
|
368
|
+
**What can break.** An API that registers a catch-all as a class *and* opts into `:grape_exceptions` will now answer Grape's own status for Grape's own errors, where it previously answered the catch-all's. That is what the opt-in asks for, so the change makes the two spellings agree — but a client or test asserting the catch-all's status for a validation failure will see the new one.
|
|
369
|
+
|
|
370
|
+
Precedence is unchanged in every other case. A handler registered for a specific Grape exception class is more precise than the opt-in and still wins:
|
|
371
|
+
|
|
372
|
+
```ruby
|
|
373
|
+
rescue_from Grape::Exceptions::ValidationErrors do
|
|
374
|
+
error!('unprocessable', 422) # still runs
|
|
375
|
+
end
|
|
376
|
+
rescue_from StandardError { ... }
|
|
377
|
+
rescue_from :grape_exceptions
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Application errors still reach the catch-all, `rescue_from :all` behaves as before, and `Grape::Exceptions::InvalidVersionHeader` is still never rescued, so version cascading keeps working. An API that does not use `rescue_from :grape_exceptions` is unaffected.
|
|
381
|
+
|
|
382
|
+
#### `redirect` renders its default message as plain text
|
|
383
|
+
|
|
384
|
+
The `redirect` API set the content type to `text/plain`, but delegated body rendering to the formatter. With an API defaulting to JSON, a `redirect` would render the body as JSON with a `text/plain` content-type header.
|
|
385
|
+
|
|
386
|
+
```ruby
|
|
387
|
+
class API < Grape::API
|
|
388
|
+
format :json
|
|
389
|
+
get('/r') { redirect '/there' }
|
|
390
|
+
end
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
```
|
|
394
|
+
Content-Type: text/plain
|
|
395
|
+
|
|
396
|
+
"This resource has been moved temporarily to /there."
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Grape now renders the message it generates with the txt formatter, so the body is the plain sentence the content type claims:
|
|
400
|
+
|
|
401
|
+
```
|
|
402
|
+
Content-Type: text/plain
|
|
403
|
+
|
|
404
|
+
This resource has been moved temporarily to /there.
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**What can break.** Code that parses a redirect body — `JSON.parse(response.body)` on a redirect succeeded before and now raises — or a test asserting on the encoded form. On a JSON API the `Location` header, the status and the `Content-Type` are unchanged, so a client that follows the redirect is unaffected.
|
|
408
|
+
|
|
409
|
+
On an API whose formatter cannot serialize a String, such as `format :xml`, `redirect` did not work at all: the formatter raised, and the response was a `500` carrying an error document and no `Location` header. Those APIs now get the `302` and the `Location` they always should have.
|
|
410
|
+
|
|
411
|
+
This applies only to the message Grape generates. A body you pass yourself is still rendered by the API's formatter, unchanged:
|
|
412
|
+
|
|
413
|
+
```ruby
|
|
414
|
+
redirect '/there', body: { message: 'moved' } # still {"message":"moved"} on a JSON API
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
#### Path params are tagged UTF-8 instead of ASCII-8BIT
|
|
418
|
+
|
|
419
|
+
Params captured from the request path — `route_param`, `:id`-style segments, splats — now come back tagged `UTF-8`. They used to carry the `ASCII-8BIT` encoding of Rack's `PATH_INFO`, because Mustermann decodes the path against that raw string and nothing re-tagged the result. Query and body params were already `UTF-8`, since Rack tags those itself.
|
|
420
|
+
|
|
421
|
+
**Why UTF-8.** Nothing obliges a client to send it — HTTP treats the request target as octets, and Rack's SPEC has CGI keys carry non-ASCII as `ASCII-8BIT`. UTF-8 is a convention rather than a guarantee. But it is the convention Rack itself already applies to everything *except* the path: `Rack::QueryParser#unescape` decodes the query string and form bodies with `URI.decode_www_form_component(string, Encoding::UTF_8)`, which tags the result without validating it.
|
|
422
|
+
|
|
423
|
+
One request carrying the same invalid octets in three places, before this change:
|
|
424
|
+
|
|
425
|
+
| source | encoding | `valid_encoding?` |
|
|
426
|
+
| --- | --- | --- |
|
|
427
|
+
| query — `?q=%C3%28` | `UTF-8` | `false` |
|
|
428
|
+
| form body — `form=%C3%28` | `UTF-8` | `false` |
|
|
429
|
+
| path — `/%C3%28` | **`ASCII-8BIT`** | `true` |
|
|
430
|
+
|
|
431
|
+
The bytes are equally malformed in all three; only the label differed. The path reads `true` merely because `ASCII-8BIT` considers every byte sequence valid. So this change is not Grape adopting an outside convention — it is Grape agreeing with the library handing it the request. The path param was the odd one out only because Mustermann decodes against `PATH_INFO` directly and never had Rack's `unescape` applied to it.
|
|
432
|
+
|
|
433
|
+
Grape does exactly what Rack does: re-tag, do not validate. The bytes are untouched, so octets that are *not* UTF-8 stay detectably invalid rather than being scrubbed into something the client never sent. (Rails takes the same approach for path captures — `ActionDispatch::Journey::Router` force-encodes each one to UTF-8 after unescaping.)
|
|
434
|
+
|
|
435
|
+
**What this fixes.** An API's own declarations used to disagree with themselves depending on where a value arrived from — a binary string never equals the UTF-8 literal it was written as:
|
|
436
|
+
|
|
437
|
+
```ruby
|
|
438
|
+
params { requires :id, type: String, values: ['café'] }
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
| request | before | 4.0 |
|
|
442
|
+
| --- | --- | --- |
|
|
443
|
+
| `GET /?id=café` (query) | `200` | unchanged |
|
|
444
|
+
| `GET /café` (path) | `400 "id does not have a valid value"` | `200` |
|
|
445
|
+
|
|
446
|
+
The same held for `same_as`, `except_values`, and any comparison an endpoint made against a non-ASCII literal. Serialization was affected too: a non-ASCII path param rendered into a JSON response emitted an encoding warning from the `json` gem, and is slated to raise there in json 3.0.
|
|
447
|
+
|
|
448
|
+
**What can break.** Only the encoding tag changes; the bytes are untouched, and an invalid byte sequence stays invalid rather than being scrubbed. Comparisons against pure-ASCII strings are unaffected. Code that relied on a path param being binary — concatenating one with genuinely binary data, for instance — can now raise `Encoding::CompatibilityError`, and should call `.b` on the param to opt back into binary:
|
|
449
|
+
|
|
450
|
+
```ruby
|
|
451
|
+
params[:id].b + binary_blob
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
An application that worked around the old behavior with its own `force_encoding(Encoding::UTF_8)` needs no change; that call is now a no-op.
|
|
455
|
+
|
|
456
|
+
**Restoring the old behavior wholesale.** If enough code depends on binary path params that patching each site is impractical, a `before` filter at the top of your root API re-tags them all back. `env['grape.routing_args']` holds exactly the params that came from the path, so query and body params are left alone, and `before` runs ahead of validation, so declarations see the binary strings too:
|
|
457
|
+
|
|
458
|
+
```ruby
|
|
459
|
+
class API < Grape::API
|
|
460
|
+
before do
|
|
461
|
+
env['grape.routing_args']&.each_key do |key|
|
|
462
|
+
next if key == :route_info
|
|
463
|
+
|
|
464
|
+
value = params[key]
|
|
465
|
+
params[key] = value.b if value.is_a?(String)
|
|
466
|
+
params[key] = value.map(&:b) if value.is_a?(Array)
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# ... mounts and routes
|
|
471
|
+
end
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Declared in the root API, this covers mounted APIs too. Treat it as a migration aid rather than a permanent setting: it restores the inconsistency this change fixes, so `values: ['café']` will keep rejecting `GET /café` while accepting `?id=café`.
|
|
475
|
+
#### A failed error rendering answers 500 instead of escaping the middleware stack
|
|
476
|
+
|
|
477
|
+
When Grape could not render an error response — an error formatter handed a payload it cannot serialize, most often — the exception escaped every middleware above Grape and reached the application server. Rendering runs inside `Grape::Middleware::Error#call!`'s own `rescue` clause, so that clause did not cover it.
|
|
478
|
+
|
|
479
|
+
Grape now answers `500` instead: first retrying the API's format with the framework's own `Internal Server Error` message, then falling back to a bare `text/plain` body if even that cannot be rendered.
|
|
480
|
+
|
|
481
|
+
This mirrors what `ActionDispatch::ShowExceptions#render_exception` does in Rails: try the application's own error rendering, and fall back to a bare `500 Internal Server Error` in `text/plain` when that rendering is itself broken.
|
|
482
|
+
|
|
483
|
+
**What can break.** Code that observed these exceptions by letting them propagate — a test asserting `expect { get '/' }.to raise_error`, most directly — no longer sees them raised. The exception is published on the rack env instead, under both Grape's own key and the conventional one that error trackers read:
|
|
484
|
+
|
|
485
|
+
```ruby
|
|
486
|
+
env[Grape::Env::RACK_EXCEPTION] # 'rack.exception' — what trackers collect
|
|
487
|
+
env[Grape::Env::GRAPE_EXCEPTION] # 'grape.exception' — same object, Grape's key
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
An error tracker mounted as Rack middleware above Grape therefore keeps reporting these with no change on your side: sentry-ruby, for one, collects `env['rack.exception'] || env['sinatra.error']` for exactly this case — an exception that was handled rather than raised. The failure is also written to `rack.errors`, so it lands in the server log even with no tracker installed.
|
|
491
|
+
|
|
492
|
+
**Opting out.** To keep the pre-4.0 behaviour and have the exception propagate out of the middleware stack:
|
|
493
|
+
|
|
494
|
+
```ruby
|
|
495
|
+
Grape.configure do |config|
|
|
496
|
+
config.raise_rendering_errors = true
|
|
497
|
+
end
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
With this on, the failsafe never runs: the exception is re-raised untouched, so neither env key is set and nothing is written to `rack.errors` — whatever caught it before catches it again.
|
|
501
|
+
|
|
502
|
+
Exceptions that no `rescue_from` matches still propagate exactly as before; only rendering failures changed.
|
|
503
|
+
|
|
504
|
+
#### `Array`/`Set` of an unsupported type is rejected when the API is defined
|
|
505
|
+
|
|
506
|
+
Declaring a collection whose element type Grape cannot coerce — `type: Array[Foo]` or `type: Set[Foo]` where `Foo` is neither a primitive, a structure, nor a valid custom type — now raises as soon as the `params` block is evaluated, i.e. while the API class is being loaded:
|
|
507
|
+
|
|
508
|
+
```
|
|
509
|
+
ArgumentError: type Foo should support coercion via `[]`
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
This is a **load-time** failure. An application that boots today can fail to boot on 4.0 without any request being made.
|
|
513
|
+
|
|
514
|
+
A valid custom type is one that implements a class-level `parse` taking exactly one argument (`Grape::Validations::Types.custom?`). A class that implements neither that nor `[]` was never coercible, so such a declaration was always a misconfiguration — but it used to surface much later, and much less clearly.
|
|
515
|
+
|
|
516
|
+
**What changed.** Nothing about which types are supported; only *when* the element coercer is built. [#2817](https://github.com/ruby-grape/grape/pull/2817) made `ArrayCoercer` build it eagerly in the constructor rather than memoizing it on first use, because coercers are shared across requests and must not create state at request time. Building it eagerly means the unsupported element type is discovered while the route is being defined.
|
|
517
|
+
|
|
518
|
+
Previously the coercer was built on the first request that actually supplied the parameter. An API that declared the parameter but was never sent one — a documentation-only declaration, for instance — never built it and never raised. When a request did supply it, the same `ArgumentError` was raised inside the coercer and swallowed by the coercion validator into a generic `400`:
|
|
519
|
+
|
|
520
|
+
```json
|
|
521
|
+
{ "error": "foo is invalid" }
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
so the misconfiguration presented as a puzzling per-request validation failure rather than as a broken declaration.
|
|
525
|
+
|
|
526
|
+
Note that the non-collection form has always raised at definition time:
|
|
527
|
+
|
|
528
|
+
| declaration | before | 4.0 |
|
|
529
|
+
| --- | --- | --- |
|
|
530
|
+
| `type: Foo` | `ArgumentError` when defined | unchanged |
|
|
531
|
+
| `type: Array[Foo]` | accepted; `400 "is invalid"` per request | `ArgumentError` when defined |
|
|
532
|
+
|
|
533
|
+
The collection form's leniency was an accident of the lazy build, not a supported behavior. The two are now consistent.
|
|
534
|
+
|
|
535
|
+
**Fixing a declaration that now raises.** Give the type a one-argument `parse`, which is what makes it a custom type:
|
|
536
|
+
|
|
537
|
+
```ruby
|
|
538
|
+
class Foo
|
|
539
|
+
def self.parse(value)
|
|
540
|
+
new(value)
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
params { requires :foos, type: Array[Foo] }
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
Or coerce the collection yourself, in which case Grape does not build an element coercer at all:
|
|
548
|
+
|
|
549
|
+
```ruby
|
|
550
|
+
params { requires :foos, type: Array[Foo], coerce_with: ->(value) { Array(value).map { |v| Foo.new(v) } } }
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Or, if the class is only there to describe the parameter and never to coerce it — the common case behind this break — drop `type:` and document it instead:
|
|
554
|
+
|
|
555
|
+
```ruby
|
|
556
|
+
params { requires :foos, documentation: { type: Foo } }
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
Defining `self.[]` on the class silences the load-time error, because that is the escape hatch for `dry-types` objects, but it does **not** make the type coercible: such a parameter still fails with `400 "is invalid"` on every request. Prefer `parse` or `coerce_with`.
|
|
560
|
+
|
|
561
|
+
#### The `cascade` getter returns the configured value
|
|
562
|
+
|
|
563
|
+
Calling `cascade` with no argument used to report whether cascading had been *configured at all* (`cascade false` still read back as `true`, contradicting the actual runtime behavior, which was correctly disabled). It now returns the configured value itself — `true`/`false` as set, or `true` when never set. Code that used the getter to detect "was `cascade` called" rather than "does this API cascade" must track that separately.
|
|
564
|
+
#### A cascading route hands over to every remaining route
|
|
565
|
+
|
|
566
|
+
A route that answers with `X-Cascade: pass` — what a version mismatch does by default — now hands the request to every remaining matching route, in registration order, before the router gives up. Previously it handed over to the *last* route registered for the path and stopped there, so with three or more routes sharing a path (typically three mounted API versions) every route in between was unreachable.
|
|
567
|
+
|
|
568
|
+
Two consequences:
|
|
569
|
+
|
|
570
|
+
* **A middle version is now served.** `mount v1; mount v2; mount v3` alongside a catch-all `route :any, '*path'` answered `406 API version not found` for v2, while v1 and v3 worked; v2 is now served.
|
|
571
|
+
* **An unmatched version reaches a catch-all.** When a catch-all ANY route is present, a request whose version matches nothing now falls through to it instead of surfacing the versioner's `406`, which is what `cascade: true` (the default) asks for. Declare `version ..., cascade: false` to keep the hard 406 — that path is unchanged and never consults the catch-all.
|
|
572
|
+
#### `Grape::Util::InheritableValues` has been removed
|
|
573
|
+
|
|
574
|
+
Inheritable settings no longer layer a scope's values over a store handed down from the enclosing scope. Each `Grape::Util::InheritableSetting` now keeps only what its own scope assigned, and resolves a lookup by walking `#parent` — the same move `Grape::Util::StackableValues` got in [#2823](https://github.com/ruby-grape/grape/pull/2823), and the reason `Grape::Util::InheritableValues` no longer has anything to do.
|
|
575
|
+
|
|
576
|
+
Resolution is unchanged in every observable way: the nearest scope wins, a scope may override an inherited value with an explicit `nil`, and a value an enclosing scope gains later is still visible through scopes already nested inside it. Only the class is gone. Nothing in Grape referenced it outside `InheritableSetting`; code that constructed one directly should keep a plain Hash per scope and resolve against the parent chain.
|
|
577
|
+
|
|
578
|
+
#### `forward_match` is no longer exposed on routes
|
|
579
|
+
|
|
580
|
+
`forward_match` is an internal, construction-time detail that decides how a route matches incoming paths (a prefix match for mounted Rack apps, the compiled pattern otherwise). It is now passed to `Grape::Router::Route` as a keyword argument and derived from the mounted app instead of being carried in the route's options.
|
|
581
|
+
|
|
582
|
+
As a result it is no longer readable through `route.options[:forward_match]` or the `route.forward_match` reader — both previously returned the flag. Nothing in Grape consumed either, so this only affects code that introspected routes directly; there is no replacement, as the value is now purely internal.
|
|
583
|
+
|
|
584
|
+
#### `Grape::Endpoint.new` takes `http_methods:` instead of `method:`
|
|
585
|
+
|
|
586
|
+
`Grape::Endpoint.new` now receives the HTTP verb(s) under the `http_methods:` keyword instead of `method:`, matching the name used everywhere else. If you build endpoints directly (uncommon — this is an internal API normally driven by the routing DSL), rename the keyword:
|
|
587
|
+
|
|
588
|
+
```ruby
|
|
589
|
+
# before
|
|
590
|
+
Grape::Endpoint.new(settings, method: :get, path: '/foo', for: self)
|
|
591
|
+
|
|
592
|
+
# after
|
|
593
|
+
Grape::Endpoint.new(settings, http_methods: :get, path: '/foo', for: self)
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
Relatedly, an endpoint's public `options` Hash no longer carries `:method` or `:path`. Nothing in Grape read `:method`, and the only reader of `:path` was an internal test; both values are available from the route instead — `route.request_method` for the verb and `route.path` for the (compiled) path, which is what grape-swagger and other introspection already use. The raw definition-time path array is no longer exposed on the endpoint.
|
|
597
|
+
|
|
598
|
+
#### `Grape::Endpoint.new` takes `api:` instead of `for:`
|
|
599
|
+
|
|
600
|
+
The keyword identifying the API an endpoint belongs to has been renamed from `for:` — a reserved word whose value can't be referenced as a local — to `api:`:
|
|
601
|
+
|
|
602
|
+
```ruby
|
|
603
|
+
# before
|
|
604
|
+
Grape::Endpoint.new(settings, http_methods: :get, path: '/foo', for: my_api)
|
|
605
|
+
|
|
606
|
+
# after
|
|
607
|
+
Grape::Endpoint.new(settings, http_methods: :get, path: '/foo', api: my_api)
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
The owning API is no longer carried on the endpoint's public `options` Hash — `endpoint.options[:for]` is gone. Use the new `endpoint.api` reader instead.
|
|
611
|
+
|
|
612
|
+
#### Route metadata is exposed through readers, not the `options` Hash
|
|
613
|
+
|
|
614
|
+
A route's computed metadata — `version`, `namespace`, `prefix`, `requirements`, `anchor` and `settings` — is now exposed through plain readers instead of being merged into the route's `options` Hash. `namespace`, `prefix` and `settings` are passed to `Grape::Router::Route` as explicit keyword arguments; `version`, `anchor` and `requirements` are read from the route's pattern (they shape how it matches).
|
|
615
|
+
|
|
616
|
+
The readers are unchanged — keep using them:
|
|
617
|
+
|
|
618
|
+
```ruby
|
|
619
|
+
route.version # => 'v1'
|
|
620
|
+
route.namespace # => '/things'
|
|
621
|
+
route.prefix # => 'api'
|
|
622
|
+
route.requirements # => {}
|
|
623
|
+
route.anchor # => true
|
|
624
|
+
route.settings # => { ... }
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
What changed is the raw bag. `route.options` now holds only what was declared for the route, so it no longer carries the *computed* values for these keys — `route.options[:version]`, `[:namespace]`, `[:prefix]` and `[:settings]` return `nil`. Nothing in Grape or grape-swagger read them that way (grape-swagger uses the `route.prefix` and `route.settings` readers). `requirements` and `anchor` — which can be supplied as route options (e.g. a mount's `anchor: false`) — are covered separately below: they too are now first-class endpoint inputs and likewise no longer appear in `route.options`. The effective value always comes from the reader.
|
|
628
|
+
|
|
629
|
+
#### `Grape::Endpoint` no longer accepts a `format:` keyword
|
|
630
|
+
|
|
631
|
+
The `:format` member was removed from the endpoint's internal `Options`. Nothing ever passed `format:` to `Grape::Endpoint.new` — `config.format` was always `nil` — so this has no runtime effect (the `(.:format)`/`(.json)` route suffix comes from `Grape::Path`, not from this value). But `Grape::Endpoint.new(..., format: …)` now raises `unknown keyword: :format` instead of silently ignoring it.
|
|
632
|
+
|
|
633
|
+
`Grape::Router::Pattern#initialize` no longer accepts `format:` either. It was never given a non-`nil` value — a route's `:format` capture comes from the path suffix built by `Grape::Path`, not from the pattern — so the keyword was dead. `Grape::Router::Pattern.new(..., format: …)` now raises `unknown keyword: :format`.
|
|
634
|
+
|
|
635
|
+
#### `desc` no longer populates `namespace_setting(:description)`
|
|
636
|
+
|
|
637
|
+
`desc` used to store its settings under both the route scope (`route_setting(:description)`) and the namespace scope (`namespace_setting(:description)`). The namespace copy was write-only — the namespace scope isn't wired for inheritance and nothing in Grape or grape-swagger ever read it — so `desc` now writes only the route scope. `namespace_setting(:description)` returns `nil`; read a route's description through `route_setting(:description)` or the `route.description` reader.
|
|
638
|
+
#### `Grape::Router::Route#params` no longer takes an argument
|
|
639
|
+
|
|
640
|
+
`Route#params` used to do two jobs depending on its argument: `route.params(input)` extracted param values from a matched request path, while `route.params` (no argument) returned the route's declared param definitions. These are now separate methods:
|
|
641
|
+
|
|
642
|
+
```ruby
|
|
643
|
+
route.params # declared param definitions, keyed by name (unchanged)
|
|
644
|
+
route.params_for(input) # values extracted from a matched path (was route.params(input))
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
The no-argument form is unchanged — grape-swagger and other documentation consumers keep using `route.params`. Only the value-extraction form moved, and it is internal to the router; if you called `route.params(input)` directly, switch to `route.params_for(input)`.
|
|
648
|
+
|
|
649
|
+
#### `params` is a first-class endpoint input, no longer in `route.options`
|
|
650
|
+
|
|
651
|
+
A route's declared params were previously carried inside the `route_options` bag and reachable as `route.options[:params]`. They are now composed into their own endpoint input (`Grape::Endpoint::Options` gains a `:params` member and `Grape::Endpoint.new` a `params:` keyword) and exposed only through `route.params`. `route.options[:params]` now returns `nil`. Nothing in Grape or grape-swagger read it that way — grape-swagger uses the `route.params` method — so this only affects code that reached into the options Hash for params directly.
|
|
652
|
+
|
|
653
|
+
#### `requirements` and `anchor` are first-class endpoint inputs, no longer in `route.options`
|
|
654
|
+
|
|
655
|
+
Like `params` above, a route's `requirements` and `anchor` were previously carried inside the `route_options` bag. They are now composed into their own endpoint inputs (`Grape::Endpoint::Options` gains `:requirements` and `:anchor` members, and `Grape::Endpoint.new` gains `requirements:` and `anchor:` keywords) and exposed only through the `route.requirements` and `route.anchor` readers. `route.options[:requirements]` and `route.options[:anchor]` now return `nil` — including for a mount's `anchor: false`. Nothing in Grape or grape-swagger read them that way, so this only affects code that reached into the options Hash for these keys directly.
|
|
656
|
+
|
|
657
|
+
#### Params state is recorded through `InheritableSetting` accessors
|
|
658
|
+
|
|
659
|
+
The state accumulated by `params` / `contract` blocks — validator instances, declared params, param documentation and reusable named params — is now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`validations` / `add_validation`, `declared_params` / `add_declared_params`, `params_documentation` / `add_params_documentation`, `named_params` / `add_named_params`, `reset_validations!`) instead of raw `namespace_stackable` keys, following the same move made for rescue handlers. The keys' storage is unchanged for now, so `namespace_stackable[:validations]` and friends still return the same values, but they should be considered internal.
|
|
660
|
+
|
|
661
|
+
Related contract changes, none with known external consumers:
|
|
662
|
+
|
|
663
|
+
* `Grape::Endpoint#inherit_settings` now takes the parent `Grape::Util::InheritableSetting` instead of that setting's raw `namespace_stackable` store.
|
|
664
|
+
* The endpoint's request-time validator snapshot moved from `route_setting(:saved_validations)` to `route_setting(:validations)`, symmetric with `route_setting(:declared_params)`. The vestigial `saved_` prefix dated back to the 2014 settings refactor. Neither key appears in `route.settings`, as before.
|
|
665
|
+
* `Grape::Util::InheritableSetting#api_class` (a Hash nothing in Grape ever wrote to) and `#point_in_time_copies` (only ever used internally) are removed.
|
|
666
|
+
#### Callback filters are recorded through `InheritableSetting` accessors
|
|
667
|
+
|
|
668
|
+
The filter blocks registered by `before`, `before_validation`, `after_validation`, `after` and `finally` are now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` — `add_callback(name, block)` to record, and `callbacks` returning a Hash keyed by the DSL method names (`callbacks[:before]`, `callbacks[:finally]`, …) — instead of raw `namespace_stackable` keys, following the same move made for rescue handlers. The keys' storage is unchanged for now, so `namespace_stackable[:befores]` and friends still return the same values, but the pluralized keys should be considered internal.
|
|
669
|
+
#### Rescue configuration is recorded through `InheritableSetting` accessors
|
|
670
|
+
|
|
671
|
+
The remaining rescue state written by `rescue_from` is now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting`, completing the encapsulation started with `rescue_handlers` / `add_rescue_handlers`:
|
|
672
|
+
|
|
673
|
+
* `rescue_options` / `add_rescue_options(options)` replace `namespace_stackable[:rescue_options]`. The reader absorbs the nearest-scope-wins convention (previously the `&.last` at the read site) and returns a single `Grape::DSL::RescueOptions` or `nil`, not the raw stack.
|
|
674
|
+
* `add_all_rescue_handler(handler)`, `add_grape_exceptions_rescue_handler(handler)` and `add_internal_grape_exceptions_rescue_handler(handler)` replace the direct `namespace_inheritable` writes for the `rescue_from :all` / `:grape_exceptions` / `:internal_grape_exceptions` meta selectors; each records its handler and flips the associated flags.
|
|
675
|
+
* `rescue_all?`, `rescue_grape_exceptions?`, `all_rescue_handler`, `grape_exceptions_rescue_handler` and `internal_grape_exceptions_rescue_handler` replace the corresponding `namespace_inheritable` reads. The two `?` readers return `false` (rather than `nil`) when never set; `Grape::Middleware::Error` only ever used them in boolean context, so behavior is unchanged.
|
|
676
|
+
|
|
677
|
+
The keys' storage is unchanged for now, so `namespace_stackable[:rescue_options]` and the `namespace_inheritable` keys still return the same values, but they should be considered internal.
|
|
678
|
+
#### Content negotiation state is recorded through `InheritableSetting` accessors
|
|
679
|
+
|
|
680
|
+
The state written by the `content_type`, `format`, `formatter`, `parser` and `error_formatter` DSL methods is now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`content_types` / `add_content_type`, `formatters` / `add_formatter`, `parsers` / `add_parser`, `error_formatters` / `add_error_formatter`) instead of raw `namespace_stackable` keys, following the same move made for rescue handlers. Each writer records one single-entry Hash per registration and each reader absorbs the `namespace_stackable_with_hash` deep-merge convention, returning the merged name => value Hash (nearest scope wins) or `nil` when nothing is registered. The keys' storage is unchanged for now, so `namespace_stackable[:content_types]` and friends still return the same values, but they should be considered internal.
|
|
681
|
+
#### `represent` registrations are recorded through `InheritableSetting` accessors
|
|
682
|
+
|
|
683
|
+
The model-class => entity-class registrations written by `represent` are now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`representations` / `add_representation(model_class, entity_class)`) instead of raw `namespace_stackable` keys, following the same move made for rescue handlers. The reader absorbs the `namespace_stackable_with_hash` deep-merge convention, returning the merged Hash (nearest scope wins) or `nil` when nothing is registered. The key's storage is unchanged for now, so `namespace_stackable[:representations]` still returns the same values, but it should be considered internal.
|
|
684
|
+
|
|
685
|
+
Also removed: the `namespace_stackable[:representations] ||= []` line in `Grape::Endpoint#initialize`. It was provably inert — `Grape::Util::StackableValues#[]` always returns an array (a frozen empty one for never-written keys), which is truthy, so the `||=` assignment could never execute. No settings state changes as a result.
|
|
686
|
+
#### Middleware and helper registrations are recorded through `InheritableSetting` accessors
|
|
687
|
+
|
|
688
|
+
The state written by the middleware DSL (`use`, `insert`, `insert_before`, `insert_after`) and by `helpers` is now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`middleware` / `add_middleware`, `helpers` / `add_helper`) instead of raw `namespace_stackable` keys, following the same move made for rescue handlers. The keys' storage is unchanged for now, so `namespace_stackable[:middleware]` and `namespace_stackable[:helpers]` still return the same values, but they should be considered internal.
|
|
689
|
+
|
|
690
|
+
One micro-change: the `middleware` DSL reader dropped its `|| []` fallback — `Grape::Util::StackableValues#[]` never returns `nil`, so the fallback was dead code. When no middleware is registered the reader now returns the shared frozen empty array instead of a fresh mutable one; no caller in Grape or grape-swagger mutates the returned array.
|
|
691
|
+
#### Namespace and mount-path registrations are recorded through `InheritableSetting` accessors
|
|
692
|
+
|
|
693
|
+
The `Grape::Namespace` objects registered by the `namespace` DSL (and its `group` / `resource` / `resources` / `segment` aliases) and the mount path recorded by `mount` are now written and read through dedicated accessors on `Grape::Util::InheritableSetting` instead of raw `namespace_stackable` keys, following the same move made for rescue handlers:
|
|
694
|
+
|
|
695
|
+
* `namespaces` / `add_namespace(namespace)` replace `namespace_stackable[:namespace]` (not to be confused with the pre-existing `namespace` reader, which returns the namespace-settings store).
|
|
696
|
+
* `namespace_path` returns the normalized joined path prefix, absorbing the `Grape::Namespace.joined_space_path(...)` call previously spelled out at the read sites, and `namespace_requirements` returns the requirements declared by registered namespaces, absorbing the `filter_map(&:requirements)`.
|
|
697
|
+
* `mount_path` / `add_mount_path(path)` replace `namespace_stackable[:mount_path]`; the reader absorbs the outermost-wins convention (previously the `.first` at the read site in `Endpoint#build_stack`).
|
|
698
|
+
|
|
699
|
+
The keys' storage is unchanged for now, so `namespace_stackable[:namespace]` and `namespace_stackable[:mount_path]` still return the same values, but they should be considered internal.
|
|
700
|
+
#### Contract key maps are recorded through `InheritableSetting` accessors
|
|
701
|
+
|
|
702
|
+
The Dry::Schema key maps registered by `contract` blocks are now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`contract_key_maps` / `add_contract_key_map(key_map)`) instead of the raw `namespace_stackable[:contract_key_map]` key, following the same move made for rescue handlers. The key's storage is unchanged for now, so `namespace_stackable[:contract_key_map]` still returns the same values, but it should be considered internal.
|
|
703
|
+
#### Format and error-response defaults are recorded through `InheritableSetting` accessors
|
|
704
|
+
|
|
705
|
+
The nearest-wins scalars written by the `format`, `default_format`, `default_error_formatter` and `default_error_status` DSL methods are now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`format` / `format=`, `default_format` / `default_format=`, `default_error_formatter` / `default_error_formatter=`, `default_error_status` / `default_error_status=`) instead of raw `namespace_inheritable` keys — the first batch of the `namespace_inheritable` cleanup, following the pattern used for `namespace_stackable`. Since these are overriding assignments rather than stacking registrations, the writers use plain `=` instead of the `add_*` naming. The keys' storage is unchanged for now, so `namespace_inheritable[:format]` and friends still return the same values, but they should be considered internal.
|
|
706
|
+
#### Versioning state is recorded through `InheritableSetting` accessors
|
|
707
|
+
|
|
708
|
+
The nearest-wins scalars written by the `version`, `prefix` and `cascade` DSL methods are now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`version` / `version=`, `version_options` / `version_options=`, `root_prefix` / `root_prefix=`, `cascade` / `cascade=`) instead of raw `namespace_inheritable` keys. `cascade` is presence-sensitive — an explicit `nil` is distinct from never-set — so the accessor family includes `cascade_defined?`, absorbing the `namespace_inheritable.key?(:cascade)` checks previously spelled out in `DSL::Routing#cascade` and `Grape::API::Instance#cascade?`. The keys' storage is unchanged for now, so `namespace_inheritable[:version]` and friends still return the same values, but they should be considered internal.
|
|
709
|
+
#### Routing scope flags are recorded through `InheritableSetting` accessors
|
|
710
|
+
|
|
711
|
+
The flags flipped by `do_not_route_head!`, `do_not_route_options!`, `do_not_document!` and `lint!` are now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`!` writers, `?` readers) instead of raw `namespace_inheritable` keys. The `?` readers return `false` (rather than `nil`) when never set; every consumer only used them in boolean context, so behavior is unchanged. The keys' storage is unchanged for now, so `namespace_inheritable[:do_not_route_head]` and friends still return the same values, but they should be considered internal.
|
|
712
|
+
#### `build_params_with` and `auth` are recorded through `InheritableSetting` accessors
|
|
713
|
+
|
|
714
|
+
The params-builder strategy written by `build_with` (both the API-level and the params-block DSL) and the authentication configuration written by the `auth` DSL are now recorded and read through dedicated accessors on `Grape::Util::InheritableSetting` (`build_params_with` / `build_params_with=`, `auth` / `auth=`) instead of raw `namespace_inheritable` keys, completing the per-key `namespace_inheritable` cleanup. The keys' storage is unchanged for now, so `namespace_inheritable[:build_params_with]` and `namespace_inheritable[:auth]` still return the same values, but they should be considered internal.
|
|
715
|
+
|
|
716
|
+
#### `InheritableSetting`'s raw stores are internal, except `namespace_stackable`
|
|
717
|
+
|
|
718
|
+
The per-key encapsulation documented in the sections above is now enforced where the ecosystem allows: `Grape::Util::InheritableSetting#namespace_inheritable` is protected and `#namespace_stackable_with_hash` is private — every `namespace_inheritable` key is reachable only through its dedicated accessor.
|
|
719
|
+
|
|
720
|
+
`#namespace_stackable` deliberately **remains public**: grape-swagger reads it directly (`swagger_documentation_adder.rb` reads `[:namespace]` and `[:mount_path]`, `token_owner_resolver.rb` reads `[:helpers]`, and `request_param_parsers/route.rb` walks the `StackableValues#inherited_values` chain to collect per-scope params). Treat it as read-only from outside Grape and prefer the dedicated accessors (`namespaces`, `mount_paths`, `helpers`, …) for everything they cover; the reader may be narrowed in a future major once grape-swagger has migrated. The `route`, `namespace` and `global` scratch stores (backing `route_setting` / `namespace_setting` / `global_setting`) also remain public and unchanged.
|
|
721
|
+
|
|
722
|
+
Two supporting changes:
|
|
723
|
+
|
|
724
|
+
* `Router::Pattern::Path` no longer receives a merged dump of both stores; `Endpoint#to_routes` passes the new `InheritableSetting#path_settings` snapshot — a `Grape::Util::InheritableSetting::PathSettings` value object (`Data`) — which carries exactly the six values `Path` reads (full mount-path stack, root prefix, format, raw content-types stack, version, version options) with always-present keys. `Path`'s `key?`-presence guards became nil-tolerant truthiness checks — equivalent under the snapshot. `Endpoint#prepare_default_path_settings` is removed.
|
|
725
|
+
* `InheritableSetting#mount_paths` is added, exposing the full mount-path stack (one entry per mount level, outermost first), complementing `#mount_path`, which returns only the outermost entry.
|
|
726
|
+
|
|
727
|
+
#### `Grape::Util::StackableValues` is a read-only view, not a store
|
|
728
|
+
|
|
729
|
+
Stackable registrations now live on `Grape::Util::InheritableSetting` itself, as one plain Hash of `key => Array` per scope, and inheritance is resolved by walking `#parent` — the same recursion `#rescue_handlers` already used. Previously the registrations lived in a `StackableValues` instance which maintained its own parallel chain through `#inherited_values`, so every scope carried two links to its parent.
|
|
730
|
+
|
|
731
|
+
`InheritableSetting#namespace_stackable` still returns a `Grape::Util::StackableValues`, still resolves reads across the chain, and still exposes `#new_values` / `#inherited_values` / `#keys` / `#to_hash`, so the grape-swagger call sites listed above keep working unchanged. What changed:
|
|
732
|
+
|
|
733
|
+
* It is now a **view built on demand**, not the backing store. Each call returns a new instance, so the object is no longer identity-stable across calls, and writing to it registers nothing.
|
|
734
|
+
* `StackableValues#[]=` and `#delete` are removed — the class is read-only. Registration happens through the semantic `add_*` accessors on `InheritableSetting`.
|
|
735
|
+
* `StackableValues.new` now takes `(new_values, inherited_values)` instead of `(inherited_values = nil)`.
|
|
736
|
+
* `Grape::Util::BaseInheritable` is removed. It existed only to share plumbing between `StackableValues` and `InheritableValues`; with the former no longer a store, its contents moved into `Grape::Util::InheritableValues`, whose public behavior is unchanged (its now-unused `#keys` is dropped).
|
|
737
|
+
|
|
738
|
+
Settings semantics are otherwise deliberately identical, including two pre-existing quirks worth knowing about, since neither is fixed here:
|
|
739
|
+
|
|
740
|
+
* A stack reader returns the backing Array itself when only the current scope registered anything (a frozen empty Array when nothing is registered anywhere), so callers must treat the result as read-only.
|
|
741
|
+
* `#point_in_time_copy` copies the per-scope Hash shallowly, so the per-key Arrays stay shared with the source. A registration made on a scope *after* an endpoint was defined in it is therefore still visible to that endpoint — but only when the same key already had a registration when the endpoint was defined. This is why `use SomeMiddleware` written below a route can still apply to it.
|
|
742
|
+
|
|
743
|
+
### Upgrading to >= 3.3
|
|
744
|
+
|
|
745
|
+
#### Minimum required Ruby is now 3.3
|
|
746
|
+
|
|
747
|
+
Grape no longer supports Ruby 3.2; 3.3 is now the minimum (`required_ruby_version = '>= 3.3'`). Upgrade your runtime to Ruby 3.3 or newer before bumping Grape.
|
|
748
|
+
|
|
749
|
+
#### `mustermann-grape` is no longer a dependency
|
|
750
|
+
|
|
751
|
+
Grape's path-pattern grammar (previously the `mustermann-grape` gem) now lives in Grape itself as `Grape::Router::MustermannPattern`, and Grape depends on `mustermann` directly. This is transparent for normal Grape usage.
|
|
752
|
+
|
|
753
|
+
The inlined class is no longer registered as a Mustermann type, so if your app called `Mustermann.new(pattern, type: :grape)` and relied on Grape loading `mustermann-grape` for you, add it to your Gemfile explicitly:
|
|
754
|
+
|
|
755
|
+
```ruby
|
|
756
|
+
gem 'mustermann-grape'
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
#### `Grape::Exceptions::ValidationErrors.new` keyword renamed `errors:` → `exceptions:`
|
|
760
|
+
|
|
761
|
+
`Grape::Exceptions::ValidationErrors#initialize` now takes its input array under the `exceptions:` keyword instead of `errors:`. The kwarg accepts a mix of `Grape::Exceptions::Validation` and `Grape::Exceptions::ValidationArrayErrors` instances; `ValidationArrayErrors` wrappers are flattened internally via `flat_map(&:errors)`. The `errors` reader on the constructed instance (the grouped `{params => [Validation, ...]}` Hash) is unchanged.
|
|
762
|
+
|
|
763
|
+
```ruby
|
|
764
|
+
# before
|
|
765
|
+
Grape::Exceptions::ValidationErrors.new(errors: [validation, validation_array_errors], headers:)
|
|
766
|
+
|
|
767
|
+
# after
|
|
768
|
+
Grape::Exceptions::ValidationErrors.new(exceptions: [validation, validation_array_errors], headers:)
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
#### `Grape::Exceptions::ValidationErrors` no longer mixes in `Enumerable`
|
|
772
|
+
|
|
773
|
+
`Grape::Exceptions::ValidationErrors` no longer includes `Enumerable` and no longer defines a public `#each`. The Enumerable surface (`#each`, `#map`, `#select`, `#to_a`, etc.) was undocumented and untested; the documented accessors — `#errors`, `#full_messages`, `#message`, `#as_json` — are unchanged.
|
|
774
|
+
|
|
775
|
+
If a `rescue_from` block iterated over the exception instance, switch to `#errors`:
|
|
776
|
+
|
|
777
|
+
```ruby
|
|
778
|
+
# before
|
|
779
|
+
rescue_from Grape::Exceptions::ValidationErrors do |e|
|
|
780
|
+
e.each { |attribute, error| ... }
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
# after
|
|
784
|
+
rescue_from Grape::Exceptions::ValidationErrors do |e|
|
|
785
|
+
e.errors.each do |attributes, errs|
|
|
786
|
+
errs.each { |error| ... }
|
|
787
|
+
end
|
|
788
|
+
end
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
#### `rescue_from` rejects meta selectors mixed with exception classes
|
|
792
|
+
|
|
793
|
+
`rescue_from` used to silently drop additional exception classes when its first argument was a meta selector (`:all`, `:grape_exceptions`, `:internal_grape_exceptions`). It now raises `ArgumentError` so the misuse is caught at definition time:
|
|
794
|
+
|
|
795
|
+
```ruby
|
|
796
|
+
# previously: MyError was silently dropped — only :all took effect
|
|
797
|
+
rescue_from :all, MyError, with: :handler
|
|
798
|
+
|
|
799
|
+
# now: ArgumentError ("rescue_from :all does not accept additional arguments")
|
|
800
|
+
# split into two declarations instead:
|
|
801
|
+
rescue_from :all, with: :handler
|
|
802
|
+
rescue_from MyError, with: :other_handler
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
Calls that only use one meta selector or only use exception classes (the documented forms) are unaffected.
|
|
806
|
+
|
|
807
|
+
#### `auth` and `http_basic` now take keyword arguments
|
|
808
|
+
|
|
809
|
+
`Grape::Middleware::Auth::DSL#auth` and `#http_basic` now accept their options as keyword arguments instead of a positional `Hash`. Calls using bare keyword syntax or a block are unaffected:
|
|
810
|
+
|
|
811
|
+
```ruby
|
|
812
|
+
http_basic realm: 'API' do |u, p|
|
|
813
|
+
# ...
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
auth :my_strategy, realm: 'API', &proc
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
Passing a positional options `Hash` still works but is deprecated and will be removed in a future release:
|
|
820
|
+
|
|
821
|
+
```ruby
|
|
822
|
+
# deprecated
|
|
823
|
+
http_basic({ realm: 'API' })
|
|
824
|
+
auth :my_strategy, { realm: 'API' }
|
|
825
|
+
|
|
826
|
+
# preferred
|
|
827
|
+
http_basic(realm: 'API')
|
|
828
|
+
auth :my_strategy, realm: 'API'
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
#### Middleware options now route through per-class `Options` `Data` value objects
|
|
832
|
+
|
|
833
|
+
`Grape::Middleware::Error`, `Grape::Middleware::Formatter`, and `Grape::Middleware::Versioner::Base` each declare an `Options` `Data.define` and route their `**options` kwargs through it on `initialize`. This means **unknown kwargs now raise `ArgumentError`** instead of being silently swallowed:
|
|
834
|
+
|
|
835
|
+
```ruby
|
|
836
|
+
# previously: silently swallowed (Formatter doesn't actually read :rescue_options)
|
|
837
|
+
Grape::Middleware::Formatter.new(app, rescue_options: { backtrace: true })
|
|
838
|
+
|
|
839
|
+
# now: ArgumentError (unknown keyword: :rescue_options)
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
Each `Options` class accepts exactly the kwargs the middleware actually reads. The supported sets:
|
|
843
|
+
|
|
844
|
+
- `Middleware::Error::Options`: `all_rescue_handler`, `base_only_rescue_handlers`, `content_types`, `default_error_formatter`, `default_message`, `default_status`, `error_formatters`, `format`, `grape_exceptions_rescue_handler`, `internal_grape_exceptions_rescue_handler`, `rescue_all`, `rescue_grape_exceptions`, `rescue_handlers`, `rescue_options`.
|
|
845
|
+
- `Middleware::Formatter::Options`: `content_types`, `default_format`, `format`, `formatters`, `parsers`.
|
|
846
|
+
- `Middleware::Versioner::Base::Options`: `content_types`, `format`, `mount_path`, `pattern`, `prefix`, `version_options`, `versions`.
|
|
847
|
+
|
|
848
|
+
The `Hash`-based `options` reader on `Grape::Middleware::Base` continues to return a frozen Hash representation of the Data (`config.to_h.freeze`) for back-compat with subclasses that read `options[:key]`. A new `config` reader exposes the typed Data instance — prefer the named accessors going forward:
|
|
849
|
+
|
|
850
|
+
```ruby
|
|
851
|
+
# back-compat (still works)
|
|
852
|
+
options[:format]
|
|
853
|
+
|
|
854
|
+
# preferred
|
|
855
|
+
config.format
|
|
856
|
+
# or, on converted middlewares, just `format` (provided via def_delegators)
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
`Options#[]` is defined as a Hash-style shim with a deprecation warning so legacy `data[:key]` callers get a migration nudge:
|
|
860
|
+
|
|
861
|
+
```ruby
|
|
862
|
+
# emits Grape.deprecator warning
|
|
863
|
+
Grape::Middleware::Error::Options.new[:format]
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
#### `DEFAULT_OPTIONS` constants on converted middlewares are deprecated
|
|
867
|
+
|
|
868
|
+
`Grape::Middleware::Error::DEFAULT_OPTIONS`, `Grape::Middleware::Formatter::DEFAULT_OPTIONS`, and `Grape::Middleware::Versioner::Base::DEFAULT_OPTIONS` still exist as a frozen `Hash` representation of the `Options` defaults (`Options.new.to_h.freeze`), for back-compat with any code that referenced these constants directly. They will be removed in a future release; introspect the `Options` `Data` class itself instead.
|
|
869
|
+
|
|
870
|
+
#### `Grape::Middleware::Globals` removed
|
|
871
|
+
|
|
872
|
+
`Grape::Middleware::Globals` and the three env constants it set (`Grape::Env::GRAPE_REQUEST`, `Grape::Env::GRAPE_REQUEST_HEADERS`, `Grape::Env::GRAPE_REQUEST_PARAMS`) have been deleted. The middleware was introduced in 2013 (commit `9987090b`) but never mounted by Grape's own stack — the `Grape::Request` it built is now constructed directly inside `Grape::Endpoint`. Nothing in `lib/` read those env keys.
|
|
873
|
+
|
|
874
|
+
If you mounted `Grape::Middleware::Globals` in your own Rack stack to populate `env['grape.request']` for downstream middleware, replicate it locally:
|
|
875
|
+
|
|
876
|
+
```ruby
|
|
877
|
+
class MyGlobals
|
|
878
|
+
def initialize(app); @app = app; end
|
|
879
|
+
|
|
880
|
+
def call(env)
|
|
881
|
+
request = Grape::Request.new(env)
|
|
882
|
+
env['grape.request'] = request
|
|
883
|
+
env['grape.request.headers'] = request.headers
|
|
884
|
+
env['grape.request.params'] = request.params if env['rack.input']
|
|
885
|
+
@app.call(env)
|
|
886
|
+
end
|
|
887
|
+
end
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
The original implementation is preserved in git history at [`6b4111b3:lib/grape/middleware/globals.rb`](https://github.com/ruby-grape/grape/blob/6b4111b3/lib/grape/middleware/globals.rb).
|
|
891
|
+
|
|
892
|
+
#### `error_formatter` now receives a `Grape::Exceptions::ErrorResponse` value object
|
|
893
|
+
|
|
894
|
+
Custom error formatters now receive a frozen `Grape::Exceptions::ErrorResponse` as the `error:` keyword argument, alongside three request-time context kwargs. The new signature:
|
|
895
|
+
|
|
896
|
+
```ruby
|
|
897
|
+
def call(error:, env: nil, include_backtrace: false, include_original_exception: false)
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
`error` is the same value object the middleware uses internally, with `status` / `message` / `headers` / `backtrace` / `original_exception` accessors. The two `include_*` booleans are forwarded from the matching `rescue_from` options (previously buried inside `options[:rescue_options]`).
|
|
901
|
+
|
|
902
|
+
Existing positional formatters break and need to be updated:
|
|
903
|
+
|
|
904
|
+
```ruby
|
|
905
|
+
# Before
|
|
906
|
+
error_formatter :txt, ->(message, backtrace, options, env, original_exception) { ... }
|
|
907
|
+
|
|
908
|
+
module CustomFormatter
|
|
909
|
+
def self.call(message, backtrace, options, env, original_exception)
|
|
910
|
+
...
|
|
911
|
+
end
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
# After — pick fields off `error`
|
|
915
|
+
error_formatter :txt, ->(error:, **) { "[#{error.status}] #{error.message}" }
|
|
916
|
+
|
|
917
|
+
module CustomFormatter
|
|
918
|
+
def self.call(error:, **)
|
|
919
|
+
{ status: error.status, message: error.message, backtrace: error.backtrace }
|
|
920
|
+
end
|
|
921
|
+
end
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
Migration:
|
|
925
|
+
|
|
926
|
+
| Old positional arg | New |
|
|
927
|
+
| --- | --- |
|
|
928
|
+
| `message` | `error.message` |
|
|
929
|
+
| `backtrace` | `error.backtrace` |
|
|
930
|
+
| `original_exception` | `error.original_exception` |
|
|
931
|
+
| `options[:rescue_options][:backtrace]` | `include_backtrace` (kwarg) |
|
|
932
|
+
| `options[:rescue_options][:original_exception]` | `include_original_exception` (kwarg) |
|
|
933
|
+
| `env` | `env` (kwarg, still passed) |
|
|
934
|
+
| HTTP status | `error.status` (newly exposed) |
|
|
935
|
+
| Response headers | `error.headers` (newly exposed) |
|
|
936
|
+
|
|
937
|
+
The remaining middleware-options keys (`default_status`, `format`, `rescue_handlers`, …) were framework-internal and have never been part of the documented contract.
|
|
938
|
+
|
|
939
|
+
The change resolves [#2527](https://github.com/ruby-grape/grape/issues/2527): the HTTP `status` and the response `headers` are now part of the formatter contract, so JSON:API–style error bodies (which embed the status code) and header-aware formatters can be written without reaching into `env[Grape::Env::API_ENDPOINT]`.
|
|
940
|
+
|
|
941
|
+
#### `version` now takes explicit keyword arguments
|
|
942
|
+
|
|
943
|
+
`version` previously accepted `**options` and silently ignored any keys it didn't use. It now declares its options explicitly:
|
|
944
|
+
|
|
945
|
+
```ruby
|
|
946
|
+
def version(*args, using: :path, cascade: true, parameter: 'apiver', strict: false, vendor: nil, &block)
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
Passing an unrecognised keyword now raises `ArgumentError` instead of being swallowed. The most common offender is `format:` — it was never a `version` option (response format is set with `format`/`default_format`, and header-versioned requests carry the format in their `Accept` header), but the old splat let `version 'v1', using: :header, vendor: 'x', format: :json` through as a no-op.
|
|
950
|
+
|
|
951
|
+
```ruby
|
|
952
|
+
# Before — `format:` silently ignored
|
|
953
|
+
version 'v1', using: :header, vendor: 'x', format: :json
|
|
954
|
+
|
|
955
|
+
# After
|
|
956
|
+
version 'v1', using: :header, vendor: 'x' # set responses with `format :json` / `default_format :json`
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
Recognized keys are `using:`, `cascade:`, `parameter:`, `strict:`, `vendor:`. Calls using only those are unaffected.
|
|
960
|
+
|
|
961
|
+
#### `Grape::Middleware::Base#options` is now frozen
|
|
962
|
+
|
|
963
|
+
`@options` is frozen at the end of `Grape::Middleware::Base#initialize` (after `merge_default_options`). The hash is initialized once and treated as immutable for the lifetime of the middleware. Custom middleware that mutates `options[...]` at runtime will now raise `FrozenError`.
|
|
964
|
+
|
|
965
|
+
If your custom middleware was patching its own options on the fly:
|
|
966
|
+
|
|
967
|
+
```ruby
|
|
968
|
+
# Before
|
|
969
|
+
class MyMiddleware < Grape::Middleware::Base
|
|
970
|
+
def before
|
|
971
|
+
options[:flag] = compute_flag
|
|
972
|
+
# ...
|
|
973
|
+
end
|
|
974
|
+
end
|
|
975
|
+
|
|
976
|
+
# After — store mutable runtime state on a dedicated ivar
|
|
977
|
+
class MyMiddleware < Grape::Middleware::Base
|
|
978
|
+
def before
|
|
979
|
+
@flag = compute_flag
|
|
980
|
+
# ...
|
|
981
|
+
end
|
|
982
|
+
end
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
Reading `options[...]` is unchanged.
|
|
986
|
+
|
|
987
|
+
#### Throw `:error` payloads are now `Grape::Exceptions::ErrorResponse`
|
|
988
|
+
|
|
989
|
+
The payload thrown via `throw :error, ...` is now a `Grape::Exceptions::ErrorResponse` value object instead of a `Hash`. If you `catch(:error)` and inspect the payload, switch from `payload[:status]` to `payload.status` (or `payload[:message]` to `payload.message`, etc.). User-defined `throw :error, hash` calls continue to work — `Middleware::Error#error_response` coerces Hashes, exceptions, and `ErrorResponse` instances at the boundary.
|
|
990
|
+
|
|
991
|
+
Returning or throwing a `Hash` with `:message`, `:status`, and `:headers` from a `rescue_from` handler is now deprecated and will be removed in a future release. Use `error!(...)` or return/throw a `Grape::Exceptions::ErrorResponse` instead.
|
|
992
|
+
|
|
993
|
+
#### `Grape::Request#grape_routing_args` has been removed
|
|
994
|
+
|
|
995
|
+
`grape_routing_args` was previously public to support third-party `params_builder` extensions, which have since been removed. With no remaining callers, the method has been removed. If you were calling it externally, read `env[Grape::Env::GRAPE_ROUTING_ARGS]` directly.
|
|
996
|
+
|
|
997
|
+
#### `endpoint_run_filters.grape` notification no longer fired for empty filter lists
|
|
998
|
+
|
|
999
|
+
`ActiveSupport::Notifications` subscribers listening to `endpoint_run_filters.grape` will no longer receive an event when the filter list for a given phase (`:before`, `:before_validation`, `:after_validation`, `:after`, `:finally`) is empty. Previously every phase emitted an event on every request regardless of whether any filters were registered. If you relied on these events to infer per-phase timing, subscribe to `endpoint_run.grape` (which always fires once per request) or register a no-op filter to keep the phase instrumented.
|
|
1000
|
+
|
|
1001
|
+
#### `Grape::Endpoint.before_each` moved to `Grape::Testing`
|
|
1002
|
+
|
|
1003
|
+
`Grape::Endpoint.before_each` and `Grape::Endpoint.reset_before_each` are now only available after requiring `grape/testing`. This module is intended for test environments only and is not loaded by default.
|
|
1004
|
+
|
|
1005
|
+
Add the following to your test helper:
|
|
1006
|
+
|
|
1007
|
+
```ruby
|
|
1008
|
+
require 'grape/testing'
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
The `before_each` method now always requires a block — calling it without one raises `ArgumentError`. To clear registered hooks, use the new dedicated `reset_before_each` method:
|
|
1012
|
+
|
|
1013
|
+
```ruby
|
|
1014
|
+
# Before
|
|
1015
|
+
after { Grape::Endpoint.before_each nil }
|
|
1016
|
+
|
|
1017
|
+
# After
|
|
1018
|
+
after { Grape::Endpoint.reset_before_each }
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
#### `Grape::Endpoint#logger` now returns the API's configured logger
|
|
1022
|
+
|
|
1023
|
+
Calling `logger` inside a route handler, filter (`before` / `before_validation` / `after_validation` / `after` / `finally`), or `rescue_from` block previously raised `NoMethodError` unless the application defined a helper:
|
|
1024
|
+
|
|
1025
|
+
```ruby
|
|
1026
|
+
class MyAPI < Grape::API
|
|
1027
|
+
logger Logger.new($stdout)
|
|
1028
|
+
|
|
1029
|
+
helpers do
|
|
1030
|
+
def logger
|
|
1031
|
+
MyAPI.logger
|
|
1032
|
+
end
|
|
1033
|
+
end
|
|
1034
|
+
end
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1037
|
+
`Grape::Endpoint` now exposes `#logger` directly, so the helper is no longer necessary:
|
|
1038
|
+
|
|
1039
|
+
```ruby
|
|
1040
|
+
class MyAPI < Grape::API
|
|
1041
|
+
logger Logger.new($stdout)
|
|
1042
|
+
# logger is now reachable inside route handlers, filters, and rescue_from blocks
|
|
1043
|
+
end
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
**Helper override still wins.** Helpers are mixed into the endpoint's singleton class via `singleton_class.include(@helpers)`, and singleton-class methods take precedence over instance methods on `Grape::Endpoint`. If your application already defines `logger` in a `helpers` block (or a module included via `helpers`), that definition continues to override `Endpoint#logger`. You can safely keep the helper or remove it — both paths produce the same result for the canonical `MyAPI.logger` case above.
|
|
1047
|
+
|
|
1048
|
+
**Behaviour change for code that didn't define a helper.** If your code references `logger` inside an endpoint context *without* a corresponding `helpers` definition, that call previously raised `NoMethodError` and now returns the API's configured logger. This is almost always the intended behaviour, but if you were relying on the `NoMethodError` (for instance to short-circuit logging in test environments via `rescue NoMethodError`), update your code to check `respond_to?(:logger)` or to gate logging on a feature flag.
|
|
1049
|
+
|
|
1050
|
+
#### Exceptions raised inside `rescue_from` blocks are now caught
|
|
1051
|
+
|
|
1052
|
+
Previously, an exception raised inside a `rescue_from` block was uncaught and bubbled up to Rack, producing the Rack default 500 page. The framework now catches and routes it:
|
|
1053
|
+
|
|
1054
|
+
1. If the re-raised exception's class has a registered `rescue_from` handler, that handler runs (one redispatch only — a second raise stops the chain).
|
|
1055
|
+
2. If the re-raised exception is a `Grape::Exceptions::Base` subclass, it is rendered via the default Grape error path with its own `status` and `message`.
|
|
1056
|
+
3. Otherwise, the original exception is exposed on `env['grape.exception']` for upstream Rack middleware to observe, and the response is a generic `Grape::Exceptions::InternalServerError` (`500 Internal Server Error`) — the original exception's message is **not** rendered to the API consumer.
|
|
1057
|
+
|
|
1058
|
+
This means deliberate re-raises in a `rescue_from` block (e.g. translating one exception class into another) now compose with the rest of your `rescue_from` configuration, and accidental crashes (typos, `NoMethodError`, …) no longer leak internal detail to API consumers.
|
|
1059
|
+
|
|
1060
|
+
The framework deliberately does **not** log unhandled internal exceptions itself — formatting and destination are application concerns. To log, forward to an error tracker, or customize the response shape for these errors, register a `rescue_from :internal_grape_exceptions` handler:
|
|
1061
|
+
|
|
1062
|
+
```ruby
|
|
1063
|
+
rescue_from :internal_grape_exceptions do |e|
|
|
1064
|
+
Sentry.capture_exception(e)
|
|
1065
|
+
error!({ message: 'Something went wrong' }, 500)
|
|
1066
|
+
end
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
When this handler is registered, the framework hands the original exception to you and you own the response shape entirely.
|
|
1070
|
+
|
|
1071
|
+
If you relied on the old behaviour and want raw exception messages exposed in development, register a catch-all handler:
|
|
1072
|
+
|
|
1073
|
+
```ruby
|
|
1074
|
+
rescue_from StandardError do |e|
|
|
1075
|
+
error!({ message: e.message, class: e.class.name }, 500)
|
|
1076
|
+
end
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
### Upgrading to >= 3.2
|
|
1081
|
+
|
|
1082
|
+
#### Rack parameter parsing errors now raise `Grape::Exceptions::RequestError`
|
|
1083
|
+
|
|
1084
|
+
Rack errors raised during parameter parsing (malformed multipart, parameter type conflicts, encoding issues, etc.) are now wrapped in `Grape::Exceptions::RequestError` instead of their previous specific exception classes (`Grape::Exceptions::EmptyMessageBody`, `Grape::Exceptions::TooManyMultipartFiles`, `Grape::Exceptions::TooDeepParameters`, `Grape::Exceptions::ConflictingTypes`, `Grape::Exceptions::InvalidParameters`). Those classes have been removed.
|
|
1085
|
+
|
|
1086
|
+
If you rescue any of these specific exceptions, update your rescue clauses to use `Grape::Exceptions::RequestError`:
|
|
1087
|
+
|
|
1088
|
+
```ruby
|
|
1089
|
+
# Before
|
|
1090
|
+
rescue Grape::Exceptions::ConflictingTypes, Grape::Exceptions::TooDeepParameters => e
|
|
1091
|
+
# ...
|
|
1092
|
+
|
|
1093
|
+
# After
|
|
1094
|
+
rescue Grape::Exceptions::RequestError => e
|
|
1095
|
+
# ...
|
|
1096
|
+
```
|
|
1097
|
+
|
|
1098
|
+
The error message is now forwarded directly from Rack rather than translated through Grape's locale system. On Rack 3, all Rack bad-request errors share the `Rack::BadRequest` marker module and are covered by a single rescue.
|
|
1099
|
+
|
|
1100
|
+
#### `endpoint_run_validators.grape` notification no longer fired when there are no validators
|
|
1101
|
+
|
|
1102
|
+
`ActiveSupport::Notifications` subscribers listening to `endpoint_run_validators.grape` will no longer receive an event for endpoints that have no validators. If you rely on this notification to measure every request, subscribe to `endpoint_run.grape` instead, which always fires.
|
|
1103
|
+
|
|
1104
|
+
#### Custom validators: use `default_message_key` and `validation_error!`
|
|
1105
|
+
|
|
1106
|
+
Validators are now instantiated once at definition time and frozen. Any setup should happen in `initialize`, not in `validate_param!`.
|
|
1107
|
+
|
|
1108
|
+
If your custom validator did work in `validate_param!` that only depends on the validator's options (not the param value), move it to `initialize`. A common case is compiling a value derived from options — for example, building a `Regexp`. Previously this may have been cached back into `@options`, which now raises `FrozenError` since `@options` and its nested values are deep-frozen by the base class:
|
|
1109
|
+
|
|
1110
|
+
**Before:**
|
|
1111
|
+
```ruby
|
|
1112
|
+
class MyValidator < Grape::Validations::Validators::Base
|
|
1113
|
+
def validate_param!(attr_name, params)
|
|
1114
|
+
# raises FrozenError: @options is frozen, cannot store compiled pattern back into it
|
|
1115
|
+
@options[:compiled] ||= Regexp.new(@options[:pattern])
|
|
1116
|
+
validation_error!(attr_name) unless params[attr_name].match?(@options[:compiled])
|
|
1117
|
+
end
|
|
1118
|
+
end
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
**After:**
|
|
1122
|
+
```ruby
|
|
1123
|
+
class MyValidator < Grape::Validations::Validators::Base
|
|
1124
|
+
def initialize(attrs, options, required, scope, opts)
|
|
1125
|
+
super
|
|
1126
|
+
@pattern = Regexp.new(@options[:pattern]).freeze
|
|
1127
|
+
end
|
|
1128
|
+
|
|
1129
|
+
def validate_param!(attr_name, params)
|
|
1130
|
+
validation_error!(attr_name) unless params[attr_name].match?(@pattern)
|
|
1131
|
+
end
|
|
1132
|
+
end
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1135
|
+
Any Array or Hash derived from options and stored in an ivar should be frozen, since the validator instance is shared across requests. `@options` itself (and any nested Hash/Array/String values within it) is deep-frozen by the base class, so mutations like `@options[:values] << 'extra'` will also raise a `FrozenError`.
|
|
1136
|
+
|
|
1137
|
+
#### Custom validators: rename `@option` to `@options`
|
|
1138
|
+
|
|
1139
|
+
The instance variable holding the validator's option value has been renamed from `@option` to `@options`. `@option` remains as an alias for backwards compatibility but will be removed in the next major release. Update any custom validators to use `@options` instead.
|
|
1140
|
+
|
|
1141
|
+
Several new helpers are available — see [Available helpers](README.md#available-helpers) in the README for full documentation and examples.
|
|
1142
|
+
|
|
1143
|
+
#### `with` now uses keyword arguments
|
|
1144
|
+
|
|
1145
|
+
The `with` DSL method now uses `**opts` instead of a positional hash. Calls using bare keyword syntax are unaffected:
|
|
1146
|
+
|
|
1147
|
+
```ruby
|
|
1148
|
+
# still works
|
|
1149
|
+
with(type: String, documentation: { in: 'body' }) { ... }
|
|
1150
|
+
```
|
|
1151
|
+
|
|
1152
|
+
However, passing an explicit hash literal will now raise an `ArgumentError`:
|
|
1153
|
+
|
|
1154
|
+
```ruby
|
|
1155
|
+
# raises ArgumentError
|
|
1156
|
+
with({ type: String }) { ... }
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
See [#2663](https://github.com/ruby-grape/grape/pull/2663) for more information.
|
|
1160
|
+
|
|
1161
|
+
#### Custom validators: use `translate` instead of `I18n` directly
|
|
1162
|
+
|
|
1163
|
+
`Grape::Util::Translation` is now included in `Grape::Validations::Validators::Base`. Custom validators that previously called `I18n.t` or `I18n.translate` directly should switch to the `translate`, which provides the same `:en` fallback logic used by all built-in validators.
|
|
1164
|
+
|
|
1165
|
+
Key points:
|
|
1166
|
+
- `scope` defaults to `'grape.errors.messages'` — no need to specify it for standard error message keys.
|
|
1167
|
+
- Interpolation variables are passed directly to I18n.
|
|
1168
|
+
- `format` is no longer needed — `translate` returns the fully interpolated string.
|
|
1169
|
+
|
|
1170
|
+
```ruby
|
|
1171
|
+
# Before
|
|
1172
|
+
raise Grape::Exceptions::Validation.new(
|
|
1173
|
+
params: [@scope.full_name(attr_name)],
|
|
1174
|
+
message: format(I18n.t(:my_key, scope: 'grape.errors.messages'), min: 2, max: 10)
|
|
1175
|
+
)
|
|
1176
|
+
|
|
1177
|
+
# After
|
|
1178
|
+
raise Grape::Exceptions::Validation.new(
|
|
1179
|
+
params: [@scope.full_name(attr_name)],
|
|
1180
|
+
message: translate(:my_key, min: 2, max: 10)
|
|
1181
|
+
)
|
|
1182
|
+
```
|
|
1183
|
+
|
|
1184
|
+
See [#2662](https://github.com/ruby-grape/grape/pull/2662) for more information.
|
|
1185
|
+
|
|
1186
|
+
### Upgrading to >= 3.1
|
|
1187
|
+
|
|
1188
|
+
#### Explicit kwargs for `namespace` and `route_param`
|
|
1189
|
+
|
|
1190
|
+
The `API#namespace` and `route_param` methods are now defined with `**options` instead of `options = {}`. In addtion, `requirements` in explicitly defined so it's not in `options` anymore. You can still call `requirements` like before but `options[:requirements]` will be empty. For `route_param`, `type` is also an explicit parameter so it's not in `options` anymore. See [#2647](https://github.com/ruby-grape/grape/pull/2647) for more information.
|
|
1191
|
+
|
|
1192
|
+
#### ParamsBuilder Grape::Extensions
|
|
1193
|
+
|
|
1194
|
+
Deprecated [ParamsBuilder's extensions](https://github.com/ruby-grape/grape/blob/master/UPGRADING.md#params-builder) have been removed.
|
|
1195
|
+
|
|
1196
|
+
#### Enhanced API compile!
|
|
1197
|
+
|
|
1198
|
+
Endpoints are now "compiled" instead of lazy loaded. Historically, when calling `YourAPI.compile!` in `config.ru` (or just receiving the first API call), only routing was compiled see [Grape::Router#compile!](https://github.com/ruby-grape/grape/blob/bf90e95c3b17c415c944363b1c07eb9727089ee7/lib/grape/router.rb#L41-L54) and endpoints were lazy loaded. Now, it's part of the API compilation. See [#2645](https://github.com/ruby-grape/grape/pull/2645) for more information.
|
|
1199
|
+
|
|
1200
|
+
### Upgrading to >= 3.0.0
|
|
1201
|
+
|
|
1202
|
+
#### Ruby 3+ Argument Delegation Modernization
|
|
1203
|
+
|
|
1204
|
+
Grape has been modernized to use Ruby 3+'s preferred argument delegation patterns. This change replaces `args.extract_options!` with explicit `**kwargs` parameters throughout the codebase.
|
|
1205
|
+
|
|
1206
|
+
- All DSL methods now use explicit keyword arguments (`**kwargs`) instead of extracting options from mixed argument lists
|
|
1207
|
+
- Method signatures are now more explicit and follow Ruby 3+ best practices
|
|
1208
|
+
- The `active_support/core_ext/array/extract_options` dependency has been removed
|
|
1209
|
+
|
|
1210
|
+
Passing the options of `requires`, `optional` and `use` as a trailing positional Hash still works, but is deprecated:
|
|
1211
|
+
|
|
1212
|
+
```ruby
|
|
1213
|
+
params do
|
|
1214
|
+
requires :id, { type: Integer } # deprecated
|
|
1215
|
+
requires :id, type: Integer # do this instead
|
|
1216
|
+
end
|
|
1217
|
+
```
|
|
1218
|
+
|
|
1219
|
+
Under `extract_options!` the braces made no difference. They do now: Ruby only turns a trailing Hash into keyword arguments when it is written without braces, so a braced Hash lands in the splat and would otherwise be taken for a parameter name.
|
|
1220
|
+
|
|
1221
|
+
See [#2618](https://github.com/ruby-grape/grape/pull/2618) for more information.
|
|
1222
|
+
|
|
1223
|
+
#### Configuration API Migration from ActiveSupport::Configurable to Dry::Configurable
|
|
1224
|
+
|
|
1225
|
+
Grape has migrated from `ActiveSupport::Configurable` to `Dry::Configurable` for its configuration system since its [deprecated](https://github.com/rails/rails/blob/1cdd190a25e483b65f1f25bbd0f13a25d696b461/activesupport/lib/active_support/configurable.rb#L3-L7).
|
|
1226
|
+
|
|
1227
|
+
See [#2617](https://github.com/ruby-grape/grape/pull/2617) for more information.
|
|
1228
|
+
|
|
1229
|
+
#### Endpoint execution simplified and `return` deprecated
|
|
1230
|
+
|
|
1231
|
+
Executing a endpoint's block has been simplified and calling `return` in it has been deprecated. Use `next` instead.
|
|
1232
|
+
|
|
1233
|
+
See [#2577](https://github.com/ruby-grape/grape/pull/2577) for more information.
|
|
1234
|
+
|
|
1235
|
+
#### Old Deprecations Clean Up
|
|
1236
|
+
|
|
1237
|
+
- `rack_response` has been removed in favor of using `error!`.
|
|
1238
|
+
- `Grape::Exceptions::MissingGroupType` and `Grape::Exceptions::UnsupportedGroupType` aliases `MissingGroupTypeError and `UnsupportedGroupType` have been removed.
|
|
1239
|
+
- `Grape::Validations::Base` has been removed in favor of `Grape::Validations::Validators::Base`.
|
|
1240
|
+
|
|
1241
|
+
See [2573](https://github.com/ruby-grape/grape/pull/2573) for more information.
|
|
1242
|
+
|
|
1243
|
+
### Upgrading to >= 2.4.0
|
|
1244
|
+
|
|
1245
|
+
#### Grape::Middleware::Auth::Base
|
|
1246
|
+
`type` is now validated at compile time and will raise a `Grape::Exceptions::UnknownAuthStrategy` if unknown.
|
|
1247
|
+
|
|
1248
|
+
#### Grape::Middleware::Base
|
|
1249
|
+
|
|
1250
|
+
- Second argument `options` is now a double splat (**) instead of single splat (*). If you're redefining `initialize` in your middleware and/or calling `super` in it, you might have to adapt the signature and the `super` call. Also, you might have to remove `{}` if you're pass `options` as a literal `Hash` or add `**` if you're using a variable.
|
|
1251
|
+
- `Grape::Middleware::Helpers` has been removed. The equivalent method `context` is now part of `Grape::Middleware::Base`.
|
|
1252
|
+
|
|
1253
|
+
#### Grape::Http::Headers, Grape::Util::Lazy::Object
|
|
1254
|
+
|
|
1255
|
+
Both have been removed. See [2554](https://github.com/ruby-grape/grape/pull/2554).
|
|
1256
|
+
Here are the notable changes:
|
|
1257
|
+
|
|
1258
|
+
- Constants like `HTTP_ACCEPT` have been replaced by their literal value.
|
|
1259
|
+
- `SUPPORTED_METHODS` has been moved to `Grape` module.
|
|
1260
|
+
- `HTTP_HEADERS` has been moved to `Grape::Request` and renamed `KNOWN_HEADERS`. The last has been refreshed with new headers, and it's not lazy anymore.
|
|
1261
|
+
- `SUPPORTED_METHODS_WITHOUT_OPTIONS` and `find_supported_method` have been removed.
|
|
1262
|
+
|
|
1263
|
+
#### Grape::Middleware::Base
|
|
1264
|
+
|
|
1265
|
+
- Constant `TEXT_HTML` has been removed in favor of using literal string 'text/html'.
|
|
1266
|
+
- `rack_request` and `query_params` have been added. Feel free to call these in your middlewares.
|
|
1267
|
+
|
|
1268
|
+
#### Params Builder
|
|
1269
|
+
|
|
1270
|
+
- Passing a class to `build_with` or `Grape.config.param_builder` has been deprecated in favor of a symbolized short_name. See `SHORTNAME_LOOKUP` in [params_builder](lib/grape/params_builder.rb).
|
|
1271
|
+
- Including Grape's extensions like `Grape::Extensions::Hashie::Mash::ParamBuilder` has been deprecated in favor of using `build_with` at the route level.
|
|
1272
|
+
|
|
1273
|
+
#### Accept Header Negotiation Harmonized
|
|
1274
|
+
|
|
1275
|
+
[Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept) header is now fully interpreted through `Rack::Utils.best_q_match` which is following [RFC2616 14.1](https://datatracker.ietf.org/doc/html/rfc2616#section-14.1). Since [Grape 2.1.0](https://github.com/ruby-grape/grape/blob/master/CHANGELOG.md#210-20240615), the [header versioning strategy](https://github.com/ruby-grape/grape?tab=readme-ov-file#header) was adhering to it, but `Grape::Middleware::Formatter` never did.
|
|
1276
|
+
|
|
1277
|
+
Your API might act differently since it will strictly follow the [RFC2616 14.1](https://datatracker.ietf.org/doc/html/rfc2616#section-14.1) when interpreting the `Accept` header. Here are the differences:
|
|
1278
|
+
|
|
1279
|
+
##### Invalid or missing quality ranking
|
|
1280
|
+
The following used to yield `application/xml` and now will yield `application/json` as the preferred media type:
|
|
1281
|
+
- `application/json;q=invalid,application/xml;q=0.5`
|
|
1282
|
+
- `application/json,application/xml;q=1.0`
|
|
1283
|
+
|
|
1284
|
+
For the invalid case, the value `invalid` was automatically `to_f` and `invalid.to_f` equals `0.0`. Now, since it doesn't match [Rack's regex](https://github.com/rack/rack/blob/3-1-stable/lib/rack/utils.rb#L138), its interpreted as non provided and its quality ranking equals 1.0.
|
|
1285
|
+
|
|
1286
|
+
For the non provided case, 1.0 was automatically assigned and in a case of multiple best matches, the first was returned based on Ruby's sort_by `quality`. Now, 1.0 is still assigned and the last is returned in case of multiple best matches. See [Rack's implementation](https://github.com/rack/rack/blob/e8f47608668d507e0f231a932fa37c9ca551c0a5/lib/rack/utils.rb#L167) of the RFC.
|
|
1287
|
+
|
|
1288
|
+
##### Considering the closest generic when vendor tree
|
|
1289
|
+
Excluding the [header versioning strategy](https://github.com/ruby-grape/grape?tab=readme-ov-file#header), whenever a media type with the [vendor tree](https://datatracker.ietf.org/doc/html/rfc6838#section-3.2) leading facet `vnd.` like `application/vnd.api+json` was provided, Grape would also consider its closest generic when negotiating. In that case, `application/json` was added to the negotiation. Now, it will just consider the provided media types without considering any closest generics, and you'll need to [register](https://github.com/ruby-grape/grape?tab=readme-ov-file#api-formats) it.
|
|
1290
|
+
You can find the official vendor tree registrations on [IANA](https://www.iana.org/assignments/media-types/media-types.xhtml)
|
|
1291
|
+
|
|
1292
|
+
#### Custom Validators
|
|
1293
|
+
|
|
1294
|
+
If you now receive an error of `'Grape::Validations.require_validator': unknown validator: your_custom_validation (Grape::Exceptions::UnknownValidator)` after upgrading to 2.4.0 then you will need to ensure that you require the `your_custom_validation` file before your Grape API code is loaded.
|
|
1295
|
+
|
|
1296
|
+
See [2533](https://github.com/ruby-grape/grape/issues/2533) for more information.
|
|
1297
|
+
|
|
1298
|
+
### Upgrading to >= 2.3.0
|
|
1299
|
+
|
|
1300
|
+
### `content_type` vs `api.format` inside API
|
|
1301
|
+
|
|
1302
|
+
Before 2.3.0, `content_type` had priority over `env['api.format']` when set in an API, which was incorrect. The priority has been flipped and `env['api.format']` will be checked first.
|
|
1303
|
+
In addition, the function `api_format` has been added. Instead of setting `env['api.format']` directly, you can call `api_format`.
|
|
1304
|
+
See [#2506](https://github.com/ruby-grape/grape/pull/2506) for more information.
|
|
1305
|
+
|
|
1306
|
+
#### Remove Deprecated Methods and Options
|
|
1307
|
+
|
|
1308
|
+
- Deprecated `file` method has been removed. Use `send_file` or `stream`.
|
|
1309
|
+
See [#2500](https://github.com/ruby-grape/grape/pull/2500) for more information.
|
|
1310
|
+
|
|
1311
|
+
- The `except` and `proc` options have been removed from the `values` validator. Use `except_values` validator or assign `proc` directly to `values`.
|
|
1312
|
+
See [#2501](https://github.com/ruby-grape/grape/pull/2501) for more information.
|
|
1313
|
+
|
|
1314
|
+
- `Passing an options hash and a block to 'desc'` deprecation has been removed. Move all hash options to block instead.
|
|
1315
|
+
See [#2502](https://github.com/ruby-grape/grape/pull/2502) for more information.
|
|
1316
|
+
|
|
1317
|
+
### Upgrading to >= 2.2.0
|
|
1318
|
+
|
|
1319
|
+
### `Length` validator
|
|
1320
|
+
|
|
1321
|
+
After Grape 2.2.0, `length` validator will only take effect for parameters with types that support `#length` method, will not throw `ArgumentError` exception.
|
|
1322
|
+
|
|
1323
|
+
See [#2464](https://github.com/ruby-grape/grape/pull/2464) for more information.
|
|
1324
|
+
|
|
1325
|
+
### Upgrading to >= 2.1.0
|
|
1326
|
+
|
|
1327
|
+
#### Optional Builder
|
|
1328
|
+
|
|
1329
|
+
The `builder` gem dependency has been made optional as it's only used when generating XML. If your code does, add `builder` to your `Gemfile`.
|
|
1330
|
+
|
|
1331
|
+
See [#2445](https://github.com/ruby-grape/grape/pull/2445) for more information.
|
|
1332
|
+
|
|
1333
|
+
#### Deep Merging of Parameter Attributes
|
|
1334
|
+
|
|
1335
|
+
Grape now uses `deep_merge` to combine parameter attributes within the `with` method. Previously, attributes defined at the parameter level would override those defined at the group level.
|
|
1336
|
+
With deep merge, attributes are now combined, allowing for more detailed and nuanced API specifications.
|
|
1337
|
+
|
|
1338
|
+
For example:
|
|
1339
|
+
|
|
1340
|
+
```ruby
|
|
1341
|
+
with(documentation: { in: 'body' }) do
|
|
1342
|
+
optional :vault, documentation: { default: 33 }
|
|
1343
|
+
end
|
|
1344
|
+
```
|
|
1345
|
+
|
|
1346
|
+
Before it was equivalent to:
|
|
1347
|
+
|
|
1348
|
+
```ruby
|
|
1349
|
+
optional :vault, documentation: { default: 33 }
|
|
1350
|
+
```
|
|
1351
|
+
|
|
1352
|
+
After it is an equivalent of:
|
|
1353
|
+
|
|
1354
|
+
```ruby
|
|
1355
|
+
optional :vault, documentation: { in: 'body', default: 33 }
|
|
1356
|
+
```
|
|
1357
|
+
|
|
1358
|
+
See [#2432](https://github.com/ruby-grape/grape/pull/2432) for more information.
|
|
1359
|
+
|
|
1360
|
+
#### Zeitwerk
|
|
1361
|
+
|
|
1362
|
+
Grape's autoloader has been updated and it's now based on [Zeitwerk](https://github.com/fxn/zeitwerk).
|
|
1363
|
+
If you MP (Monkey Patch) some files and you're not following the [file structure](https://github.com/fxn/zeitwerk?tab=readme-ov-file#file-structure), you might end up with a Zeitwerk error.
|
|
1364
|
+
|
|
1365
|
+
See [#2363](https://github.com/ruby-grape/grape/pull/2363) for more information.
|
|
1366
|
+
|
|
1367
|
+
#### Changes in rescue_from
|
|
1368
|
+
|
|
1369
|
+
The `rack_response` method has been deprecated and the `error_response` method has been removed. Use `error!` instead.
|
|
1370
|
+
|
|
1371
|
+
See [#2414](https://github.com/ruby-grape/grape/pull/2414) for more information.
|
|
1372
|
+
|
|
1373
|
+
#### Change in parameters precedence
|
|
1374
|
+
|
|
1375
|
+
When using together with `Grape::Extensions::Hash::ParamBuilder`, `route_param` takes higher precedence over a regular parameter defined with same name, which now matches the default param builder behavior.
|
|
1376
|
+
|
|
1377
|
+
This was a regression introduced by [#2326](https://github.com/ruby-grape/grape/pull/2326) in Grape v1.8.0.
|
|
1378
|
+
|
|
1379
|
+
```ruby
|
|
1380
|
+
Grape.configure do |config|
|
|
1381
|
+
config.param_builder = Grape::Extensions::Hash::ParamBuilder
|
|
1382
|
+
end
|
|
1383
|
+
|
|
1384
|
+
params do
|
|
1385
|
+
requires :foo, type: String
|
|
1386
|
+
end
|
|
1387
|
+
route_param :foo do
|
|
1388
|
+
get do
|
|
1389
|
+
{ value: params[:foo] }
|
|
1390
|
+
end
|
|
1391
|
+
end
|
|
1392
|
+
```
|
|
1393
|
+
|
|
1394
|
+
Request:
|
|
1395
|
+
|
|
1396
|
+
```bash
|
|
1397
|
+
curl -X POST -H "Content-Type: application/json" localhost:9292/bar -d '{"foo": "baz"}'
|
|
1398
|
+
```
|
|
1399
|
+
|
|
1400
|
+
Response prior to v1.8.0:
|
|
1401
|
+
|
|
1402
|
+
```json
|
|
1403
|
+
{
|
|
1404
|
+
"value": "bar"
|
|
1405
|
+
}
|
|
1406
|
+
```
|
|
1407
|
+
|
|
1408
|
+
v1.8.0..v2.0.0:
|
|
1409
|
+
|
|
1410
|
+
```json
|
|
1411
|
+
{
|
|
1412
|
+
"value": "baz"
|
|
1413
|
+
}
|
|
1414
|
+
```
|
|
1415
|
+
|
|
1416
|
+
v2.1.0+:
|
|
1417
|
+
|
|
1418
|
+
```json
|
|
1419
|
+
{
|
|
1420
|
+
"value": "bar"
|
|
1421
|
+
}
|
|
1422
|
+
```
|
|
1423
|
+
|
|
1424
|
+
See [#2378](https://github.com/ruby-grape/grape/pull/2378) for details.
|
|
1425
|
+
|
|
1426
|
+
#### Grape::Router::Route.route_xxx methods have been removed
|
|
1427
|
+
|
|
1428
|
+
- `route_method` is accessible through `request_method`
|
|
1429
|
+
- `route_path` is accessible through `path`
|
|
1430
|
+
- Any other `route_xyz` are accessible through `options[xyz]`
|
|
1431
|
+
|
|
1432
|
+
#### Instance variables scope
|
|
1433
|
+
|
|
1434
|
+
Due to the changes done in [#2377](https://github.com/ruby-grape/grape/pull/2377), the instance variables defined inside each of the endpoints (or inside a `before` validator) are now accessible inside the `rescue_from`. The behavior of the instance variables was undefined until `2.1.0`.
|
|
1435
|
+
|
|
1436
|
+
If you were using the same variable name defined inside an endpoint or `before` validator inside a `rescue_from` handler, you need to take in mind that you can start getting different values or you can be overriding values.
|
|
1437
|
+
|
|
1438
|
+
Before:
|
|
1439
|
+
```ruby
|
|
1440
|
+
class TwitterAPI < Grape::API
|
|
1441
|
+
before do
|
|
1442
|
+
@var = 1
|
|
1443
|
+
end
|
|
1444
|
+
|
|
1445
|
+
get '/' do
|
|
1446
|
+
puts @var # => 1
|
|
1447
|
+
raise
|
|
1448
|
+
end
|
|
1449
|
+
|
|
1450
|
+
rescue_from :all do
|
|
1451
|
+
puts @var # => nil
|
|
1452
|
+
end
|
|
1453
|
+
end
|
|
1454
|
+
```
|
|
1455
|
+
|
|
1456
|
+
After:
|
|
1457
|
+
```ruby
|
|
1458
|
+
class TwitterAPI < Grape::API
|
|
1459
|
+
before do
|
|
1460
|
+
@var = 1
|
|
1461
|
+
end
|
|
1462
|
+
|
|
1463
|
+
get '/' do
|
|
1464
|
+
puts @var # => 1
|
|
1465
|
+
raise
|
|
1466
|
+
end
|
|
1467
|
+
|
|
1468
|
+
rescue_from :all do
|
|
1469
|
+
puts @var # => 1
|
|
1470
|
+
end
|
|
1471
|
+
end
|
|
1472
|
+
```
|
|
1473
|
+
|
|
1474
|
+
#### Recognizing Path
|
|
1475
|
+
|
|
1476
|
+
Grape now considers the types of the configured `route_params` in order to determine the endpoint that matches with the performed request.
|
|
1477
|
+
|
|
1478
|
+
So taking into account this `Grape::API` class
|
|
1479
|
+
|
|
1480
|
+
```ruby
|
|
1481
|
+
class Books < Grape::API
|
|
1482
|
+
resource :books do
|
|
1483
|
+
route_param :id, type: Integer do
|
|
1484
|
+
# GET /books/:id
|
|
1485
|
+
get do
|
|
1486
|
+
#...
|
|
1487
|
+
end
|
|
1488
|
+
end
|
|
1489
|
+
|
|
1490
|
+
resource :share do
|
|
1491
|
+
# POST /books/share
|
|
1492
|
+
post do
|
|
1493
|
+
# ....
|
|
1494
|
+
end
|
|
1495
|
+
end
|
|
1496
|
+
end
|
|
1497
|
+
end
|
|
1498
|
+
```
|
|
1499
|
+
|
|
1500
|
+
Before:
|
|
1501
|
+
```ruby
|
|
1502
|
+
API.recognize_path '/books/1' # => /books/:id
|
|
1503
|
+
API.recognize_path '/books/share' # => /books/:id
|
|
1504
|
+
API.recognize_path '/books/other' # => /books/:id
|
|
1505
|
+
```
|
|
1506
|
+
|
|
1507
|
+
After:
|
|
1508
|
+
```ruby
|
|
1509
|
+
API.recognize_path '/books/1' # => /books/:id
|
|
1510
|
+
API.recognize_path '/books/share' # => /books/share
|
|
1511
|
+
API.recognize_path '/books/other' # => nil
|
|
1512
|
+
```
|
|
1513
|
+
|
|
1514
|
+
This implies that before this changes, when you performed `/books/other` and it matched with the `/books/:id` endpoint, you get a `400 Bad Request` response because the type of the provided `:id` param was not an `Integer`. However, after upgrading to version `2.1.0` you will get a `404 Not Found` response, because there is not a defined endpoint that matches with `/books/other`.
|
|
1515
|
+
|
|
1516
|
+
See [#2379](https://github.com/ruby-grape/grape/pull/2379) for more information.
|
|
1517
|
+
|
|
1518
|
+
### Upgrading to >= 2.0.0
|
|
1519
|
+
|
|
1520
|
+
#### Headers
|
|
1521
|
+
|
|
1522
|
+
As per [rack/rack#1592](https://github.com/rack/rack/issues/1592) Rack 3 is following the HTTP/2+ semantics which require header names to be lower case. To avoid compatibility issues, starting with Grape 1.9.0, headers will be cased based on what version of Rack you are using.
|
|
1523
|
+
|
|
1524
|
+
Given this request:
|
|
1525
|
+
|
|
1526
|
+
```shell
|
|
1527
|
+
curl -H "Content-Type: application/json" -H "Secret-Password: foo" ...
|
|
1528
|
+
```
|
|
1529
|
+
|
|
1530
|
+
If you are using Rack 3 in your application then the headers will be set to:
|
|
1531
|
+
|
|
1532
|
+
```ruby
|
|
1533
|
+
{ "content-type" => "application/json", "secret-password" => "foo"}
|
|
1534
|
+
```
|
|
1535
|
+
|
|
1536
|
+
This means if you are checking for header values in your application, you would need to change your code to use downcased keys.
|
|
1537
|
+
|
|
1538
|
+
```ruby
|
|
1539
|
+
get do
|
|
1540
|
+
# This would use headers['Secret-Password'] in Rack < 3
|
|
1541
|
+
error!('Unauthorized', 401) unless headers['secret-password'] == 'swordfish'
|
|
1542
|
+
end
|
|
1543
|
+
```
|
|
1544
|
+
|
|
1545
|
+
See [#2355](https://github.com/ruby-grape/grape/pull/2355) for more information.
|
|
1546
|
+
|
|
1547
|
+
#### Digest auth deprecation
|
|
1548
|
+
|
|
1549
|
+
Digest auth has been removed along with the deprecation of `Rack::Auth::Digest` in Rack 3.
|
|
1550
|
+
|
|
1551
|
+
See [#2294](https://github.com/ruby-grape/grape/issues/2294) for more information.
|
|
1552
|
+
|
|
4
1553
|
### Upgrading to >= 1.7.0
|
|
5
1554
|
|
|
6
1555
|
#### Exceptions renaming
|
|
@@ -230,7 +1779,7 @@ class Api < Grape::API
|
|
|
230
1779
|
params[:my_param]
|
|
231
1780
|
end
|
|
232
1781
|
get '/example', params: { my_param: nil }
|
|
233
|
-
# 1.3.
|
|
1782
|
+
# 1.3.3 = []
|
|
234
1783
|
# 1.3.2 = nil
|
|
235
1784
|
end
|
|
236
1785
|
```
|
|
@@ -439,8 +1988,7 @@ end
|
|
|
439
1988
|
|
|
440
1989
|
##### `name` (and other caveats) of the mounted API
|
|
441
1990
|
|
|
442
|
-
After the patch, the mounted API is no longer a Named class inheriting from `Grape::API`, it is an anonymous class
|
|
443
|
-
which inherit from `Grape::API::Instance`.
|
|
1991
|
+
After the patch, the mounted API is no longer a Named class inheriting from `Grape::API`, it is an anonymous class which inherit from `Grape::API::Instance`.
|
|
444
1992
|
|
|
445
1993
|
What this means in practice, is:
|
|
446
1994
|
|
|
@@ -820,8 +2368,7 @@ See [#1114](https://github.com/ruby-grape/grape/pull/1114) for more information.
|
|
|
820
2368
|
|
|
821
2369
|
#### Bypasses formatters when status code indicates no content
|
|
822
2370
|
|
|
823
|
-
To be consistent with rack and it's handling of standard responses associated with no content, both default and custom formatters will now
|
|
824
|
-
be bypassed when processing responses for status codes defined [by rack](https://github.com/rack/rack/blob/master/lib/rack/utils.rb#L567)
|
|
2371
|
+
To be consistent with rack and it's handling of standard responses associated with no content, both default and custom formatters will now be bypassed when processing responses for status codes defined [by rack](https://github.com/rack/rack/blob/master/lib/rack/utils.rb#L567)
|
|
825
2372
|
|
|
826
2373
|
See [#1190](https://github.com/ruby-grape/grape/pull/1190) for more information.
|
|
827
2374
|
|
|
@@ -1262,8 +2809,7 @@ As replacement can be used
|
|
|
1262
2809
|
* `Grape::Middleware::Auth::Digest` => [`Rack::Auth::Digest::MD5`](https://github.com/rack/rack/blob/master/lib/rack/auth/digest/md5.rb)
|
|
1263
2810
|
* `Grape::Middleware::Auth::OAuth2` => [warden-oauth2](https://github.com/opperator/warden-oauth2) or [rack-oauth2](https://github.com/nov/rack-oauth2)
|
|
1264
2811
|
|
|
1265
|
-
If this is not possible you can extract the middleware files from [grape v0.7.0](https://github.com/ruby-grape/grape/tree/v0.7.0/lib/grape/middleware/auth)
|
|
1266
|
-
and host these files within your application
|
|
2812
|
+
If this is not possible you can extract the middleware files from [grape v0.7.0](https://github.com/ruby-grape/grape/tree/v0.7.0/lib/grape/middleware/auth) and host these files within your application
|
|
1267
2813
|
|
|
1268
2814
|
See [#703](https://github.com/ruby-grape/Grape/pull/703) for more information.
|
|
1269
2815
|
|