response.rb 10.6 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
    # Get headers for this response.
42
    attr_reader :header
43

44 45 46
    alias_method :headers,  :header

    delegate :[], :[]=, :to => :@header
47
    delegate :each, :to => :@stream
48

49 50 51
    CONTENT_TYPE = "Content-Type".freeze
    SET_COOKIE   = "Set-Cookie".freeze
    LOCATION     = "Location".freeze
52
    NO_CONTENT_CODES = [100, 101, 102, 204, 205, 304]
S
Santiago Pastorino 已提交
53

54
    cattr_accessor(:default_charset) { "utf-8" }
E
Egor Homakov 已提交
55
    cattr_accessor(:default_headers)
56

57
    include Rack::Response::Helpers
58
    include ActionDispatch::Http::FilterRedirect
59
    include ActionDispatch::Http::Cache::Response
60
    include MonitorMixin
61

62 63 64 65 66
    class Buffer # :nodoc:
      def initialize(response, buf)
        @response = response
        @buf      = buf
        @closed   = false
67 68 69 70 71 72 73 74 75
        @str_body = nil
      end

      def body
        @str_body ||= begin
                        buf = ''
                        each { |chunk| buf << chunk }
                        buf
                      end
76 77 78 79 80
      end

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

81
        @str_body = nil
82 83 84 85 86
        @response.commit!
        @buf.push string
      end

      def each(&block)
87 88 89 90
        @response.sending!
        x = @buf.each(&block)
        @response.sent!
        x
91 92
      end

93 94 95
      def abort
      end

96 97 98 99 100 101 102 103 104 105
      def close
        @response.commit!
        @closed = true
      end

      def closed?
        @closed
      end
    end

106
    # The underlying body, as a streamable object.
107 108
    attr_reader :stream

109
    def initialize(status = 200, header = {}, body = [], default_headers: self.class.default_headers)
110 111
      super()

112
      header = merge_default_headers(header, default_headers)
113
      @header = header
E
Egor Homakov 已提交
114

115
      self.body, self.status = body, status
116

117 118 119
      @blank        = false
      @cv           = new_cond
      @committed    = false
120 121
      @sending      = false
      @sent         = false
J
Joshua Peek 已提交
122

123 124 125 126
      prepare_cache_control!

      yield self if block_given?
    end
127

128 129 130 131 132
    def have_header?(key);  headers.key? key;   end
    def get_header(key);    headers[key];       end
    def set_header(key, v); headers[key] = v;   end
    def delete_header(key); headers.delete key; end

133 134 135 136 137 138
    def await_commit
      synchronize do
        @cv.wait_until { @committed }
      end
    end

139 140 141 142
    def await_sent
      synchronize { @cv.wait_until { @sent } }
    end

143 144
    def commit!
      synchronize do
145
        before_committed
146 147 148 149 150
        @committed = true
        @cv.broadcast
      end
    end

151 152 153 154 155 156
    def sending!
      synchronize do
        before_sending
        @sending = true
        @cv.broadcast
      end
157 158
    end

159 160 161 162 163 164 165 166 167 168 169
    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

170
    # Sets the HTTP status code.
171
    def status=(status)
172
      @status = Rack::Utils.status_code(status)
173 174
    end

175
    # Sets the HTTP content type.
S
Santiago Pastorino 已提交
176
    def content_type=(content_type)
177
      header_info = parse_content_type(get_header(CONTENT_TYPE))
A
Aaron Patterson 已提交
178
      set_content_type content_type.to_s, header_info.charset || self.class.default_charset
179 180
    end

A
Aaron Patterson 已提交
181 182 183 184 185 186 187 188
    # 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.
189

A
Aaron Patterson 已提交
190 191 192
    def content_type
      type = parse_content_type(get_header(CONTENT_TYPE)).mime_type
      type && type.to_s
193 194 195 196 197 198
    end

    def sending_file=(v)
      if true == v
        self.charset = false
      end
S
Santiago Pastorino 已提交
199 200
    end

201 202 203 204
    # Sets the HTTP character set. In case of nil parameter
    # it sets the charset to utf-8.
    #
    #   response.charset = 'utf-16' # => 'utf-16'
205
    #   response.charset = nil      # => 'utf-8'
206
    def charset=(charset)
207 208
      header_info = parse_content_type(get_header(CONTENT_TYPE))
      if false == charset
209
        set_header CONTENT_TYPE, header_info.mime_type
210
      else
211
        content_type = header_info.mime_type
212
        set_content_type content_type, charset || self.class.default_charset
213
      end
214 215
    end

A
Aaron Patterson 已提交
216 217 218 219 220 221 222
    # 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.
    def charset
      header_info = parse_content_type(get_header(CONTENT_TYPE))
      header_info.charset || self.class.default_charset
    end

223
    # The response code of the request.
224
    def response_code
225
      @status
226 227
    end

228
    # Returns a string to ensure compatibility with <tt>Net::HTTPResponse</tt>.
229
    def code
230
      @status.to_s
231 232
    end

233 234
    # Returns the corresponding message for the current HTTP status code:
    #
235 236 237 238 239
    #   response.status = 200
    #   response.message # => "OK"
    #
    #   response.status = 404
    #   response.message # => "Not Found"
240
    #
241
    def message
242
      Rack::Utils::HTTP_STATUS_CODES[@status]
243
    end
244
    alias_method :status_message, :message
245

246
    # Returns the content of the response as a string. This contains the contents
247
    # of any calls to <tt>render</tt>.
248
    def body
249
      @stream.body
250
    end
251

Y
Yehuda Katz 已提交
252 253
    EMPTY = " "

254
    # Allows you to manually set or override the response body.
255
    def body=(body)
Y
Yehuda Katz 已提交
256
      @blank = true if body == EMPTY
257

258 259 260
      if body.respond_to?(:to_path)
        @stream = body
      else
261 262 263
        synchronize do
          @stream = build_buffer self, munge_body_object(body)
        end
264
      end
265 266 267
    end

    def body_parts
268 269 270
      parts = []
      @stream.each { |x| parts << x }
      parts
D
Initial  
David Heinemeier Hansson 已提交
271 272
    end

273
    # The location header we'll be responding with.
274
    alias_method :redirect_url, :location
275

276
    def close
277
      stream.close if stream.respond_to?(:close)
278 279
    end

280 281 282 283 284 285 286 287 288 289 290
    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

291
    # Turns the Response into a Rack-compatible array of the status, headers,
292
    # and body. Allows explicit splatting:
293 294
    #
    #   status, headers, body = *response
295
    def to_a
296
      commit!
297
      rack_response @status, @header.to_hash
298
    end
299
    alias prepare! to_a
300

301 302 303 304 305
    # Returns the response cookies, converted to a Hash of (name => value) pairs
    #
    #   assert_equal 'AuthorOfNewPage', r.cookies['author']
    def cookies
      cookies = {}
306
      if header = get_header(SET_COOKIE)
307 308 309 310 311 312 313
        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
314 315 316 317
      end
      cookies
    end

318
  private
J
Joshua Peek 已提交
319

A
Aaron Patterson 已提交
320 321 322 323 324
    ContentTypeHeader = Struct.new :mime_type, :charset

    def parse_content_type(content_type)
      if content_type
        type, charset = content_type.split(/;\s*charset=/)
325
        type = nil if type.empty?
326
        ContentTypeHeader.new(type, charset)
A
Aaron Patterson 已提交
327 328 329 330 331 332
      else
        ContentTypeHeader.new(nil, nil)
      end
    end

    def set_content_type(content_type, charset)
333
      type = (content_type || '').dup
A
Aaron Patterson 已提交
334 335 336 337
      type << "; charset=#{charset}" if charset
      set_header CONTENT_TYPE, type
    end

338
    def before_committed
339 340 341
      return if committed?
      assign_default_content_type_and_charset!
      handle_conditional_get!
342 343 344
    end

    def before_sending
345 346
    end

347
    def merge_default_headers(original, default)
348
      default.respond_to?(:merge) ? default.merge(original) : original
349 350
    end

351 352 353 354 355 356 357 358
    def build_buffer(response, body)
      Buffer.new response, body
    end

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

359
    def assign_default_content_type_and_charset!
360
      return if content_type
J
Joshua Peek 已提交
361

362
      ct = parse_content_type get_header(CONTENT_TYPE)
A
Aaron Patterson 已提交
363 364
      set_content_type(ct.mime_type || Mime::HTML.to_s,
                       ct.charset || self.class.default_charset)
365
    end
366

367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
    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
397 398 399 400

      def to_ary
        nil
      end
401 402
    end

403
    def rack_response(status, header)
404
      if NO_CONTENT_CODES.include?(@status)
405
        header.delete CONTENT_TYPE
406
        header.delete 'Content-Length'
407 408
        [status, header, []]
      else
409
        [status, header, RackBody.new(self)]
410 411
      end
    end
D
Initial  
David Heinemeier Hansson 已提交
412
  end
413
end