response.rb 9.9 KB
Newer Older
1
require 'active_support/core_ext/module/attribute_accessors'
2
require 'action_dispatch/http/filter_redirect'
3
require 'monitor'
4

5
module ActionDispatch # :nodoc:
6 7 8 9 10
  # Represents an HTTP response generated by a controller action. Use it to
  # retrieve the current state of the response, or customize the response. It can
  # either represent a real HTTP response (i.e. one that is meant to be sent
  # back to the web browser) or a TestResponse (i.e. one that is generated
  # from integration tests).
P
Pratik Naik 已提交
11
  #
12
  # \Response is mostly a Ruby on \Rails framework implementation detail, and
13 14 15 16
  # should never be used directly in controllers. Controllers should use the
  # methods defined in ActionController::Base instead. For example, if you want
  # to set the HTTP response's content MIME type, then use
  # ActionControllerBase#headers instead of Response#headers.
P
Pratik Naik 已提交
17
  #
18
  # Nevertheless, integration tests may want to inspect controller responses in
19
  # more detail, and that's when \Response can be useful for application
20
  # developers. Integration test methods such as
J
Joshua Peek 已提交
21 22
  # ActionDispatch::Integration::Session#get and
  # ActionDispatch::Integration::Session#post return objects of type
23
  # TestResponse (which are of course also of type \Response).
P
Pratik Naik 已提交
24
  #
25
  # For example, the following demo integration test prints the body of the
P
Pratik Naik 已提交
26 27
  # controller response to the console:
  #
J
Joshua Peek 已提交
28
  #  class DemoControllerTest < ActionDispatch::IntegrationTest
P
Pratik Naik 已提交
29 30
  #    def test_print_root_path_to_console
  #      get('/')
A
Alexey Vakhov 已提交
31
  #      puts response.body
P
Pratik Naik 已提交
32 33
  #    end
  #  end
34
  class Response
35 36 37 38
    # The request that the response is responding to.
    attr_accessor :request

    # The HTTP status code.
39
    attr_reader :status
40

41
    attr_writer :sending_file
P
Pratik Naik 已提交
42

43 44
    # Get and set headers for this response.
    attr_accessor :header
45

46
    alias_method :headers=, :header=
47 48 49
    alias_method :headers,  :header

    delegate :[], :[]=, :to => :@header
50
    delegate :each, :to => :@stream
51 52 53 54 55 56 57 58 59

    # Sets the HTTP response's content MIME type. For example, in the controller
    # you could write this:
    #
    #  response.content_type = "text/plain"
    #
    # If a character set has been defined for this response (see charset=) then
    # the character set information will also be included in the content type
    # information.
S
Santiago Pastorino 已提交
60
    attr_reader   :content_type
61

62 63 64 65
    # The charset of the response. HTML wants to know the encoding of the
    # content you're giving them, so we need to send that along.
    attr_accessor :charset

66 67 68
    CONTENT_TYPE = "Content-Type".freeze
    SET_COOKIE   = "Set-Cookie".freeze
    LOCATION     = "Location".freeze
69
    NO_CONTENT_CODES = [204, 304]
S
Santiago Pastorino 已提交
70

71
    cattr_accessor(:default_charset) { "utf-8" }
E
Egor Homakov 已提交
72
    cattr_accessor(:default_headers)
73

74
    include Rack::Response::Helpers
75
    include ActionDispatch::Http::FilterRedirect
76
    include ActionDispatch::Http::Cache::Response
77
    include MonitorMixin
78

79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
    class Buffer # :nodoc:
      def initialize(response, buf)
        @response = response
        @buf      = buf
        @closed   = false
      end

      def write(string)
        raise IOError, "closed stream" if closed?

        @response.commit!
        @buf.push string
      end

      def each(&block)
94 95 96 97
        @response.sending!
        x = @buf.each(&block)
        @response.sent!
        x
98 99
      end

100 101 102
      def abort
      end

103 104 105 106 107 108 109 110 111 112
      def close
        @response.commit!
        @closed = true
      end

      def closed?
        @closed
      end
    end

113
    # The underlying body, as a streamable object.
114 115
    attr_reader :stream

116 117 118
    # Ruby 2.2 bug https://bugs.ruby-lang.org/issues/10685 prevents
    # default_headers from being a keyword argument.
    def initialize(status = 200, header = {}, body = [], default_headers = self.class.default_headers)
119 120
      super()

121
      header = merge_default_headers(header, default_headers)
E
Egor Homakov 已提交
122

123
      self.body, self.header, self.status = body, header, status
124

125
      @sending_file = false
126 127 128
      @blank        = false
      @cv           = new_cond
      @committed    = false
129 130
      @sending      = false
      @sent         = false
131 132
      @content_type = nil
      @charset      = nil
J
Joshua Peek 已提交
133

134
      if content_type = self[CONTENT_TYPE]
135 136
        type, charset = content_type.split(/;\s*charset=/)
        @content_type = Mime::Type.lookup(type)
137
        @charset = charset || self.class.default_charset
138
      end
139

140 141 142 143
      prepare_cache_control!

      yield self if block_given?
    end
144

145 146 147 148 149 150
    def await_commit
      synchronize do
        @cv.wait_until { @committed }
      end
    end

151 152 153 154
    def await_sent
      synchronize { @cv.wait_until { @sent } }
    end

155 156
    def commit!
      synchronize do
157
        before_committed
158 159 160 161 162
        @committed = true
        @cv.broadcast
      end
    end

163 164 165 166 167 168
    def sending!
      synchronize do
        before_sending
        @sending = true
        @cv.broadcast
      end
169 170
    end

171 172 173 174 175 176 177 178 179 180 181
    def sent!
      synchronize do
        @sent = true
        @cv.broadcast
      end
    end

    def sending?;   synchronize { @sending };   end
    def committed?; synchronize { @committed }; end
    def sent?;      synchronize { @sent };      end

182
    # Sets the HTTP status code.
183
    def status=(status)
184
      @status = Rack::Utils.status_code(status)
185 186
    end

187
    # Sets the HTTP content type.
S
Santiago Pastorino 已提交
188 189 190 191
    def content_type=(content_type)
      @content_type = content_type.to_s
    end

192
    # The response code of the request.
193
    def response_code
194
      @status
195 196
    end

197
    # Returns a string to ensure compatibility with <tt>Net::HTTPResponse</tt>.
198
    def code
199
      @status.to_s
200 201
    end

202 203
    # Returns the corresponding message for the current HTTP status code:
    #
204 205 206 207 208
    #   response.status = 200
    #   response.message # => "OK"
    #
    #   response.status = 404
    #   response.message # => "Not Found"
209
    #
210
    def message
211
      Rack::Utils::HTTP_STATUS_CODES[@status]
212
    end
213
    alias_method :status_message, :message
214

215
    # Returns the content of the response as a string. This contains the contents
216
    # of any calls to <tt>render</tt>.
217
    def body
218 219 220
      strings = []
      each { |part| strings << part.to_s }
      strings.join
221
    end
222

Y
Yehuda Katz 已提交
223 224
    EMPTY = " "

225
    # Allows you to manually set or override the response body.
226
    def body=(body)
Y
Yehuda Katz 已提交
227
      @blank = true if body == EMPTY
228

229 230 231
      if body.respond_to?(:to_path)
        @stream = body
      else
232 233 234
        synchronize do
          @stream = build_buffer self, munge_body_object(body)
        end
235
      end
236 237 238
    end

    def body_parts
239 240 241
      parts = []
      @stream.each { |x| parts << x }
      parts
D
Initial  
David Heinemeier Hansson 已提交
242 243
    end

244 245 246 247 248 249 250 251
    def set_cookie(key, value)
      ::Rack::Utils.set_cookie_header!(header, key, value)
    end

    def delete_cookie(key, value={})
      ::Rack::Utils.delete_cookie_header!(header, key, value)
    end

252
    # The location header we'll be responding with.
253
    def location
254
      headers[LOCATION]
255 256
    end
    alias_method :redirect_url, :location
257

258
    # Sets the location header we'll be responding with.
259
    def location=(url)
260
      headers[LOCATION] = url
261
    end
262

263
    def close
264
      stream.close if stream.respond_to?(:close)
265 266
    end

267 268 269 270 271 272 273 274 275 276 277
    def abort
      if stream.respond_to?(:abort)
        stream.abort
      elsif stream.respond_to?(:close)
        # `stream.close` should really be reserved for a close from the
        # other direction, but we must fall back to it for
        # compatibility.
        stream.close
      end
    end

278
    # Turns the Response into a Rack-compatible array of the status, headers,
279 280 281
    # and body. Allows explict splatting:
    #
    #   status, headers, body = *response
282
    def to_a
283
      rack_response @status, @header.to_hash
284
    end
285
    alias prepare! to_a
286

287 288 289 290 291
    # Returns the response cookies, converted to a Hash of (name => value) pairs
    #
    #   assert_equal 'AuthorOfNewPage', r.cookies['author']
    def cookies
      cookies = {}
292
      if header = self[SET_COOKIE]
293 294 295 296 297 298 299
        header = header.split("\n") if header.respond_to?(:to_str)
        header.each do |cookie|
          if pair = cookie.split(';').first
            key, value = pair.split("=").map { |v| Rack::Utils.unescape(v) }
            cookies[key] = value
          end
        end
300 301 302 303
      end
      cookies
    end

304
  private
J
Joshua Peek 已提交
305

306 307 308 309
    def before_committed
    end

    def before_sending
310 311
    end

312
    def merge_default_headers(original, default)
313
      default.respond_to?(:merge) ? default.merge(original) : original
314 315
    end

316 317 318 319 320 321 322 323
    def build_buffer(response, body)
      Buffer.new response, body
    end

    def munge_body_object(body)
      body.respond_to?(:each) ? body : [body]
    end

324
    def assign_default_content_type_and_charset!(headers)
325
      return if headers[CONTENT_TYPE].present?
J
Joshua Peek 已提交
326

327
      @content_type ||= Mime::HTML
328
      @charset      ||= self.class.default_charset unless @charset == false
J
Joshua Peek 已提交
329

330
      type = @content_type.to_s.dup
331
      type << "; charset=#{@charset}" if append_charset?
J
Joshua Peek 已提交
332

333 334
      headers[CONTENT_TYPE] = type
    end
335

336 337 338 339
    def append_charset?
      !@sending_file && @charset != false
    end

340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369
    class RackBody
      def initialize(response)
        @response = response
      end

      def each(*args, &block)
        @response.each(*args, &block)
      end

      def close
        # Rack "close" maps to Response#abort, and *not* Response#close
        # (which is used when the controller's finished writing)
        @response.abort
      end

      def body
        @response.body
      end

      def respond_to?(method, include_private = false)
        if method.to_s == 'to_path'
          @response.stream.respond_to?(method)
        else
          super
        end
      end

      def to_path
        @response.stream.to_path
      end
370 371 372 373

      def to_ary
        nil
      end
374 375
    end

376
    def rack_response(status, header)
377
      assign_default_content_type_and_charset!(header)
378 379 380 381
      handle_conditional_get!

      header[SET_COOKIE] = header[SET_COOKIE].join("\n") if header[SET_COOKIE].respond_to?(:join)

382
      if NO_CONTENT_CODES.include?(@status)
383 384 385
        header.delete CONTENT_TYPE
        [status, header, []]
      else
386
        [status, header, RackBody.new(self)]
387 388
      end
    end
D
Initial  
David Heinemeier Hansson 已提交
389
  end
390
end