response.rb 10.7 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 133 134 135 136 137 138 139 140 141
    # 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.

    def content_type
      type = parse_content_type(self[CONTENT_TYPE]).mime_type
      type && type.to_s
    end

142 143 144 145 146
    ContentTypeHeader = Struct.new :mime_type, :charset

    def parse_content_type(content_type)
      if content_type
        type, charset = content_type.split(/;\s*charset=/)
147
        ContentTypeHeader.new(Mime::Type.lookup(type), charset)
148
      else
149
        ContentTypeHeader.new(nil, nil)
150 151 152
      end
    end

153 154 155 156 157
    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

158 159 160 161 162 163
    def await_commit
      synchronize do
        @cv.wait_until { @committed }
      end
    end

164 165 166 167
    def await_sent
      synchronize { @cv.wait_until { @sent } }
    end

168 169
    def commit!
      synchronize do
170
        before_committed
171 172 173 174 175
        @committed = true
        @cv.broadcast
      end
    end

176 177 178 179 180 181
    def sending!
      synchronize do
        before_sending
        @sending = true
        @cv.broadcast
      end
182 183
    end

184 185 186 187 188 189 190 191 192 193 194
    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

195
    # Sets the HTTP status code.
196
    def status=(status)
197
      @status = Rack::Utils.status_code(status)
198 199
    end

200
    # Sets the HTTP content type.
S
Santiago Pastorino 已提交
201
    def content_type=(content_type)
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
      header_info = parse_content_type(get_header(CONTENT_TYPE))
      type = content_type.to_s.dup
      type << "; charset=#{header_info.charset || self.class.default_charset}"

      set_header CONTENT_TYPE, type
    end

    # 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

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

223 224 225 226
    # Sets the HTTP character set. In case of nil parameter
    # it sets the charset to utf-8.
    #
    #   response.charset = 'utf-16' # => 'utf-16'
227
    #   response.charset = nil      # => 'utf-8'
228
    def charset=(charset)
229 230 231 232 233 234 235 236 237 238
      header_info = parse_content_type(get_header(CONTENT_TYPE))
      charset = charset.nil? ? self.class.default_charset : charset
      if false == charset
        set_header CONTENT_TYPE, header_info.mime_type.to_s
      else
        content_type = header_info.mime_type || Mime::TEXT
        type = content_type.to_s.dup
        type << "; charset=#{charset}"
        set_header CONTENT_TYPE, type
      end
239 240
    end

241
    # The response code of the request.
242
    def response_code
243
      @status
244 245
    end

246
    # Returns a string to ensure compatibility with <tt>Net::HTTPResponse</tt>.
247
    def code
248
      @status.to_s
249 250
    end

251 252
    # Returns the corresponding message for the current HTTP status code:
    #
253 254 255 256 257
    #   response.status = 200
    #   response.message # => "OK"
    #
    #   response.status = 404
    #   response.message # => "Not Found"
258
    #
259
    def message
260
      Rack::Utils::HTTP_STATUS_CODES[@status]
261
    end
262
    alias_method :status_message, :message
263

264
    # Returns the content of the response as a string. This contains the contents
265
    # of any calls to <tt>render</tt>.
266
    def body
267
      @stream.body
268
    end
269

Y
Yehuda Katz 已提交
270 271
    EMPTY = " "

272
    # Allows you to manually set or override the response body.
273
    def body=(body)
Y
Yehuda Katz 已提交
274
      @blank = true if body == EMPTY
275

276 277 278
      if body.respond_to?(:to_path)
        @stream = body
      else
279 280 281
        synchronize do
          @stream = build_buffer self, munge_body_object(body)
        end
282
      end
283 284 285
    end

    def body_parts
286 287 288
      parts = []
      @stream.each { |x| parts << x }
      parts
D
Initial  
David Heinemeier Hansson 已提交
289 290
    end

291
    # The location header we'll be responding with.
292
    alias_method :redirect_url, :location
293

294
    def close
295
      stream.close if stream.respond_to?(:close)
296 297
    end

298 299 300 301 302 303 304 305 306 307 308
    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

309
    # Turns the Response into a Rack-compatible array of the status, headers,
310
    # and body. Allows explicit splatting:
311 312
    #
    #   status, headers, body = *response
313
    def to_a
314
      commit!
315
      rack_response @status, @header.to_hash
316
    end
317
    alias prepare! to_a
318

319 320 321 322 323
    # Returns the response cookies, converted to a Hash of (name => value) pairs
    #
    #   assert_equal 'AuthorOfNewPage', r.cookies['author']
    def cookies
      cookies = {}
324
      if header = get_header(SET_COOKIE)
325 326 327 328 329 330 331
        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
332 333 334 335
      end
      cookies
    end

336
  private
J
Joshua Peek 已提交
337

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 get_header(CONTENT_TYPE).present?
J
Joshua Peek 已提交
361

362 363 364 365 366
      ct = parse_content_type get_header(CONTENT_TYPE)
      content_type = ct.mime_type || Mime::HTML
      charset = ct.charset || self.class.default_charset
      type = content_type.to_s.dup
      type << "; charset=#{charset}"
J
Joshua Peek 已提交
367

368
      set_header CONTENT_TYPE, type
369
    end
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 397 398 399 400
    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
401 402 403 404

      def to_ary
        nil
      end
405 406
    end

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