hanami-controller 1.3.3 → 2.0.0.alpha4

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +299 -537
  5. data/hanami-controller.gemspec +4 -3
  6. data/lib/hanami/action/application_action.rb +131 -0
  7. data/lib/hanami/action/application_configuration/content_security_policy.rb +118 -0
  8. data/lib/hanami/action/application_configuration/cookies.rb +29 -0
  9. data/lib/hanami/action/application_configuration/sessions.rb +46 -0
  10. data/lib/hanami/action/application_configuration.rb +90 -0
  11. data/lib/hanami/action/base_params.rb +2 -2
  12. data/lib/hanami/action/cache/cache_control.rb +4 -4
  13. data/lib/hanami/action/cache/conditional_get.rb +3 -1
  14. data/lib/hanami/action/cache/directives.rb +1 -1
  15. data/lib/hanami/action/cache/expires.rb +3 -3
  16. data/lib/hanami/action/cache.rb +1 -139
  17. data/lib/hanami/action/configuration.rb +428 -0
  18. data/lib/hanami/action/cookie_jar.rb +3 -3
  19. data/lib/hanami/action/cookies.rb +3 -62
  20. data/lib/hanami/action/csrf_protection.rb +214 -0
  21. data/lib/hanami/action/flash.rb +102 -207
  22. data/lib/hanami/action/glue.rb +5 -31
  23. data/lib/hanami/action/halt.rb +12 -0
  24. data/lib/hanami/action/mime.rb +78 -485
  25. data/lib/hanami/action/params.rb +2 -2
  26. data/lib/hanami/action/rack/file.rb +1 -1
  27. data/lib/hanami/action/request.rb +30 -20
  28. data/lib/hanami/action/response.rb +193 -0
  29. data/lib/hanami/action/session.rb +11 -128
  30. data/lib/hanami/action/standalone_action.rb +578 -0
  31. data/lib/hanami/action/validatable.rb +1 -1
  32. data/lib/hanami/action/view_name_inferrer.rb +46 -0
  33. data/lib/hanami/action.rb +129 -73
  34. data/lib/hanami/controller/version.rb +1 -1
  35. data/lib/hanami/controller.rb +0 -227
  36. data/lib/hanami/http/status.rb +2 -2
  37. metadata +44 -26
  38. data/lib/hanami/action/callable.rb +0 -92
  39. data/lib/hanami/action/callbacks.rb +0 -214
  40. data/lib/hanami/action/configurable.rb +0 -50
  41. data/lib/hanami/action/exposable/guard.rb +0 -104
  42. data/lib/hanami/action/exposable.rb +0 -126
  43. data/lib/hanami/action/head.rb +0 -121
  44. data/lib/hanami/action/rack/callable.rb +0 -47
  45. data/lib/hanami/action/rack/errors.rb +0 -53
  46. data/lib/hanami/action/rack.rb +0 -411
  47. data/lib/hanami/action/redirect.rb +0 -59
  48. data/lib/hanami/action/throwable.rb +0 -169
  49. data/lib/hanami/controller/configuration.rb +0 -763
  50. data/lib/hanami-controller.rb +0 -1
@@ -17,13 +17,14 @@ Gem::Specification.new do |spec|
17
17
  spec.executables = []
18
18
  spec.test_files = spec.files.grep(%r{^(spec)/})
19
19
  spec.require_paths = ['lib']
20
- spec.required_ruby_version = '>= 2.3.0'
20
+ spec.required_ruby_version = '>= 2.6.0'
21
21
 
22
22
  spec.add_dependency 'rack', '~> 2.0'
23
- spec.add_dependency 'hanami-utils', '~> 1.3'
23
+ spec.add_dependency 'hanami-utils', '~> 2.0.alpha'
24
+ spec.add_dependency 'dry-configurable', '~> 0.13', '>= 0.13.0'
24
25
 
25
26
  spec.add_development_dependency 'bundler', '>= 1.6', '< 3'
26
27
  spec.add_development_dependency 'rack-test', '~> 1.0'
27
28
  spec.add_development_dependency 'rake', '~> 13'
28
- spec.add_development_dependency 'rspec', '~> 3.7'
29
+ spec.add_development_dependency 'rspec', '~> 3.9'
29
30
  end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Action
5
+ class ApplicationAction < Module
6
+ attr_reader :provider
7
+ attr_reader :application
8
+
9
+ def initialize(provider)
10
+ @provider = provider
11
+ @application = provider.respond_to?(:application) ? provider.application : Hanami.application
12
+ end
13
+
14
+ def included(action_class)
15
+ action_class.include InstanceMethods
16
+
17
+ define_initialize
18
+ configure_action action_class
19
+ extend_behavior action_class
20
+ end
21
+
22
+ def inspect
23
+ "#<#{self.class.name}[#{provider.name}]>"
24
+ end
25
+
26
+ private
27
+
28
+ def define_initialize
29
+ resolve_view = method(:resolve_paired_view)
30
+ resolve_context = method(:resolve_view_context)
31
+ resolve_routes = method(:resolve_routes)
32
+
33
+ define_method :initialize do |**deps|
34
+ # Conditionally assign these to repsect any explictly auto-injected
35
+ # dependencies provided by the class
36
+ @view ||= deps[:view] || resolve_view.(self.class)
37
+ @view_context ||= deps[:view_context] || resolve_context.()
38
+ @routes ||= deps[:routes] || resolve_routes.()
39
+
40
+ super(**deps)
41
+ end
42
+ end
43
+
44
+ def resolve_paired_view(action_class)
45
+ view_identifiers = application.config.actions.view_name_inferrer.(
46
+ action_name: action_class.name,
47
+ provider: provider
48
+ )
49
+
50
+ view_identifiers.detect { |identifier|
51
+ break provider[identifier] if provider.key?(identifier)
52
+ }
53
+ end
54
+
55
+ def resolve_view_context
56
+ identifier = application.config.actions.view_context_identifier
57
+
58
+ if provider.key?(identifier)
59
+ provider[identifier]
60
+ elsif application.key?(identifier)
61
+ application[identifier]
62
+ end
63
+ end
64
+
65
+ def resolve_routes
66
+ application[:routes_helper] if application.key?(:routes_helper)
67
+ end
68
+
69
+ def configure_action(action_class)
70
+ action_class.config.settings.each do |setting|
71
+ application_value = application.config.actions.public_send(:"#{setting}")
72
+ action_class.config.public_send :"#{setting}=", application_value
73
+ end
74
+ end
75
+
76
+ def extend_behavior(action_class)
77
+ if application.config.actions.sessions.enabled?
78
+ require "hanami/action/session"
79
+ action_class.include Hanami::Action::Session
80
+ end
81
+
82
+ if application.config.actions.csrf_protection
83
+ require "hanami/action/csrf_protection"
84
+ action_class.include Hanami::Action::CSRFProtection
85
+ end
86
+
87
+ if application.config.actions.cookies.enabled?
88
+ require "hanami/action/cookies"
89
+ action_class.include Hanami::Action::Cookies
90
+ end
91
+ end
92
+
93
+ module InstanceMethods
94
+ attr_reader :view
95
+ attr_reader :view_context
96
+ attr_reader :routes
97
+
98
+ def build_response(**options)
99
+ options = options.merge(view_options: method(:view_options))
100
+ super(**options)
101
+ end
102
+
103
+ def view_options(req, res)
104
+ {context: view_context&.with(**view_context_options(req, res))}.compact
105
+ end
106
+
107
+ def view_context_options(req, res)
108
+ {request: req, response: res}
109
+ end
110
+
111
+ def finish(req, res, halted)
112
+ res.render(view, **req.params) if render?(res)
113
+ super
114
+ end
115
+
116
+ # Decide whether to render the current response with the associated view.
117
+ # This can be overridden to enable/disable automatic rendering.
118
+ #
119
+ # @param res [Hanami::Action::Response]
120
+ #
121
+ # @return [TrueClass,FalseClass]
122
+ #
123
+ # @since 2.0.0
124
+ # @api public
125
+ def render?(res)
126
+ view && res.body.empty?
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Action
5
+ class ApplicationConfiguration
6
+ # Configuration for Content Security Policy in Hanami applications
7
+ #
8
+ # @since 2.0.0
9
+ class ContentSecurityPolicy
10
+ # @since 2.0.0
11
+ # @api private
12
+ def initialize(&blk)
13
+ @policy = {
14
+ base_uri: "'self'",
15
+ child_src: "'self'",
16
+ connect_src: "'self'",
17
+ default_src: "'none'",
18
+ font_src: "'self'",
19
+ form_action: "'self'",
20
+ frame_ancestors: "'self'",
21
+ frame_src: "'self'",
22
+ img_src: "'self' https: data:",
23
+ media_src: "'self'",
24
+ object_src: "'none'",
25
+ plugin_types: "application/pdf",
26
+ script_src: "'self'",
27
+ style_src: "'self' 'unsafe-inline' https:"
28
+ }
29
+
30
+ blk&.(self)
31
+ end
32
+
33
+ # @since 2.0.0
34
+ # @api private
35
+ def initialize_copy(original_object)
36
+ @policy = original_object.instance_variable_get(:@policy).dup
37
+ super
38
+ end
39
+
40
+ # Get a CSP setting
41
+ #
42
+ # @param key [Symbol] the underscored name of the CPS setting
43
+ # @return [String,NilClass] the CSP setting, if any
44
+ #
45
+ # @since 2.0.0
46
+ # @api public
47
+ #
48
+ # @example
49
+ # module MyApp
50
+ # class Application < Hanami::Application
51
+ # config.actions.content_security_policy[:base_uri] # => "'self'"
52
+ # end
53
+ # end
54
+ def [](key)
55
+ @policy[key]
56
+ end
57
+
58
+ # Set a CSP setting
59
+ #
60
+ # @param key [Symbol] the underscored name of the CPS setting
61
+ # @param value [String] the CSP setting value
62
+ #
63
+ # @since 2.0.0
64
+ # @api public
65
+ #
66
+ # @example Replace a default value
67
+ # module MyApp
68
+ # class Application < Hanami::Application
69
+ # config.actions.content_security_policy[:plugin_types] = nil
70
+ # end
71
+ # end
72
+ #
73
+ # @example Append to a default value
74
+ # module MyApp
75
+ # class Application < Hanami::Application
76
+ # config.actions.content_security_policy[:script_src] += " https://my.cdn.test"
77
+ # end
78
+ # end
79
+ def []=(key, value)
80
+ @policy[key] = value
81
+ end
82
+
83
+ # Deletes a CSP key
84
+ #
85
+ # @param key [Symbol] the underscored name of the CPS setting
86
+ #
87
+ # @since 2.0.0
88
+ # @api public
89
+ #
90
+ # @example
91
+ # module MyApp
92
+ # class Application < Hanami::Application
93
+ # config.actions.content_security_policy.delete(:object_src)
94
+ # end
95
+ # end
96
+ def delete(key)
97
+ @policy.delete(key)
98
+ end
99
+
100
+ # @since 2.0.0
101
+ # @api private
102
+ def to_str
103
+ @policy.map do |key, value|
104
+ "#{dasherize(key)} #{value}"
105
+ end.join(";\n")
106
+ end
107
+
108
+ private
109
+
110
+ # @since 2.0.0
111
+ # @api private
112
+ def dasherize(key)
113
+ key.to_s.gsub("_", "-")
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Action
5
+ class ApplicationConfiguration
6
+ # Wrapper for application-level configuration of HTTP cookies for Hanami actions.
7
+ # This decorates the hash of cookie options that is otherwise directly configurable
8
+ # on actions, and adds the `enabled?` method to allow `ApplicationAction` to
9
+ # determine whether to include the `Action::Cookies` module.
10
+ #
11
+ # @since 2.0.0
12
+ class Cookies
13
+ attr_reader :options
14
+
15
+ def initialize(options)
16
+ @options = options
17
+ end
18
+
19
+ def enabled?
20
+ !options.nil?
21
+ end
22
+
23
+ def to_h
24
+ options.to_h
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/constants"
4
+ require "hanami/utils/string"
5
+ require "hanami/utils/class"
6
+
7
+ module Hanami
8
+ class Action
9
+ class ApplicationConfiguration
10
+ # Configuration for HTTP sessions in Hanami actions
11
+ #
12
+ # @since 2.0.0
13
+ class Sessions
14
+ attr_reader :storage, :options
15
+
16
+ def initialize(storage = nil, *options)
17
+ @storage = storage
18
+ @options = options
19
+ end
20
+
21
+ def enabled?
22
+ !storage.nil?
23
+ end
24
+
25
+ def middleware
26
+ return [] if !enabled?
27
+
28
+ [[storage_middleware, options]]
29
+ end
30
+
31
+ private
32
+
33
+ def storage_middleware
34
+ require_storage
35
+
36
+ name = Utils::String.classify(storage)
37
+ Utils::Class.load!(name, ::Rack::Session)
38
+ end
39
+
40
+ def require_storage
41
+ require "rack/session/#{storage}"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "application_configuration/cookies"
4
+ require_relative "application_configuration/sessions"
5
+ require_relative "application_configuration/content_security_policy"
6
+ require_relative "configuration"
7
+ require_relative "view_name_inferrer"
8
+
9
+ module Hanami
10
+ class Action
11
+ class ApplicationConfiguration
12
+ include Dry::Configurable
13
+
14
+ setting :cookies, default: {}, constructor: -> options { Cookies.new(options) }
15
+ setting :sessions, constructor: proc { |storage, *options| Sessions.new(storage, *options) }
16
+ setting :csrf_protection
17
+
18
+ setting :name_inference_base, default: "actions"
19
+ setting :view_context_identifier, default: "view.context"
20
+ setting :view_name_inferrer, default: ViewNameInferrer
21
+ setting :view_name_inference_base, default: "views"
22
+
23
+ attr_accessor :content_security_policy
24
+
25
+ def initialize(*, **options)
26
+ super()
27
+
28
+ @base_configuration = Configuration.new
29
+ @content_security_policy = ContentSecurityPolicy.new do |csp|
30
+ if assets_server_url = options[:assets_server_url]
31
+ csp[:script_src] += " #{assets_server_url}"
32
+ csp[:style_src] += " #{assets_server_url}"
33
+ end
34
+ end
35
+
36
+ configure_defaults
37
+ end
38
+
39
+ def finalize!
40
+ # A nil value for `csrf_protection` means it has not been explicitly configured
41
+ # (neither true nor false), so we can default it to whether sessions are enabled
42
+ self.csrf_protection = sessions.enabled? if csrf_protection.nil?
43
+
44
+ if self.content_security_policy
45
+ self.default_headers["Content-Security-Policy"] = self.content_security_policy.to_str
46
+ end
47
+ end
48
+
49
+ # Returns the list of available settings
50
+ #
51
+ # @return [Set]
52
+ #
53
+ # @since 2.0.0
54
+ # @api private
55
+ def settings
56
+ base_configuration.settings + self.class.settings
57
+ end
58
+
59
+ private
60
+
61
+ attr_reader :base_configuration
62
+
63
+ # Apply defaults for base configuration settings
64
+ def configure_defaults
65
+ self.default_request_format = :html
66
+ self.default_response_format = :html
67
+
68
+ self.default_headers = {
69
+ "X-Frame-Options" => "DENY",
70
+ "X-Content-Type-Options" => "nosniff",
71
+ "X-XSS-Protection" => "1; mode=block"
72
+ }
73
+ end
74
+
75
+ def method_missing(name, *args, &block)
76
+ if config.respond_to?(name)
77
+ config.public_send(name, *args, &block)
78
+ elsif base_configuration.respond_to?(name)
79
+ base_configuration.public_send(name, *args, &block)
80
+ else
81
+ super
82
+ end
83
+ end
84
+
85
+ def respond_to_missing?(name, _incude_all = false)
86
+ config.respond_to?(name) || base_configuration.respond_to?(name) || super
87
+ end
88
+ end
89
+ end
90
+ end
@@ -2,7 +2,7 @@ require 'rack/request'
2
2
  require 'hanami/utils/hash'
3
3
 
4
4
  module Hanami
5
- module Action
5
+ class Action
6
6
  class BaseParams
7
7
  # The key that returns raw input from the Rack env
8
8
  #
@@ -173,7 +173,7 @@ module Hanami
173
173
  def _router_params(fallback = {})
174
174
  env.fetch(ROUTER_PARAMS) do
175
175
  if session = fallback.delete(RACK_SESSION) # rubocop:disable Lint/AssignmentInCondition
176
- fallback[RACK_SESSION] = Utils::Hash.new(session).symbolize!.to_hash
176
+ fallback[RACK_SESSION] = Utils::Hash.deep_symbolize(session)
177
177
  end
178
178
 
179
179
  fallback
@@ -1,7 +1,7 @@
1
1
  require 'hanami/action/cache/directives'
2
2
 
3
3
  module Hanami
4
- module Action
4
+ class Action
5
5
  module Cache
6
6
  # Module with Cache-Control logic
7
7
  #
@@ -37,7 +37,7 @@ module Hanami
37
37
  def cache_control_directives
38
38
  @cache_control_directives || Object.new.tap do |null_object|
39
39
  def null_object.headers
40
- Hash.new
40
+ ::Hash.new
41
41
  end
42
42
  end
43
43
  end
@@ -49,9 +49,9 @@ module Hanami
49
49
  # @api private
50
50
  #
51
51
  # @see Hanami::Action#finish
52
- def finish
52
+ def finish(_, res, _)
53
+ res.headers.merge!(self.class.cache_control_directives.headers) unless res.headers.include? HEADER
53
54
  super
54
- headers.merge!(self.class.cache_control_directives.headers) unless headers.include? HEADER
55
55
  end
56
56
 
57
57
  # Class which stores CacheControl values
@@ -1,5 +1,7 @@
1
+ require "hanami/utils/blank"
2
+
1
3
  module Hanami
2
- module Action
4
+ class Action
3
5
  module Cache
4
6
  # @since 0.3.0
5
7
  # @api private
@@ -1,5 +1,5 @@
1
1
  module Hanami
2
- module Action
2
+ class Action
3
3
  module Cache
4
4
  # Cache-Control directives which have values
5
5
  #
@@ -1,7 +1,7 @@
1
1
  require 'hanami/action/cache/cache_control'
2
2
 
3
3
  module Hanami
4
- module Action
4
+ class Action
5
5
  module Cache
6
6
  # Module with Expires logic
7
7
  #
@@ -49,9 +49,9 @@ module Hanami
49
49
  # @api private
50
50
  #
51
51
  # @see Hanami::Action#finish
52
- def finish
52
+ def finish(_, res, _)
53
+ res.headers.merge!(self.class.expires_directives.headers) unless res.headers.include? HEADER
53
54
  super
54
- headers.merge!(self.class.expires_directives.headers) unless headers.include? HEADER
55
55
  end
56
56
 
57
57
  # Class which stores Expires directives
@@ -3,7 +3,7 @@ require 'hanami/action/cache/expires'
3
3
  require 'hanami/action/cache/conditional_get'
4
4
 
5
5
  module Hanami
6
- module Action
6
+ class Action
7
7
  # Cache type API
8
8
  #
9
9
  # @since 0.3.0
@@ -26,144 +26,6 @@ module Hanami
26
26
  include CacheControl, Expires
27
27
  end
28
28
  end
29
-
30
- protected
31
-
32
- # Specify response freshness policy for HTTP caches (Cache-Control header).
33
- # Any number of non-value directives (:public, :private, :no_cache,
34
- # :no_store, :must_revalidate, :proxy_revalidate) may be passed along with
35
- # a Hash of value directives (:max_age, :min_stale, :s_max_age).
36
- #
37
- # See RFC 2616 / 14.9 for more on standard cache control directives:
38
- # http://tools.ietf.org/html/rfc2616#section-14.9.1
39
- #
40
- # @param values [Array<Symbols, Hash>] mapped to cache_control directives
41
- # @option values [Symbol] :public
42
- # @option values [Symbol] :private
43
- # @option values [Symbol] :no_cache
44
- # @option values [Symbol] :no_store
45
- # @option values [Symbol] :must_validate
46
- # @option values [Symbol] :proxy_revalidate
47
- # @option values [Hash] :max_age
48
- # @option values [Hash] :min_stale
49
- # @option values [Hash] :s_max_age
50
- #
51
- # @return void
52
- #
53
- # @since 0.3.0
54
- #
55
- # @example
56
- # require 'hanami/controller'
57
- # require 'hanami/action/cache'
58
- #
59
- # class Show
60
- # include Hanami::Action
61
- # include Hanami::Action::Cache
62
- #
63
- # def call(params)
64
- # # ...
65
- #
66
- # # set Cache-Control directives
67
- # cache_control :public, max_age: 900, s_maxage: 86400
68
- #
69
- # # overwrite previous Cache-Control directives
70
- # cache_control :private, :no_cache, :no_store
71
- #
72
- # => Cache-Control: private, no-store, max-age=900
73
- #
74
- # end
75
- # end
76
- def cache_control(*values)
77
- cache_control = CacheControl::Directives.new(*values)
78
- headers.merge!(cache_control.headers)
79
- end
80
-
81
- # Set the Expires header and Cache-Control/max-age directive. Amount
82
- # can be an integer number of seconds in the future or a Time object
83
- # indicating when the response should be considered "stale". The remaining
84
- # "values" arguments are passed to the #cache_control helper:
85
- #
86
- # @param amount [Integer,Time] number of seconds or point in time
87
- # @param values [Array<Symbols>] mapped to cache_control directives
88
- #
89
- # @return void
90
- #
91
- # @since 0.3.0
92
- #
93
- # @example
94
- # require 'hanami/controller'
95
- # require 'hanami/action/cache'
96
- #
97
- # class Show
98
- # include Hanami::Action
99
- # include Hanami::Action::Cache
100
- #
101
- # def call(params)
102
- # # ...
103
- #
104
- # # set Cache-Control directives and Expires
105
- # expires 900, :public
106
- #
107
- # # overwrite Cache-Control directives and Expires
108
- # expires 300, :private, :no_cache, :no_store
109
- #
110
- # => Expires: Thu, 26 Jun 2014 12:00:00 GMT
111
- # => Cache-Control: private, no-cache, no-store max-age=300
112
- #
113
- # end
114
- # end
115
- def expires(amount, *values)
116
- expires = Expires::Directives.new(amount, *values)
117
- headers.merge!(expires.headers)
118
- end
119
-
120
- # Set the etag, last_modified, or both headers on the response
121
- # and halts a 304 Not Modified if the request is still fresh
122
- # respecting IfNoneMatch and IfModifiedSince request headers
123
- #
124
- # @param options [Hash]
125
- # @option options [Integer] :etag for testing IfNoneMatch conditions
126
- # @option options [Date] :last_modified for testing IfModifiedSince conditions
127
- #
128
- # @return void
129
- #
130
- # @since 0.3.0
131
- #
132
- # @example
133
- # require 'hanami/controller'
134
- # require 'hanami/action/cache'
135
- #
136
- # class Show
137
- # include Hanami::Action
138
- # include Hanami::Action::Cache
139
- #
140
- # def call(params)
141
- # # ...
142
- #
143
- # # set etag response header and halt 304
144
- # # if request matches IF_NONE_MATCH header
145
- # fresh etag: @resource.updated_at.to_i
146
- #
147
- # # set last_modified response header and halt 304
148
- # # if request matches IF_MODIFIED_SINCE
149
- # fresh last_modified: @resource.updated_at
150
- #
151
- # # set etag and last_modified response header,
152
- # # halt 304 if request matches IF_MODIFIED_SINCE
153
- # # and IF_NONE_MATCH
154
- # fresh last_modified: @resource.updated_at
155
- #
156
- # end
157
- # end
158
- def fresh(options)
159
- conditional_get = ConditionalGet.new(@_env, options)
160
-
161
- headers.merge!(conditional_get.headers)
162
-
163
- conditional_get.fresh? do
164
- halt 304
165
- end
166
- end
167
29
  end
168
30
  end
169
31
  end