hanami-action 3.0.0.rc1

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 (38) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +985 -0
  3. data/LICENSE +20 -0
  4. data/README.md +873 -0
  5. data/hanami-action.gemspec +39 -0
  6. data/lib/hanami/action/body_parser/json.rb +20 -0
  7. data/lib/hanami/action/body_parser/multipart_form.rb +22 -0
  8. data/lib/hanami/action/body_parser.rb +109 -0
  9. data/lib/hanami/action/cache/cache_control.rb +84 -0
  10. data/lib/hanami/action/cache/conditional_get.rb +101 -0
  11. data/lib/hanami/action/cache/directives.rb +126 -0
  12. data/lib/hanami/action/cache/expires.rb +84 -0
  13. data/lib/hanami/action/cache.rb +29 -0
  14. data/lib/hanami/action/config/formats.rb +256 -0
  15. data/lib/hanami/action/config.rb +172 -0
  16. data/lib/hanami/action/constants.rb +283 -0
  17. data/lib/hanami/action/cookie_jar.rb +214 -0
  18. data/lib/hanami/action/cookies.rb +27 -0
  19. data/lib/hanami/action/csrf_protection.rb +217 -0
  20. data/lib/hanami/action/errors.rb +109 -0
  21. data/lib/hanami/action/flash.rb +176 -0
  22. data/lib/hanami/action/halt.rb +18 -0
  23. data/lib/hanami/action/mime/request_mime_weight.rb +66 -0
  24. data/lib/hanami/action/mime.rb +438 -0
  25. data/lib/hanami/action/params.rb +342 -0
  26. data/lib/hanami/action/rack/file.rb +41 -0
  27. data/lib/hanami/action/rack_utils.rb +11 -0
  28. data/lib/hanami/action/request/session.rb +68 -0
  29. data/lib/hanami/action/request.rb +141 -0
  30. data/lib/hanami/action/response.rb +481 -0
  31. data/lib/hanami/action/session.rb +47 -0
  32. data/lib/hanami/action/validatable.rb +166 -0
  33. data/lib/hanami/action/version.rb +13 -0
  34. data/lib/hanami/action/view_name_inferrer.rb +56 -0
  35. data/lib/hanami/action.rb +672 -0
  36. data/lib/hanami/http/status.rb +149 -0
  37. data/lib/hanami-action.rb +3 -0
  38. metadata +153 -0
@@ -0,0 +1,672 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/configurable"
4
+ require "hanami/utils"
5
+ require "hanami/utils/callbacks"
6
+ require "hanami/utils/kernel"
7
+ require "hanami/utils/string"
8
+ require "rack"
9
+ require "rack/utils"
10
+ require "zeitwerk"
11
+
12
+ require_relative "action/constants"
13
+ require_relative "action/errors"
14
+ require_relative "action/rack_utils"
15
+ require_relative "action/version"
16
+
17
+ module Hanami
18
+ # An HTTP endpoint
19
+ #
20
+ # @since 0.1.0
21
+ #
22
+ # @example
23
+ # require "hanami/action"
24
+ #
25
+ # class Show < Hanami::Action
26
+ # def handle(request, response)
27
+ # # ...
28
+ # end
29
+ # end
30
+ #
31
+ # @api public
32
+ class Action
33
+ # @since 2.0.0
34
+ # @api private
35
+ def self.gem_loader
36
+ @gem_loader ||= Zeitwerk::Loader.new.tap do |loader|
37
+ root = File.expand_path("..", __dir__)
38
+ loader.tag = "hanami-action"
39
+ loader.inflector = Zeitwerk::GemInflector.new("#{root}/hanami-action.rb")
40
+ loader.push_dir(root)
41
+ loader.ignore(
42
+ "#{root}/hanami-action.rb",
43
+ "#{root}/hanami-controller.rb",
44
+ "#{root}/hanami/action/version.rb",
45
+ "#{root}/hanami/action/{constants,errors,rack_utils,validatable}.rb"
46
+ )
47
+ loader.inflector.inflect(
48
+ "csrf_protection" => "CSRFProtection",
49
+ "json" => "JSON"
50
+ )
51
+ end
52
+ end
53
+
54
+ gem_loader.setup
55
+
56
+ # Make conditional requires after Zeitwerk setup so any internal autoloading works as expected
57
+ begin
58
+ require "dry/validation"
59
+ require_relative "action/validatable"
60
+ rescue LoadError # rubocop:disable Lint/SuppressedException
61
+ end
62
+
63
+ extend Dry::Configurable(config_class: Config)
64
+
65
+ # See {Config} for individual setting accessor API docs
66
+ setting :handled_exceptions, default: {}
67
+ setting :formats, default: Config::Formats.new, mutable: true
68
+ setting :default_charset
69
+ setting :default_headers, default: {}, constructor: -> (headers) { headers.compact }
70
+ setting :default_tld_length, default: 1
71
+ setting :cookies, default: {}, constructor: -> (cookie_options) {
72
+ # Call `to_h` here to permit `ApplicationConfiguration::Cookies` object to be
73
+ # provided when application actions are configured
74
+ cookie_options.to_h.compact
75
+ }
76
+ setting :root_directory, constructor: -> (dir) {
77
+ Pathname(File.expand_path(dir || Dir.pwd))
78
+ }
79
+ setting :public_directory, default: Config::DEFAULT_PUBLIC_DIRECTORY
80
+ setting :before_callbacks, default: Utils::Callbacks::Chain.new, mutable: true
81
+ setting :after_callbacks, default: Utils::Callbacks::Chain.new, mutable: true
82
+ setting :contract_class
83
+
84
+ # @!scope class
85
+
86
+ # @!method config
87
+ # Returns the action's config. Use this to configure your action.
88
+ #
89
+ # @example Access inside class body
90
+ # class Show < Hanami::Action
91
+ # config.format :json
92
+ # end
93
+ #
94
+ # @return [Config]
95
+ #
96
+ # @api public
97
+ # @since 2.0.0
98
+
99
+ # @!scope instance
100
+
101
+ # Override Ruby's hook for modules.
102
+ # It includes basic Hanami::Action modules to the given class.
103
+ #
104
+ # @param subclass [Class] the target action
105
+ #
106
+ # @since 0.1.0
107
+ # @api private
108
+ def self.inherited(subclass)
109
+ super
110
+
111
+ if subclass.superclass == Action
112
+ subclass.class_eval do
113
+ include Validatable if defined?(Validatable)
114
+ end
115
+ end
116
+ end
117
+
118
+ # Placeholder for the `.params` method. Raises an error when the dry-validation gem is not
119
+ # installed.
120
+ #
121
+ # @raise [NoMethodError]
122
+ #
123
+ # @api public
124
+ # @since 2.0.0
125
+ def self.params(_klass = nil)
126
+ message = %(To use `.params`, please add the "dry-validation" gem to your Gemfile)
127
+ raise NoMethodError, message
128
+ end
129
+
130
+ # Placeholder for the `.contract` method. Raises an error when the dry-validation gem is not
131
+ # installed.
132
+ #
133
+ # @raise [NoMethodError]
134
+ #
135
+ # @api public
136
+ # @since 2.2.0
137
+ def self.contract
138
+ message = %(To use `.contract`, please add the "dry-validation" gem to your Gemfile)
139
+ raise NoMethodError, message
140
+ end
141
+
142
+ # @overload self.append_before(*callbacks, &block)
143
+ # Define a callback for an Action.
144
+ # The callback will be executed **before** the action is called, in the
145
+ # order they are added.
146
+ #
147
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
148
+ # each of them is representing a name of a method available in the
149
+ # context of the Action.
150
+ #
151
+ # @param blk [Proc] an anonymous function to be executed
152
+ #
153
+ # @return [void]
154
+ #
155
+ # @since 0.3.2
156
+ #
157
+ # @see Hanami::Action::Callbacks::ClassMethods#append_after
158
+ #
159
+ # @example Method names (symbols)
160
+ # require "hanami/action"
161
+ #
162
+ # class Show < Hanami::Action
163
+ # before :authenticate, :set_article
164
+ #
165
+ # def handle(request, response)
166
+ # end
167
+ #
168
+ # private
169
+ # def authenticate
170
+ # # ...
171
+ # end
172
+ #
173
+ # # `params` in the method signature is optional
174
+ # def set_article(params)
175
+ # @article = Article.find params[:id]
176
+ # end
177
+ # end
178
+ #
179
+ # # The order of execution will be:
180
+ # #
181
+ # # 1. #authenticate
182
+ # # 2. #set_article
183
+ # # 3. #call
184
+ #
185
+ # @example Anonymous functions (Procs)
186
+ # require "hanami/action"
187
+ #
188
+ # class Show < Hanami::Action
189
+ # before { ... } # 1 do some authentication stuff
190
+ # before {|request, response| @article = Article.find params[:id] } # 2
191
+ #
192
+ # def handle(request, response)
193
+ # end
194
+ # end
195
+ #
196
+ # # The order of execution will be:
197
+ # #
198
+ # # 1. authentication
199
+ # # 2. set the article
200
+ # # 3. `#handle`
201
+ def self.append_before(...)
202
+ config.before_callbacks.append(...)
203
+ end
204
+
205
+ class << self
206
+ # @since 0.1.0
207
+ alias_method :before, :append_before
208
+ end
209
+
210
+ # @overload self.append_after(*callbacks, &block)
211
+ # Define a callback for an Action.
212
+ # The callback will be executed **after** the action is called, in the
213
+ # order they are added.
214
+ #
215
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
216
+ # each of them is representing a name of a method available in the
217
+ # context of the Action.
218
+ #
219
+ # @param blk [Proc] an anonymous function to be executed
220
+ #
221
+ # @return [void]
222
+ #
223
+ # @since 0.3.2
224
+ #
225
+ # @see Hanami::Action::Callbacks::ClassMethods#append_before
226
+ def self.append_after(...)
227
+ config.after_callbacks.append(...)
228
+ end
229
+
230
+ class << self
231
+ # @since 0.1.0
232
+ alias_method :after, :append_after
233
+ end
234
+
235
+ # @overload self.prepend_before(*callbacks, &block)
236
+ # Define a callback for an Action.
237
+ # The callback will be executed **before** the action is called.
238
+ # It will add the callback at the beginning of the callbacks' chain.
239
+ #
240
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
241
+ # each of them is representing a name of a method available in the
242
+ # context of the Action.
243
+ #
244
+ # @param blk [Proc] an anonymous function to be executed
245
+ #
246
+ # @return [void]
247
+ #
248
+ # @since 0.3.2
249
+ #
250
+ # @see Hanami::Action::Callbacks::ClassMethods#prepend_after
251
+ def self.prepend_before(...)
252
+ config.before_callbacks.prepend(...)
253
+ end
254
+
255
+ # @overload self.prepend_after(*callbacks, &block)
256
+ # Define a callback for an Action.
257
+ # The callback will be executed **after** the action is called.
258
+ # It will add the callback at the beginning of the callbacks' chain.
259
+ #
260
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
261
+ # each of them is representing a name of a method available in the
262
+ # context of the Action.
263
+ #
264
+ # @param blk [Proc] an anonymous function to be executed
265
+ #
266
+ # @return [void]
267
+ #
268
+ # @since 0.3.2
269
+ #
270
+ # @see Hanami::Action::Callbacks::ClassMethods#prepend_before
271
+ def self.prepend_after(...)
272
+ config.after_callbacks.prepend(...)
273
+ end
274
+
275
+ # @see Config#handle_exception
276
+ #
277
+ # @since 2.0.0
278
+ # @api public
279
+ def self.handle_exception(...)
280
+ config.handle_exception(...)
281
+ end
282
+
283
+ # Returns the action's frozen `Data` snapshot of its resolved configuration.
284
+ #
285
+ # Instances expose `config` as a read-only `Data` value object (built via
286
+ # dry-configurable's `#to_data`), not the live `Hanami::Action::Config` used at the class
287
+ # level. All setting readers (`formats`, `default_headers`, `default_charset`, etc.) work
288
+ # exactly the same; Config-specific methods like `#handle_exception` are not available on
289
+ # instances and must be called on the class config instead.
290
+ #
291
+ # @since x.x.x
292
+ # @api private
293
+ private attr_reader :config
294
+
295
+ # @since 2.2.0
296
+ # @api private
297
+ private attr_reader :contract
298
+
299
+ # Resolved response charset, computed once from config. Passed to each {Response} so it
300
+ # doesn't have to re-parse the content type's charset on every request.
301
+ #
302
+ # @api private
303
+ private attr_reader :default_charset
304
+
305
+ # Response content type (with charset) and format used when a request has no usable `Accept`
306
+ # header. These depend only on config, so they're computed once and reused per request.
307
+ #
308
+ # @api private
309
+ private attr_reader :default_response_content_type, :default_response_format
310
+
311
+ # Returns a new action
312
+ #
313
+ # @since 2.0.0
314
+ # @api public
315
+ def initialize(config: self.class.config, contract: nil)
316
+ @config = config.finalize!.to_data
317
+ @contract = contract || config.contract_class&.new # TODO: tests showing this overridden by a dep
318
+ @default_charset = @config.default_charset || DEFAULT_CHARSET
319
+ @default_response_content_type = Mime.default_response_content_type_with_charset(@config)
320
+ @default_response_format = Mime.format_from_media_type(@default_response_content_type, @config)
321
+ freeze
322
+ end
323
+
324
+ # rubocop:disable Metrics/AbcSize
325
+
326
+ # Implements the Rack/Hanami::Action protocol
327
+ #
328
+ # @since 0.1.0
329
+ # @api private
330
+ def call(env)
331
+ request = nil
332
+ response = nil
333
+
334
+ halted = catch :halt do
335
+ # Catch body parsing errors early, and wait to raise them until _after_ we've built our
336
+ # request and response, to give exception handlers real objects to work with.
337
+ body_parse_error = nil
338
+ begin
339
+ BodyParser.parse env, config
340
+ rescue BodyParsingError => exception
341
+ body_parse_error = exception
342
+ end
343
+
344
+ params = Params.new(
345
+ # Create empty params if body parsing failed, to avoid validating corrupted input.
346
+ env: body_parse_error ? {} : env,
347
+ contract: contract
348
+ )
349
+
350
+ # Ensure env has REQUEST_METHOD for downstream code (e.g. Response) when actions are called
351
+ # with a direct params hash as a testing convenience.
352
+ env[REQUEST_METHOD] ||= DEFAULT_REQUEST_METHOD
353
+
354
+ request = build_request(
355
+ env: env,
356
+ params: params,
357
+ session_enabled: session_enabled?,
358
+ default_tld_length: config.default_tld_length
359
+ )
360
+
361
+ content_type =
362
+ if request.accept_header?
363
+ Mime.response_content_type_with_charset(request, config)
364
+ else
365
+ default_response_content_type
366
+ end
367
+
368
+ response = build_response(
369
+ request: request,
370
+ config: config,
371
+ content_type: content_type,
372
+ charset: default_charset,
373
+ env: env,
374
+ headers: config.default_headers,
375
+ session_enabled: session_enabled?
376
+ )
377
+
378
+ raise body_parse_error if body_parse_error
379
+
380
+ enforce_accepted_media_types(request)
381
+
382
+ _run_before_callbacks(request, response)
383
+ handle(request, response)
384
+ _run_after_callbacks(request, response)
385
+ rescue StandardError => exception
386
+ _handle_exception(request, response, exception)
387
+ end
388
+
389
+ # Before finishing, put ourself into the Rack env for third-party instrumentation tools to
390
+ # integrate with actions
391
+ env[ACTION_INSTANCE] = self
392
+
393
+ finish(request, response, halted)
394
+ end
395
+
396
+ # rubocop:enable Metrics/AbcSize
397
+
398
+ protected
399
+
400
+ # Hook for subclasses to apply behavior as part of action invocation
401
+ #
402
+ # @param request [Hanami::Action::Request]
403
+ # @param response [Hanami::Action::Response]
404
+ #
405
+ # @since 2.0.0
406
+ # @api public
407
+ def handle(request, response)
408
+ end
409
+
410
+ # Halt the action execution with the given HTTP status code and message.
411
+ #
412
+ # When used, the execution of a callback or of an action is interrupted
413
+ # and the control returns to the framework, that decides how to handle
414
+ # the event.
415
+ #
416
+ # If a message is provided, it sets the response body with the message.
417
+ # Otherwise, it sets the response body with the default message associated
418
+ # to the code (eg 404 will set `"Not Found"`).
419
+ #
420
+ # @param status [Integer, Symbol] A valid HTTP status code or a symbol representing the HTTP status code
421
+ # @param body [String] the response body
422
+ #
423
+ # @raise [StandardError] if the code isn't valid
424
+ #
425
+ # @since 0.2.0
426
+ #
427
+ # @see https://hanakai.org/learn/hanami/actions/status-codes List of status codes and symbols
428
+ #
429
+ # @see Hanami::Action::Throwable#handle_exception
430
+ # @see Hanami::Http::Status:ALL
431
+ #
432
+ # @example Basic usage
433
+ # require "hanami/action"
434
+ #
435
+ # class Show < Hanami::Action
436
+ # def handle(*)
437
+ # halt 404
438
+ # end
439
+ # end
440
+ #
441
+ # # => [404, {}, ["Not Found"]]
442
+ #
443
+ # @example Custom message
444
+ # require "hanami/action"
445
+ #
446
+ # class Show < Hanami::Action
447
+ # def handle(*)
448
+ # halt 404, "This is not the droid you're looking for."
449
+ # end
450
+ # end
451
+ #
452
+ # # => [404, {}, ["This is not the droid you're looking for."]]
453
+ def halt(status, body = nil)
454
+ Halt.call(status, body)
455
+ end
456
+
457
+ # @since 0.3.2
458
+ # @api private
459
+ def _requires_no_body?(response)
460
+ HTTP_STATUSES_WITHOUT_BODY.include?(response.status)
461
+ end
462
+
463
+ # @since 2.0.0
464
+ # @api private
465
+ alias_method :_requires_empty_headers?, :_requires_no_body?
466
+
467
+ private
468
+
469
+ # @since 2.0.0
470
+ # @api private
471
+ def enforce_accepted_media_types(request)
472
+ return if config.formats.empty?
473
+
474
+ Mime.enforce_accept(request, config) { return halt 406 }
475
+ Mime.enforce_content_type(request, config) { return halt 415 }
476
+ end
477
+
478
+ # @since 2.0.0
479
+ # @api private
480
+ def exception_handler(exception)
481
+ config.handled_exceptions.each do |exception_class, handler|
482
+ case exception_class
483
+ when String
484
+ return handler if exception.class.name == exception_class # rubocop:disable Style/ClassEqualityComparison
485
+ else
486
+ return handler if exception.is_a?(exception_class)
487
+ end
488
+ end
489
+
490
+ nil
491
+ end
492
+
493
+ # @see Session#session_enabled?
494
+ # @since 2.0.0
495
+ # @api private
496
+ def session_enabled?
497
+ false
498
+ end
499
+
500
+ # Hook to be overridden by `Hanami::Extensions::Action` for integrated actions
501
+ #
502
+ # @since 2.0.0
503
+ # @api private
504
+ def build_request(env:, params:, session_enabled:, default_tld_length:)
505
+ Request.new(env:, params:, session_enabled:, default_tld_length:)
506
+ end
507
+
508
+ # Hook to be overridden by `Hanami::Extensions::Action` for integrated actions
509
+ #
510
+ # @since 2.0.0
511
+ # @api private
512
+ def build_response(request:, config:, content_type:, env:, headers:, session_enabled:, charset: nil,
513
+ view_options: nil)
514
+ Response.new(request:, config:, content_type:, charset:, env:, headers:, session_enabled:, view_options:)
515
+ end
516
+
517
+ # @since 0.2.0
518
+ # @api private
519
+ def _reference_in_rack_errors(request, exception)
520
+ request.env[RACK_EXCEPTION] = exception
521
+
522
+ if errors = request.env[RACK_ERRORS]
523
+ errors.write(_dump_exception(exception))
524
+ errors.flush
525
+ end
526
+ end
527
+
528
+ # @since 0.2.0
529
+ # @api private
530
+ def _dump_exception(exception)
531
+ [[exception.class, exception.message].compact.join(": "), *exception.backtrace].join("\n\t")
532
+ end
533
+
534
+ # @since 0.1.0
535
+ # @api private
536
+ def _handle_exception(request, response, exception)
537
+ handler = exception_handler(exception)
538
+
539
+ if handler.nil?
540
+ _reference_in_rack_errors(request, exception)
541
+ raise exception
542
+ end
543
+
544
+ instance_exec(
545
+ request,
546
+ response,
547
+ exception,
548
+ &_exception_handler(handler)
549
+ )
550
+
551
+ nil
552
+ end
553
+
554
+ # @since 0.3.0
555
+ # @api private
556
+ def _exception_handler(handler)
557
+ if respond_to?(handler.to_s, true)
558
+ method(handler)
559
+ else
560
+ ->(*) { halt handler }
561
+ end
562
+ end
563
+
564
+ # @since 0.1.0
565
+ # @api private
566
+ def _run_before_callbacks(request, response)
567
+ config.before_callbacks.run(self, request, response)
568
+ nil
569
+ end
570
+
571
+ # @since 0.1.0
572
+ # @api private
573
+ def _run_after_callbacks(request, response)
574
+ config.after_callbacks.run(self, request, response)
575
+ nil
576
+ end
577
+
578
+ # According to RFC 2616, when a response MUST have an empty body, it only
579
+ # allows Entity Headers.
580
+ #
581
+ # For instance, a <tt>204</tt> doesn't allow <tt>Content-Type</tt> or any
582
+ # other custom header.
583
+ #
584
+ # This restriction is enforced by <tt>Hanami::Action#_requires_no_body?</tt>.
585
+ #
586
+ # However, there are cases that demand to bypass this rule to set meta
587
+ # informations via headers.
588
+ #
589
+ # An example is a <tt>DELETE</tt> request for a JSON API application.
590
+ # It returns a <tt>204</tt> but still wants to specify the rate limit
591
+ # quota via <tt>X-Rate-Limit</tt>.
592
+ #
593
+ # @since 0.5.0
594
+ #
595
+ # @see Hanami::Action#_requires_no_body?
596
+ #
597
+ # @example
598
+ # require "hanami/action"
599
+ #
600
+ # module Books
601
+ # class Destroy < Hanami::Action
602
+ # def handle(*, response)
603
+ # # ...
604
+ # response.headers.merge!(
605
+ # "Last-Modified" => "Fri, 27 Nov 2015 13:32:36 GMT",
606
+ # "X-Rate-Limit" => "4000",
607
+ # "Content-Type" => "application/json",
608
+ # "X-No-Pass" => "true"
609
+ # )
610
+ #
611
+ # response.status = 204
612
+ # end
613
+ #
614
+ # private
615
+ #
616
+ # def keep_response_header?(header)
617
+ # super || header == "X-Rate-Limit"
618
+ # end
619
+ # end
620
+ # end
621
+ #
622
+ # # Only the following headers will be sent:
623
+ # # * Last-Modified - because we used `super' in the method that respects the HTTP RFC
624
+ # # * X-Rate-Limit - because we explicitely allow it
625
+ #
626
+ # # Both Content-Type and X-No-Pass are removed because they're not allowed
627
+ def keep_response_header?(header)
628
+ ENTITY_HEADERS.include?(header.downcase)
629
+ end
630
+
631
+ # @since 2.0.0
632
+ # @api private
633
+ def _empty_headers(response)
634
+ response.headers.select! { |header, _| keep_response_header?(header) }
635
+ end
636
+
637
+ # @since 2.0.0
638
+ # @api private
639
+ def _empty_body(response)
640
+ response.body = Response::EMPTY_BODY
641
+ end
642
+
643
+ # Finalize the response
644
+ #
645
+ # Prepare the data before the response will be returned to the webserver
646
+ #
647
+ # @since 0.1.0
648
+ # @api private
649
+ # @abstract
650
+ #
651
+ # @see Hanami::Action::Session#finish
652
+ # @see Hanami::Action::Cookies#finish
653
+ # @see Hanami::Action::Cache#finish
654
+ def finish(request, response, halted)
655
+ response.status, response.body = *halted unless halted.nil?
656
+
657
+ _empty_headers(response) if _requires_empty_headers?(response)
658
+ _empty_body(response) if response.head?
659
+
660
+ format =
661
+ if response.content_type == default_response_content_type
662
+ default_response_format
663
+ else
664
+ Mime.format_from_media_type(response.content_type, config)
665
+ end
666
+ response.set_format(format)
667
+ response[:params] = request.params
668
+ response[:format] = response.format
669
+ response
670
+ end
671
+ end
672
+ end