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.
Files changed (317) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +517 -1
  3. data/CONTRIBUTING.md +19 -11
  4. data/README.md +841 -545
  5. data/UPGRADING.md +1553 -7
  6. data/grape.gemspec +11 -14
  7. data/lib/grape/api/instance.rb +77 -160
  8. data/lib/grape/api.rb +75 -107
  9. data/lib/grape/content_types.rb +67 -9
  10. data/lib/grape/cookies.rb +28 -25
  11. data/lib/grape/declared_params_handler.rb +116 -0
  12. data/lib/grape/dry_types.rb +48 -6
  13. data/lib/grape/dsl/callbacks.rb +8 -58
  14. data/lib/grape/dsl/declared.rb +35 -0
  15. data/lib/grape/dsl/desc.rb +25 -63
  16. data/lib/grape/dsl/entity.rb +106 -0
  17. data/lib/grape/dsl/headers.rb +2 -2
  18. data/lib/grape/dsl/helpers.rb +83 -64
  19. data/lib/grape/dsl/inside_route.rb +69 -296
  20. data/lib/grape/dsl/logger.rb +4 -9
  21. data/lib/grape/dsl/middleware.rb +22 -40
  22. data/lib/grape/dsl/parameters.rb +88 -94
  23. data/lib/grape/dsl/request_response.rb +143 -147
  24. data/lib/grape/dsl/rescue_options.rb +25 -0
  25. data/lib/grape/dsl/routing.rb +333 -191
  26. data/lib/grape/dsl/settings.rb +29 -135
  27. data/lib/grape/dsl/validations.rb +39 -32
  28. data/lib/grape/dsl/version_options.rb +23 -0
  29. data/lib/grape/endpoint/options.rb +25 -0
  30. data/lib/grape/endpoint.rb +319 -269
  31. data/lib/grape/{util/env.rb → env.rb} +8 -8
  32. data/lib/grape/error_formatter/base.rb +57 -21
  33. data/lib/grape/error_formatter/json.rb +13 -15
  34. data/lib/grape/error_formatter/serializable_hash.rb +7 -0
  35. data/lib/grape/error_formatter/txt.rb +12 -18
  36. data/lib/grape/error_formatter/xml.rb +3 -13
  37. data/lib/grape/error_formatter.rb +9 -25
  38. data/lib/grape/exceptions/base.rb +22 -58
  39. data/lib/grape/exceptions/error_response.rb +48 -0
  40. data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
  41. data/lib/grape/exceptions/internal_server_error.rb +16 -0
  42. data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
  43. data/lib/grape/exceptions/invalid_formatter.rb +1 -1
  44. data/lib/grape/exceptions/invalid_message_body.rb +1 -1
  45. data/lib/grape/exceptions/invalid_version_header.rb +1 -1
  46. data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
  47. data/lib/grape/exceptions/method_not_allowed.rb +1 -1
  48. data/lib/grape/exceptions/missing_group_type.rb +0 -2
  49. data/lib/grape/exceptions/missing_mime_type.rb +1 -1
  50. data/lib/grape/exceptions/request_error.rb +11 -0
  51. data/lib/grape/exceptions/unknown_auth_strategy.rb +11 -0
  52. data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
  53. data/lib/grape/exceptions/unknown_parameter.rb +1 -1
  54. data/lib/grape/exceptions/unknown_params_builder.rb +11 -0
  55. data/lib/grape/exceptions/unknown_validator.rb +1 -1
  56. data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
  57. data/lib/grape/exceptions/validation.rb +28 -11
  58. data/lib/grape/exceptions/validation_array_errors.rb +5 -0
  59. data/lib/grape/exceptions/validation_errors.rb +22 -26
  60. data/lib/grape/formatter/base.rb +16 -0
  61. data/lib/grape/formatter/json.rb +5 -6
  62. data/lib/grape/formatter/serializable_hash.rb +7 -10
  63. data/lib/grape/formatter/txt.rb +3 -5
  64. data/lib/grape/formatter/xml.rb +4 -6
  65. data/lib/grape/formatter.rb +7 -25
  66. data/lib/grape/json.rb +46 -0
  67. data/lib/grape/locale/en.yml +44 -42
  68. data/lib/grape/middleware/auth/base.rb +11 -33
  69. data/lib/grape/middleware/auth/dsl.rb +12 -34
  70. data/lib/grape/middleware/auth/strategies.rb +1 -2
  71. data/lib/grape/middleware/base.rb +56 -33
  72. data/lib/grape/middleware/error.rb +290 -91
  73. data/lib/grape/middleware/formatter.rb +164 -106
  74. data/lib/grape/middleware/precomputed_content_types.rb +51 -0
  75. data/lib/grape/middleware/stack.rb +38 -40
  76. data/lib/grape/middleware/versioner/accept_version_header.rb +6 -33
  77. data/lib/grape/middleware/versioner/base.rb +66 -0
  78. data/lib/grape/middleware/versioner/header.rb +44 -129
  79. data/lib/grape/middleware/versioner/param.rb +4 -25
  80. data/lib/grape/middleware/versioner/path.rb +48 -25
  81. data/lib/grape/middleware/versioner.rb +7 -14
  82. data/lib/grape/mountable.rb +22 -0
  83. data/lib/grape/namespace.rb +21 -14
  84. data/lib/grape/params_builder/base.rb +20 -0
  85. data/lib/grape/params_builder/hash.rb +11 -0
  86. data/lib/grape/params_builder/hash_with_indifferent_access.rb +11 -0
  87. data/lib/grape/params_builder/hashie_mash.rb +11 -0
  88. data/lib/grape/params_builder.rb +15 -0
  89. data/lib/grape/parser/base.rb +16 -0
  90. data/lib/grape/parser/json.rb +6 -8
  91. data/lib/grape/parser/xml.rb +6 -8
  92. data/lib/grape/parser.rb +5 -23
  93. data/lib/grape/path.rb +8 -94
  94. data/lib/grape/precompiled_json.rb +50 -0
  95. data/lib/grape/railtie.rb +9 -0
  96. data/lib/grape/request.rb +200 -26
  97. data/lib/grape/router/base_route.rb +85 -0
  98. data/lib/grape/router/greedy_route.rb +30 -0
  99. data/lib/grape/router/mustermann_pattern.rb +44 -0
  100. data/lib/grape/router/pattern/path.rb +78 -0
  101. data/lib/grape/router/pattern.rb +77 -35
  102. data/lib/grape/router/route.rb +79 -60
  103. data/lib/grape/router.rb +128 -104
  104. data/lib/grape/serve_stream/file_body.rb +7 -0
  105. data/lib/grape/serve_stream/sendfile_response.rb +3 -5
  106. data/lib/grape/serve_stream/stream_response.rb +7 -0
  107. data/lib/grape/testing.rb +33 -0
  108. data/lib/grape/util/api_description.rb +67 -0
  109. data/lib/grape/util/cache.rb +22 -5
  110. data/lib/grape/util/deep_freeze.rb +34 -0
  111. data/lib/grape/util/endpoint_configuration.rb +1 -1
  112. data/lib/grape/util/freeze_on_new.rb +20 -0
  113. data/lib/grape/util/header.rb +13 -0
  114. data/lib/grape/util/inheritable_setting.rb +777 -39
  115. data/lib/grape/util/lazy/base.rb +16 -0
  116. data/lib/grape/util/lazy/block.rb +22 -0
  117. data/lib/grape/util/lazy/value.rb +31 -0
  118. data/lib/grape/util/lazy/value_array.rb +21 -0
  119. data/lib/grape/util/lazy/value_enumerable.rb +31 -0
  120. data/lib/grape/util/lazy/value_hash.rb +21 -0
  121. data/lib/grape/util/media_type.rb +74 -0
  122. data/lib/grape/util/path_normalizer.rb +37 -0
  123. data/lib/grape/util/registry.rb +37 -0
  124. data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
  125. data/lib/grape/util/stackable_values.rb +40 -16
  126. data/lib/grape/util/translation.rb +42 -0
  127. data/lib/grape/validations/attributes_iterator.rb +61 -28
  128. data/lib/grape/validations/coerce_options.rb +21 -0
  129. data/lib/grape/validations/contract_scope.rb +29 -0
  130. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  131. data/lib/grape/validations/oneof_collector.rb +35 -0
  132. data/lib/grape/validations/param_scope_tracker.rb +62 -0
  133. data/lib/grape/validations/params_documentation.rb +52 -0
  134. data/lib/grape/validations/params_scope.rb +223 -306
  135. data/lib/grape/validations/shared_options.rb +19 -0
  136. data/lib/grape/validations/single_attribute_iterator.rb +6 -4
  137. data/lib/grape/validations/types/array_coercer.rb +7 -12
  138. data/lib/grape/validations/types/custom_type_coercer.rb +47 -85
  139. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  140. data/lib/grape/validations/types/dry_type_coercer.rb +17 -28
  141. data/lib/grape/validations/types/json.rb +1 -5
  142. data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
  143. data/lib/grape/validations/types/primitive_coercer.rb +14 -35
  144. data/lib/grape/validations/types/set_coercer.rb +1 -4
  145. data/lib/grape/validations/types/variant_collection_coercer.rb +16 -3
  146. data/lib/grape/validations/types.rb +29 -54
  147. data/lib/grape/validations/validations_spec.rb +164 -0
  148. data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
  149. data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
  150. data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
  151. data/lib/grape/validations/validators/base.rb +118 -37
  152. data/lib/grape/validations/validators/coerce_validator.rb +26 -38
  153. data/lib/grape/validations/validators/contract_scope_validator.rb +46 -0
  154. data/lib/grape/validations/validators/default_validator.rb +13 -16
  155. data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
  156. data/lib/grape/validations/validators/except_values_validator.rb +15 -5
  157. data/lib/grape/validations/validators/length_validator.rb +50 -0
  158. data/lib/grape/validations/validators/multiple_params_base.rb +12 -9
  159. data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
  160. data/lib/grape/validations/validators/oneof_validator.rb +51 -0
  161. data/lib/grape/validations/validators/presence_validator.rb +4 -2
  162. data/lib/grape/validations/validators/regexp_validator.rb +11 -3
  163. data/lib/grape/validations/validators/same_as_validator.rb +7 -15
  164. data/lib/grape/validations/validators/values_validator.rb +36 -65
  165. data/lib/grape/validations.rb +8 -21
  166. data/lib/grape/version.rb +1 -2
  167. data/lib/grape/xml.rb +17 -0
  168. data/lib/grape.rb +96 -288
  169. metadata +83 -294
  170. data/lib/grape/api/helpers.rb +0 -9
  171. data/lib/grape/dsl/api.rb +0 -19
  172. data/lib/grape/dsl/configuration.rb +0 -15
  173. data/lib/grape/eager_load.rb +0 -20
  174. data/lib/grape/exceptions/empty_message_body.rb +0 -11
  175. data/lib/grape/exceptions/missing_option.rb +0 -11
  176. data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
  177. data/lib/grape/exceptions/unknown_options.rb +0 -11
  178. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -27
  179. data/lib/grape/extensions/hash.rb +0 -22
  180. data/lib/grape/extensions/hashie/mash.rb +0 -26
  181. data/lib/grape/http/headers.rb +0 -61
  182. data/lib/grape/middleware/globals.rb +0 -16
  183. data/lib/grape/middleware/helpers.rb +0 -12
  184. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +0 -24
  185. data/lib/grape/router/attribute_translator.rb +0 -63
  186. data/lib/grape/types/invalid_value.rb +0 -8
  187. data/lib/grape/util/base_inheritable.rb +0 -43
  188. data/lib/grape/util/inheritable_values.rb +0 -31
  189. data/lib/grape/util/json.rb +0 -12
  190. data/lib/grape/util/lazy_block.rb +0 -27
  191. data/lib/grape/util/lazy_object.rb +0 -43
  192. data/lib/grape/util/lazy_value.rb +0 -91
  193. data/lib/grape/util/registrable.rb +0 -15
  194. data/lib/grape/util/reverse_stackable_values.rb +0 -20
  195. data/lib/grape/util/strict_hash_configuration.rb +0 -108
  196. data/lib/grape/util/xml.rb +0 -10
  197. data/lib/grape/validations/attributes_doc.rb +0 -58
  198. data/lib/grape/validations/types/build_coercer.rb +0 -94
  199. data/lib/grape/validations/validator_factory.rb +0 -15
  200. data/spec/grape/api/custom_validations_spec.rb +0 -213
  201. data/spec/grape/api/deeply_included_options_spec.rb +0 -56
  202. data/spec/grape/api/defines_boolean_in_params_spec.rb +0 -38
  203. data/spec/grape/api/documentation_spec.rb +0 -59
  204. data/spec/grape/api/inherited_helpers_spec.rb +0 -114
  205. data/spec/grape/api/instance_spec.rb +0 -103
  206. data/spec/grape/api/invalid_format_spec.rb +0 -45
  207. data/spec/grape/api/namespace_parameters_in_route_spec.rb +0 -38
  208. data/spec/grape/api/nested_helpers_spec.rb +0 -50
  209. data/spec/grape/api/optional_parameters_in_route_spec.rb +0 -43
  210. data/spec/grape/api/parameters_modification_spec.rb +0 -41
  211. data/spec/grape/api/patch_method_helpers_spec.rb +0 -79
  212. data/spec/grape/api/recognize_path_spec.rb +0 -21
  213. data/spec/grape/api/required_parameters_in_route_spec.rb +0 -37
  214. data/spec/grape/api/required_parameters_with_invalid_method_spec.rb +0 -26
  215. data/spec/grape/api/routes_with_requirements_spec.rb +0 -59
  216. data/spec/grape/api/shared_helpers_exactly_one_of_spec.rb +0 -41
  217. data/spec/grape/api/shared_helpers_spec.rb +0 -36
  218. data/spec/grape/api_remount_spec.rb +0 -509
  219. data/spec/grape/api_spec.rb +0 -4356
  220. data/spec/grape/dsl/callbacks_spec.rb +0 -45
  221. data/spec/grape/dsl/desc_spec.rb +0 -98
  222. data/spec/grape/dsl/headers_spec.rb +0 -62
  223. data/spec/grape/dsl/helpers_spec.rb +0 -100
  224. data/spec/grape/dsl/inside_route_spec.rb +0 -531
  225. data/spec/grape/dsl/logger_spec.rb +0 -24
  226. data/spec/grape/dsl/middleware_spec.rb +0 -60
  227. data/spec/grape/dsl/parameters_spec.rb +0 -180
  228. data/spec/grape/dsl/request_response_spec.rb +0 -225
  229. data/spec/grape/dsl/routing_spec.rb +0 -275
  230. data/spec/grape/dsl/settings_spec.rb +0 -261
  231. data/spec/grape/dsl/validations_spec.rb +0 -55
  232. data/spec/grape/endpoint/declared_spec.rb +0 -846
  233. data/spec/grape/endpoint_spec.rb +0 -1085
  234. data/spec/grape/entity_spec.rb +0 -336
  235. data/spec/grape/exceptions/base_spec.rb +0 -81
  236. data/spec/grape/exceptions/body_parse_errors_spec.rb +0 -185
  237. data/spec/grape/exceptions/invalid_accept_header_spec.rb +0 -358
  238. data/spec/grape/exceptions/invalid_formatter_spec.rb +0 -15
  239. data/spec/grape/exceptions/invalid_response_spec.rb +0 -11
  240. data/spec/grape/exceptions/invalid_versioner_option_spec.rb +0 -15
  241. data/spec/grape/exceptions/missing_group_type_spec.rb +0 -17
  242. data/spec/grape/exceptions/missing_mime_type_spec.rb +0 -17
  243. data/spec/grape/exceptions/missing_option_spec.rb +0 -15
  244. data/spec/grape/exceptions/unknown_options_spec.rb +0 -15
  245. data/spec/grape/exceptions/unknown_validator_spec.rb +0 -15
  246. data/spec/grape/exceptions/unsupported_group_type_spec.rb +0 -19
  247. data/spec/grape/exceptions/validation_errors_spec.rb +0 -92
  248. data/spec/grape/exceptions/validation_spec.rb +0 -19
  249. data/spec/grape/extensions/param_builders/hash_spec.rb +0 -83
  250. data/spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb +0 -105
  251. data/spec/grape/extensions/param_builders/hashie/mash_spec.rb +0 -79
  252. data/spec/grape/grape_spec.rb +0 -9
  253. data/spec/grape/integration/global_namespace_function_spec.rb +0 -29
  254. data/spec/grape/integration/rack_sendfile_spec.rb +0 -48
  255. data/spec/grape/integration/rack_spec.rb +0 -51
  256. data/spec/grape/loading_spec.rb +0 -44
  257. data/spec/grape/middleware/auth/base_spec.rb +0 -31
  258. data/spec/grape/middleware/auth/dsl_spec.rb +0 -60
  259. data/spec/grape/middleware/auth/strategies_spec.rb +0 -120
  260. data/spec/grape/middleware/base_spec.rb +0 -221
  261. data/spec/grape/middleware/error_spec.rb +0 -85
  262. data/spec/grape/middleware/exception_spec.rb +0 -294
  263. data/spec/grape/middleware/formatter_spec.rb +0 -461
  264. data/spec/grape/middleware/globals_spec.rb +0 -30
  265. data/spec/grape/middleware/stack_spec.rb +0 -155
  266. data/spec/grape/middleware/versioner/accept_version_header_spec.rb +0 -122
  267. data/spec/grape/middleware/versioner/header_spec.rb +0 -345
  268. data/spec/grape/middleware/versioner/param_spec.rb +0 -171
  269. data/spec/grape/middleware/versioner/path_spec.rb +0 -62
  270. data/spec/grape/middleware/versioner_spec.rb +0 -21
  271. data/spec/grape/named_api_spec.rb +0 -19
  272. data/spec/grape/parser_spec.rb +0 -86
  273. data/spec/grape/path_spec.rb +0 -252
  274. data/spec/grape/presenters/presenter_spec.rb +0 -71
  275. data/spec/grape/request_spec.rb +0 -126
  276. data/spec/grape/util/inheritable_setting_spec.rb +0 -242
  277. data/spec/grape/util/inheritable_values_spec.rb +0 -79
  278. data/spec/grape/util/reverse_stackable_values_spec.rb +0 -134
  279. data/spec/grape/util/stackable_values_spec.rb +0 -128
  280. data/spec/grape/util/strict_hash_configuration_spec.rb +0 -38
  281. data/spec/grape/validations/attributes_doc_spec.rb +0 -153
  282. data/spec/grape/validations/instance_behaivour_spec.rb +0 -43
  283. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +0 -38
  284. data/spec/grape/validations/params_scope_spec.rb +0 -1420
  285. data/spec/grape/validations/single_attribute_iterator_spec.rb +0 -56
  286. data/spec/grape/validations/types/array_coercer_spec.rb +0 -33
  287. data/spec/grape/validations/types/primitive_coercer_spec.rb +0 -150
  288. data/spec/grape/validations/types/set_coercer_spec.rb +0 -32
  289. data/spec/grape/validations/types_spec.rb +0 -111
  290. data/spec/grape/validations/validators/all_or_none_spec.rb +0 -162
  291. data/spec/grape/validations/validators/allow_blank_spec.rb +0 -575
  292. data/spec/grape/validations/validators/at_least_one_of_spec.rb +0 -205
  293. data/spec/grape/validations/validators/base_spec.rb +0 -38
  294. data/spec/grape/validations/validators/coerce_spec.rb +0 -1261
  295. data/spec/grape/validations/validators/default_spec.rb +0 -463
  296. data/spec/grape/validations/validators/exactly_one_of_spec.rb +0 -233
  297. data/spec/grape/validations/validators/except_values_spec.rb +0 -192
  298. data/spec/grape/validations/validators/mutual_exclusion_spec.rb +0 -214
  299. data/spec/grape/validations/validators/presence_spec.rb +0 -315
  300. data/spec/grape/validations/validators/regexp_spec.rb +0 -161
  301. data/spec/grape/validations/validators/same_as_spec.rb +0 -57
  302. data/spec/grape/validations/validators/values_spec.rb +0 -733
  303. data/spec/grape/validations/validators/zh-CN.yml +0 -10
  304. data/spec/grape/validations_spec.rb +0 -2030
  305. data/spec/integration/eager_load/eager_load_spec.rb +0 -15
  306. data/spec/integration/multi_json/json_spec.rb +0 -7
  307. data/spec/integration/multi_xml/xml_spec.rb +0 -7
  308. data/spec/shared/deprecated_class_examples.rb +0 -16
  309. data/spec/shared/versioning_examples.rb +0 -215
  310. data/spec/spec_helper.rb +0 -52
  311. data/spec/support/basic_auth_encode_helpers.rb +0 -11
  312. data/spec/support/chunks.rb +0 -14
  313. data/spec/support/content_type_helpers.rb +0 -15
  314. data/spec/support/endpoint_faker.rb +0 -25
  315. data/spec/support/file_streamer.rb +0 -13
  316. data/spec/support/integer_helpers.rb +0 -13
  317. data/spec/support/versioned_helpers.rb +0 -55
@@ -1,30 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/dsl/headers'
4
-
5
3
  module Grape
6
4
  module Middleware
7
5
  class Base
8
- include Helpers
9
-
10
- attr_reader :app, :env, :options
11
-
12
- TEXT_HTML = 'text/html'
13
-
14
6
  include Grape::DSL::Headers
15
7
 
8
+ attr_reader :app, :env, :options, :config
9
+
16
10
  # @param [Rack Application] app The standard argument for a Rack middleware.
17
- # @param [Hash] options A hash of options, simply stored for use by subclasses.
18
- def initialize(app, *options)
11
+ # @param [Hash] options Options forwarded to the subclass. When the
12
+ # subclass declares an `Options` Data class, the kwargs are routed
13
+ # through it and exposed via {#config}; {#options} keeps returning a
14
+ # frozen Hash representation for back-compat with subclasses that read
15
+ # `options[:key]`. Otherwise the kwargs are deep-merged with the
16
+ # subclass's `DEFAULT_OPTIONS` Hash (legacy path) and frozen.
17
+ def initialize(app, **options)
19
18
  @app = app
20
- @options = options.any? ? default_options.merge(options.shift) : default_options
19
+ if (config_class = options_data_class)
20
+ @config = config_class.new(**options)
21
+ @options = @config.to_h.freeze
22
+ else
23
+ @options = merge_default_options(options).freeze
24
+ end
21
25
  @app_response = nil
22
26
  end
23
27
 
24
- def default_options
25
- {}
26
- end
27
-
28
28
  def call(env)
29
29
  dup.call!(env).to_a
30
30
  end
@@ -57,40 +57,63 @@ module Grape
57
57
  # @return [Response, nil] a Rack SPEC response or nil to call the application afterwards.
58
58
  def after; end
59
59
 
60
- def response
61
- return @app_response if @app_response.is_a?(Rack::Response)
62
-
63
- @app_response = Rack::Response.new(@app_response[2], @app_response[0], @app_response[1])
60
+ def rack_request
61
+ @rack_request ||= Rack::Request.new(env)
64
62
  end
65
63
 
66
- def content_type_for(format)
67
- HashWithIndifferentAccess.new(content_types)[format]
64
+ def context
65
+ env[Grape::Env::API_ENDPOINT]
68
66
  end
69
67
 
70
- def content_types
71
- ContentTypes.content_types_for(options[:content_types])
72
- end
68
+ def response
69
+ return @app_response if @app_response.is_a?(Rack::Response)
73
70
 
74
- def content_type
75
- content_type_for(env[Grape::Env::API_FORMAT] || options[:format]) || TEXT_HTML
71
+ @app_response = Rack::Response[*@app_response]
76
72
  end
77
73
 
78
- def mime_types
79
- @mime_type ||= content_types.each_pair.with_object({}) do |(k, v), types_without_params|
80
- types_without_params[v.split(';').first] = k
81
- end
74
+ def query_params
75
+ rack_request.GET
76
+ rescue *Grape::RACK_ERRORS
77
+ raise Grape::Exceptions::RequestError
82
78
  end
83
79
 
84
80
  private
85
81
 
86
82
  def merge_headers(response)
87
- return unless headers.is_a?(Hash)
83
+ return if @header.blank?
88
84
 
89
85
  case response
90
- when Rack::Response then response.headers.merge!(headers)
91
- when Array then response[1].merge!(headers)
86
+ when Rack::Response then response.headers.merge!(@header)
87
+ when Array then response[1].merge!(@header)
92
88
  end
93
89
  end
90
+
91
+ def merge_default_options(options)
92
+ return default_options.deep_merge(options) if respond_to?(:default_options)
93
+
94
+ default_options_constant&.deep_merge(options) || options
95
+ end
96
+
97
+ # self.class::Options honours middleware inheritance (e.g. Versioner::Path
98
+ # → Versioner::Base) and, unlike const_defined?, never resolves a
99
+ # top-level ::Options on Object. Absent an own/inherited Options class,
100
+ # the lookup raises NameError and we fall back to the legacy path.
101
+ def options_data_class
102
+ self.class::Options
103
+ rescue NameError
104
+ nil
105
+ end
106
+
107
+ # Same idea as {#options_data_class} for the legacy DEFAULT_OPTIONS Hash.
108
+ def default_options_constant
109
+ self.class::DEFAULT_OPTIONS
110
+ rescue NameError
111
+ nil
112
+ end
113
+
114
+ def try_scrub(obj)
115
+ obj.respond_to?(:valid_encoding?) && !obj.valid_encoding? ? obj.scrub : obj
116
+ end
94
117
  end
95
118
  end
96
119
  end
@@ -1,138 +1,337 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/middleware/base'
4
- require 'active_support/core_ext/string/output_safety'
5
-
6
3
  module Grape
7
4
  module Middleware
8
5
  class Error < Base
9
- def default_options
10
- {
11
- default_status: 500, # default status returned on error
12
- default_message: '',
13
- format: :txt,
14
- helpers: nil,
15
- formatters: {},
16
- error_formatters: {},
17
- rescue_all: false, # true to rescue all exceptions
18
- rescue_grape_exceptions: false,
19
- rescue_subclasses: true, # rescue subclasses of exceptions listed
20
- rescue_options: {
21
- backtrace: false, # true to display backtrace, true to let Grape handle Grape::Exceptions
22
- original_exception: false # true to display exception
23
- },
24
- rescue_handlers: {}, # rescue handler blocks
25
- base_only_rescue_handlers: {}, # rescue handler blocks rescuing only the base class
26
- all_rescue_handler: nil # rescue handler block to rescue from all exceptions
27
- }
28
- end
29
-
30
- def initialize(app, *options)
31
- super
32
- self.class.send(:include, @options[:helpers]) if @options[:helpers]
6
+ extend Forwardable
7
+ include PrecomputedContentTypes
8
+
9
+ Options = Data.define(
10
+ :all_rescue_handler, :base_only_rescue_handlers, :content_types,
11
+ :default_error_formatter, :default_message, :default_status,
12
+ :error_formatters, :format,
13
+ :grape_exceptions_rescue_handler, :internal_grape_exceptions_rescue_handler,
14
+ :rescue_all, :rescue_grape_exceptions, :rescue_handlers, :rescue_options
15
+ ) do
16
+ def initialize(
17
+ all_rescue_handler: nil, base_only_rescue_handlers: nil, content_types: nil,
18
+ default_error_formatter: nil, default_message: '', default_status: 500,
19
+ error_formatters: nil, format: :txt,
20
+ grape_exceptions_rescue_handler: nil, internal_grape_exceptions_rescue_handler: nil,
21
+ rescue_all: false, rescue_grape_exceptions: false, rescue_handlers: nil,
22
+ rescue_options: nil
23
+ )
24
+ # `rescue_options:` and `default_error_formatter:` arrive nil from
25
+ # `Endpoint#error_middleware_options` when the API called no
26
+ # `rescue_from` and no `format` — and an explicit nil bypasses the
27
+ # keyword defaults above, so restore them here rather than letting nil
28
+ # propagate to `def_delegator :rescue_options, :backtrace` or to the
29
+ # formatter lookup in `#format_message`.
30
+ super(
31
+ rescue_options: rescue_options || Grape::DSL::RescueOptions.new,
32
+ default_error_formatter: default_error_formatter || Grape::ErrorFormatter::Txt,
33
+ all_rescue_handler:, base_only_rescue_handlers:, content_types:,
34
+ default_message:, default_status:, error_formatters:, format:,
35
+ grape_exceptions_rescue_handler:, internal_grape_exceptions_rescue_handler:,
36
+ rescue_all:, rescue_grape_exceptions:, rescue_handlers:
37
+ )
38
+ end
33
39
  end
34
40
 
41
+ def_delegators :config,
42
+ :all_rescue_handler, :base_only_rescue_handlers, :default_error_formatter,
43
+ :default_message, :default_status, :error_formatters, :format,
44
+ :grape_exceptions_rescue_handler, :internal_grape_exceptions_rescue_handler,
45
+ :rescue_all, :rescue_grape_exceptions, :rescue_handlers, :rescue_options
46
+
47
+ # +:backtrace+ / +:original_exception+ on the rescue options become
48
+ # +#include_backtrace+ / +#include_original_exception+ on the middleware,
49
+ # which is what the formatter call site reads.
50
+ def_delegator :rescue_options, :backtrace, :include_backtrace
51
+ def_delegator :rescue_options, :original_exception, :include_original_exception
52
+
53
+ # Emitted by {#render_failsafe_response} once even the framework's own message
54
+ # could not be rendered. Deliberately built without a formatter, an i18n
55
+ # lookup or anything else that could be the thing that is broken.
56
+ FAILSAFE_STATUS = 500
57
+ FAILSAFE_MESSAGE = '500 Internal Server Error'
58
+ FAILSAFE_CONTENT_TYPE = 'text/plain'
59
+
35
60
  def call!(env)
36
61
  @env = env
37
- begin
38
- error_response(catch(:error) do
39
- return @app.call(@env)
40
- end)
41
- rescue Exception => e # rubocop:disable Lint/RescueException
42
- handler =
43
- rescue_handler_for_base_only_class(e.class) ||
44
- rescue_handler_for_class_or_its_ancestor(e.class) ||
45
- rescue_handler_for_grape_exception(e.class) ||
46
- rescue_handler_for_any_class(e.class) ||
47
- raise
48
-
49
- run_rescue_handler(handler, e)
50
- end
62
+ error_response(catch(:error) { return @app.call(@env) })
63
+ rescue Exception => e # rubocop:disable Lint/RescueException
64
+ run_rescue_handler(find_handler(e.class), e, @env[Grape::Env::API_ENDPOINT])
51
65
  end
52
66
 
53
- def error!(message, status = options[:default_status], headers = {}, backtrace = [], original_exception = nil)
54
- headers = headers.reverse_merge(Grape::Http::Headers::CONTENT_TYPE => content_type)
55
- rack_response(format_message(message, backtrace, original_exception), status, headers)
67
+ private
68
+
69
+ def rack_response(status, headers, message)
70
+ body = html_content_type?(headers[Rack::CONTENT_TYPE]) ? Rack::Utils.escape_html(message) : message
71
+ Rack::Response.new(Array.wrap(body), Rack::Utils.status_code(status), Grape::Util::Header.new.merge!(headers))
56
72
  end
57
73
 
58
- def default_rescue_handler(e)
59
- error_response(message: e.message, backtrace: e.backtrace, original_exception: e)
74
+ # Escaping must key off the media type only, case-insensitively. Comparing
75
+ # the raw header against 'text/html' would let a parameterized value such
76
+ # as 'text/html; charset=utf-8' (or a differently-cased 'Text/HTML', which
77
+ # browsers still treat as HTML) skip escaping and reflect an unescaped
78
+ # message into an HTML response. Such a header can be set from several
79
+ # places (a registered content type, or a custom Content-Type passed to
80
+ # error!/rescue_from), but they all render here.
81
+ def html_content_type?(content_type)
82
+ Grape::ContentTypes.media_type(content_type).to_s.casecmp?('text/html')
60
83
  end
61
84
 
62
- # TODO: This method is deprecated. Refactor out.
63
- def error_response(error = {})
64
- status = error[:status] || options[:default_status]
65
- message = error[:message] || options[:default_message]
66
- headers = { Grape::Http::Headers::CONTENT_TYPE => content_type }
67
- headers.merge!(error[:headers]) if error[:headers].is_a?(Hash)
68
- backtrace = error[:backtrace] || error[:original_exception]&.backtrace || []
69
- original_exception = error.is_a?(Exception) ? error : error[:original_exception] || nil
70
- rack_response(format_message(message, backtrace, original_exception), status, headers)
85
+ # The registry answers nil for a format nothing is registered for, and
86
+ # the API's +default_error_formatter+ — +ErrorFormatter::Txt+ unless the
87
+ # API set another — takes it from there, so this always has something
88
+ # callable. The +throw :error, 406+ that used to stand in for a missing
89
+ # formatter could not work anyway: nothing catches +:error+ around this
90
+ # call (+#call!+ has left its +catch+ by the time +error_response+ runs),
91
+ # so it raised +UncaughtThrowError+ and the request answered with the
92
+ # failsafe 500 rather than the 406 it named.
93
+ def format_message(error)
94
+ current_format = env[Grape::Env::API_FORMAT] || format
95
+ formatter = Grape::ErrorFormatter.formatter_for(current_format, error_formatters) || default_error_formatter
96
+ formatter.call(error:, env:, include_backtrace:, include_original_exception:)
71
97
  end
72
98
 
73
- def rack_response(message, status = options[:default_status], headers = { Grape::Http::Headers::CONTENT_TYPE => content_type })
74
- message = ERB::Util.html_escape(message) if headers[Grape::Http::Headers::CONTENT_TYPE] == TEXT_HTML
75
- Rack::Response.new([message], Rack::Utils.status_code(status), headers)
99
+ def find_handler(klass)
100
+ registered_entry = registered_rescue_handler_entry(klass)
101
+
102
+ grape_exceptions_precedence_handler(klass, registered_entry) ||
103
+ registered_entry&.last ||
104
+ rescue_handler_for_grape_exception(klass) ||
105
+ rescue_handler_for_any_class(klass) ||
106
+ raise
76
107
  end
77
108
 
78
- def format_message(message, backtrace, original_exception = nil)
79
- format = env[Grape::Env::API_FORMAT] || options[:format]
80
- formatter = Grape::ErrorFormatter.formatter_for(format, **options)
81
- throw :error,
82
- status: 406,
83
- message: "The requested format '#{format}' is not supported.",
84
- backtrace: backtrace,
85
- original_exception: original_exception unless formatter
86
- formatter.call(message, backtrace, options, env, original_exception)
109
+ def error_response(error = nil)
110
+ raw = Grape::Exceptions::ErrorResponse.coerce(error)
111
+ headers = { Rack::CONTENT_TYPE => content_type }
112
+ headers.merge!(raw.headers) if raw.headers.is_a?(Hash)
113
+ payload = raw.with(
114
+ status: raw.status || default_status,
115
+ message: raw.message || default_message,
116
+ headers:,
117
+ backtrace: resolved_backtrace(raw)
118
+ )
119
+ env[Grape::Env::API_ENDPOINT].status(payload.status) # error! may not have been called
120
+ render_response(payload)
87
121
  end
88
122
 
89
- private
123
+ # The backtrace an error formatter is handed. Assembled only when the API
124
+ # asked for one with `rescue_from ..., backtrace: true`: reading it off
125
+ # the exception is not free — +Exception#backtrace+ builds the whole Array
126
+ # of location strings — and every built-in formatter drops it otherwise.
127
+ # A formatter that wants one regardless still has +original_exception+.
128
+ def resolved_backtrace(raw)
129
+ return [] unless include_backtrace
130
+
131
+ raw.backtrace || raw.original_exception&.backtrace || []
132
+ end
90
133
 
91
- def rescue_handler_for_base_only_class(klass)
92
- error, handler = options[:base_only_rescue_handlers].find { |err, _handler| klass == err }
134
+ # Rendering runs inside #call!'s own rescue clause, so it is not covered by
135
+ # that rescue: an error formatter that raises on the payload it was handed
136
+ # takes the exception out through every middleware above Grape. By this
137
+ # point Grape has committed to answering with an error, so it answers with
138
+ # one that does not depend on the payload rather than dropping the request.
139
+ #
140
+ # +Grape.config.raise_rendering_errors+ opts back out, for an application
141
+ # that would rather have the exception propagate as it did before.
142
+ def render_response(payload)
143
+ rack_response(payload.status, payload.headers, format_message(payload))
144
+ rescue StandardError => e
145
+ raise if Grape.config.raise_rendering_errors
93
146
 
94
- return unless error
147
+ record_rendering_failure(e)
148
+ render_failsafe_response
149
+ end
150
+
151
+ # The exception is published on the rack env (see {#expose_exception}) and
152
+ # written to +rack.errors+ so it reaches the log with no tracker installed.
153
+ # Rails writes to $stderr from its failsafe branch for the same reason:
154
+ # deferring the logging to the application is not an option when the
155
+ # application's own error rendering is what broke.
156
+ def record_rendering_failure(error)
157
+ expose_exception(error)
158
+ env[Rack::RACK_ERRORS]&.write("Grape could not render the error response: #{error.class}: #{error.message}\n")
159
+ end
160
+
161
+ # First retry the API's own format with the framework's InternalServerError,
162
+ # whose message is a static string and so cannot be what defeated the first
163
+ # attempt. Should even that fail — a wholesale broken formatter, rather than
164
+ # one payload it choked on — drop the formatter entirely. Both attempts call
165
+ # {#format_message} directly rather than re-entering {#error_response}, so
166
+ # this path cannot recurse.
167
+ def render_failsafe_response
168
+ headers = { Rack::CONTENT_TYPE => content_type }
169
+ rack_response(FAILSAFE_STATUS, headers, format_message(failsafe_payload(headers)))
170
+ rescue StandardError
171
+ rack_response(FAILSAFE_STATUS, { Rack::CONTENT_TYPE => FAILSAFE_CONTENT_TYPE }, FAILSAFE_MESSAGE)
172
+ end
173
+
174
+ # Publish an exception Grape swallowed, on both keys. +grape.exception+ is
175
+ # Grape's own and has been set on these paths all along; +rack.exception+ is
176
+ # what the ecosystem actually reads to find an exception that never
177
+ # propagated — sentry-ruby collects +env['rack.exception'] ||
178
+ # env['sinatra.error']+ — so a tracker mounted above Grape keeps reporting
179
+ # these with no application change.
180
+ def expose_exception(error)
181
+ env[Grape::Env::GRAPE_EXCEPTION] = error
182
+ env[Grape::Env::RACK_EXCEPTION] = error
183
+ end
184
+
185
+ def failsafe_payload(headers)
186
+ Grape::Exceptions::ErrorResponse.new(
187
+ status: FAILSAFE_STATUS,
188
+ message: Grape::Exceptions::InternalServerError.new.message,
189
+ headers:,
190
+ backtrace: [],
191
+ original_exception: nil
192
+ )
193
+ end
194
+
195
+ def default_rescue_handler(exception)
196
+ error_response(
197
+ Grape::Exceptions::ErrorResponse.new(
198
+ message: exception.message,
199
+ backtrace: exception.backtrace,
200
+ original_exception: exception
201
+ )
202
+ )
203
+ end
204
+
205
+ def registered_rescue_handler(klass)
206
+ registered_rescue_handler_entry(klass)&.last
207
+ end
95
208
 
96
- handler || :default_rescue_handler
209
+ # The matched entry rather than just its handler, so callers can tell
210
+ # *which* class matched — see {#grape_exceptions_precedence_handler}.
211
+ # @return [Array(Class, #call), nil]
212
+ def registered_rescue_handler_entry(klass)
213
+ rescue_handler_entry_from(base_only_rescue_handlers) { |err| klass == err } ||
214
+ rescue_handler_entry_from(rescue_handlers) { |err| klass <= err }
97
215
  end
98
216
 
99
- def rescue_handler_for_class_or_its_ancestor(klass)
100
- error, handler = options[:rescue_handlers].find { |err, _handler| klass <= err }
217
+ def rescue_handler_entry_from(handlers)
218
+ error, handler = handlers&.find { |err, _handler| yield(err) }
101
219
 
102
220
  return unless error
103
221
 
104
- handler || :default_rescue_handler
222
+ [error, handler || method(:default_rescue_handler)]
223
+ end
224
+
225
+ # +rescue_from :grape_exceptions+ is an opt-in to keep Grape's own errors
226
+ # rendering with their own status — a validation failure stays a 400
227
+ # instead of becoming whatever the app's catch-all returns.
228
+ #
229
+ # It only ever worked against +rescue_from :all+, which lives in
230
+ # +all_rescue_handler+ and is consulted last. Spelled as a class instead,
231
+ # +rescue_from StandardError+ is a *registered* handler, matched first,
232
+ # and Grape's exceptions are StandardErrors — so the opt-in silently did
233
+ # nothing and validation errors came back as 500s either way.
234
+ #
235
+ # Let it win over a handler that only matched through a non-Grape
236
+ # ancestor. One registered for a Grape exception class is more specific
237
+ # than the opt-in and still wins, so an explicit
238
+ # +rescue_from Grape::Exceptions::ValidationErrors+ keeps its handler.
239
+ #
240
+ # InvalidVersionHeader is left alone: it must keep reaching Rack so the
241
+ # next versioned route is tried.
242
+ def grape_exceptions_precedence_handler(klass, registered_entry)
243
+ return unless rescue_grape_exceptions
244
+ return unless klass <= Grape::Exceptions::Base
245
+ return if klass == Grape::Exceptions::InvalidVersionHeader
246
+
247
+ matched, = registered_entry
248
+ return if matched.nil? || matched <= Grape::Exceptions::Base
249
+
250
+ grape_exceptions_rescue_handler || method(:error_response)
105
251
  end
106
252
 
107
253
  def rescue_handler_for_grape_exception(klass)
108
254
  return unless klass <= Grape::Exceptions::Base
109
- return :error_response if klass == Grape::Exceptions::InvalidVersionHeader
110
- return unless options[:rescue_grape_exceptions] || !options[:rescue_all]
255
+ return method(:error_response) if klass == Grape::Exceptions::InvalidVersionHeader
256
+ return unless rescue_grape_exceptions || !rescue_all
111
257
 
112
- options[:grape_exceptions_rescue_handler] || :error_response
258
+ grape_exceptions_rescue_handler || method(:error_response)
113
259
  end
114
260
 
115
261
  def rescue_handler_for_any_class(klass)
116
262
  return unless klass <= StandardError
117
- return unless options[:rescue_all] || options[:rescue_grape_exceptions]
263
+ return unless rescue_all || rescue_grape_exceptions
118
264
 
119
- options[:all_rescue_handler] || :default_rescue_handler
265
+ all_rescue_handler || method(:default_rescue_handler)
120
266
  end
121
267
 
122
- def run_rescue_handler(handler, error)
123
- if handler.instance_of?(Symbol)
124
- raise NoMethodError, "undefined method '#{handler}'" unless respond_to?(handler)
125
-
126
- handler = public_method(handler)
268
+ def run_rescue_handler(handler, error, endpoint, redispatched: false)
269
+ callable = handler.is_a?(Symbol) ? endpoint.public_method(handler) : handler
270
+ response = catch(:error) do
271
+ callable.arity.zero? ? endpoint.instance_exec(&callable) : endpoint.instance_exec(error, &callable)
272
+ rescue StandardError => e
273
+ return redispatch(e, endpoint, redispatched)
127
274
  end
128
275
 
129
- response = handler.arity.zero? ? instance_exec(&handler) : instance_exec(error, &handler)
276
+ return error_response(response) if error?(response)
277
+ return response if response.is_a?(Rack::Response)
130
278
 
131
- if response.is_a?(Rack::Response)
132
- response
133
- else
134
- run_rescue_handler(:default_rescue_handler, Grape::Exceptions::InvalidResponse.new)
135
- end
279
+ run_rescue_handler(method(:default_rescue_handler), Grape::Exceptions::InvalidResponse.new, endpoint)
280
+ end
281
+
282
+ # Route an exception raised inside a +rescue_from+ block.
283
+ #
284
+ # * If we have already redispatched once (the redispatched handler
285
+ # itself raised), go straight to {#framework_default} — bounds the
286
+ # chain at one redispatch.
287
+ # * Else if the exception has a registered +rescue_from+ handler,
288
+ # run it.
289
+ # * Else if it's a +Grape::Exceptions::Base+ subclass, render it
290
+ # through +error_response+ with its own +status+ and +message+.
291
+ # * Else fall through to {#safe_default}, which lets the user opt
292
+ # in via +rescue_from :internal_grape_exceptions+ or, failing
293
+ # that, applies the framework default.
294
+ def redispatch(error, endpoint, already_redispatched)
295
+ return framework_default(endpoint) if already_redispatched
296
+
297
+ registered = registered_rescue_handler(error.class)
298
+
299
+ return run_rescue_handler(registered, error, endpoint, redispatched: true) if registered
300
+ return run_rescue_handler(method(:error_response), error, endpoint, redispatched: true) if error.is_a?(Grape::Exceptions::Base)
301
+
302
+ safe_default(error, endpoint)
303
+ end
304
+
305
+ # The unrecognised-error path. Exposes the original exception on
306
+ # the rack env so upstream Rack middleware (loggers, error
307
+ # trackers) can observe it. If the user registered a
308
+ # +rescue_from :internal_grape_exceptions+ handler, that handler
309
+ # runs and owns the response. Otherwise the framework renders the
310
+ # generic +InternalServerError+ — never the original exception's
311
+ # message. The framework deliberately does no logging of its own
312
+ # here; that's the application's call.
313
+ def safe_default(error, endpoint)
314
+ expose_exception(error)
315
+ return run_rescue_handler(internal_grape_exceptions_rescue_handler, error, endpoint, redispatched: true) if internal_grape_exceptions_rescue_handler
316
+
317
+ framework_default(endpoint)
318
+ end
319
+
320
+ def framework_default(endpoint)
321
+ run_rescue_handler(method(:default_rescue_handler), Grape::Exceptions::InternalServerError.new, endpoint)
322
+ end
323
+
324
+ def error!(message, status = default_status, headers = {}, backtrace = [], original_exception = nil)
325
+ env[Grape::Env::API_ENDPOINT].status(status) # not error! inside route
326
+ merged_headers = { Rack::CONTENT_TYPE => content_type }.merge!(headers)
327
+ error = Grape::Exceptions::ErrorResponse.new(
328
+ status:, message:, headers: merged_headers, backtrace:, original_exception:
329
+ )
330
+ rack_response(status, merged_headers, format_message(error))
331
+ end
332
+
333
+ def error?(response)
334
+ response.is_a?(Grape::Exceptions::ErrorResponse)
136
335
  end
137
336
  end
138
337
  end