response.rb 10.1 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
    # Get headers for this response.
44
    attr_reader :header
45

46 47 48
    alias_method :headers,  :header

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

    # 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 已提交
59
    attr_reader   :content_type
60

61 62
    # 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.
63
    attr_reader :charset
64

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

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

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

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

      def body
        @str_body ||= begin
                        buf = ''
                        each { |chunk| buf << chunk }
                        buf
                      end
92 93 94 95 96
      end

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

97
        @str_body = nil
98 99 100 101 102
        @response.commit!
        @buf.push string
      end

      def each(&block)
103 104 105 106
        @response.sending!
        x = @buf.each(&block)
        @response.sent!
        x
107 108
      end

109 110 111
      def abort
      end

112 113 114 115 116 117 118 119 120 121
      def close
        @response.commit!
        @closed = true
      end

      def closed?
        @closed
      end
    end

122
    # The underlying body, as a streamable object.
123 124
    attr_reader :stream

125
    def initialize(status = 200, header = {}, body = [], default_headers: self.class.default_headers)
126 127
      super()

128
      header = merge_default_headers(header, default_headers)
129
      @header = header
E
Egor Homakov 已提交
130

131
      self.body, self.status = body, status
132

133
      @sending_file = false
134 135 136
      @blank        = false
      @cv           = new_cond
      @committed    = false
137 138
      @sending      = false
      @sent         = false
139
      @content_type = nil
140
      @charset      = self.class.default_charset
J
Joshua Peek 已提交
141

142
      if content_type = self[CONTENT_TYPE]
143 144
        type, charset = content_type.split(/;\s*charset=/)
        @content_type = Mime::Type.lookup(type)
145
        @charset = charset || self.class.default_charset
146
      end
147

148 149 150 151
      prepare_cache_control!

      yield self if block_given?
    end
152

153 154 155 156 157 158
    def await_commit
      synchronize do
        @cv.wait_until { @committed }
      end
    end

159 160 161 162
    def await_sent
      synchronize { @cv.wait_until { @sent } }
    end

163 164
    def commit!
      synchronize do
165
        before_committed
166 167 168 169 170
        @committed = true
        @cv.broadcast
      end
    end

171 172 173 174 175 176
    def sending!
      synchronize do
        before_sending
        @sending = true
        @cv.broadcast
      end
177 178
    end

179 180 181 182 183 184 185 186 187 188 189
    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

190
    # Sets the HTTP status code.
191
    def status=(status)
192
      @status = Rack::Utils.status_code(status)
193 194
    end

195
    # Sets the HTTP content type.
S
Santiago Pastorino 已提交
196 197 198 199
    def content_type=(content_type)
      @content_type = content_type.to_s
    end

200 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'
    #   response.charset = nil # => 'utf-8'
205
    def charset=(charset)
206
      @charset = charset.nil? ? self.class.default_charset : charset
207 208
    end

209
    # The response code of the request.
210
    def response_code
211
      @status
212 213
    end

214
    # Returns a string to ensure compatibility with <tt>Net::HTTPResponse</tt>.
215
    def code
216
      @status.to_s
217 218
    end

219 220
    # Returns the corresponding message for the current HTTP status code:
    #
221 222 223 224 225
    #   response.status = 200
    #   response.message # => "OK"
    #
    #   response.status = 404
    #   response.message # => "Not Found"
226
    #
227
    def message
228
      Rack::Utils::HTTP_STATUS_CODES[@status]
229
    end
230
    alias_method :status_message, :message
231

232
    # Returns the content of the response as a string. This contains the contents
233
    # of any calls to <tt>render</tt>.
234
    def body
235
      @stream.body
236
    end
237

Y
Yehuda Katz 已提交
238 239
    EMPTY = " "

240
    # Allows you to manually set or override the response body.
241
    def body=(body)
Y
Yehuda Katz 已提交
242
      @blank = true if body == EMPTY
243

244 245 246
      if body.respond_to?(:to_path)
        @stream = body
      else
247 248 249
        synchronize do
          @stream = build_buffer self, munge_body_object(body)
        end
250
      end
251 252 253
    end

    def body_parts
254 255 256
      parts = []
      @stream.each { |x| parts << x }
      parts
D
Initial  
David Heinemeier Hansson 已提交
257 258
    end

259 260 261 262 263 264 265 266
    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

267
    # The location header we'll be responding with.
268
    def location
269
      headers[LOCATION]
270 271
    end
    alias_method :redirect_url, :location
272

273
    # Sets the location header we'll be responding with.
274
    def location=(url)
275
      headers[LOCATION] = url
276
    end
277

278
    def close
279
      stream.close if stream.respond_to?(:close)
280 281
    end

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

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

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

320
  private
J
Joshua Peek 已提交
321

322
    def before_committed
323 324 325
      return if committed?
      assign_default_content_type_and_charset!
      handle_conditional_get!
326 327 328
    end

    def before_sending
329 330
    end

331
    def merge_default_headers(original, default)
332
      default.respond_to?(:merge) ? default.merge(original) : original
333 334
    end

335 336 337 338 339 340 341 342
    def build_buffer(response, body)
      Buffer.new response, body
    end

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

343 344
    def assign_default_content_type_and_charset!
      return if self[CONTENT_TYPE].present?
J
Joshua Peek 已提交
345

346
      @content_type ||= Mime::HTML
J
Joshua Peek 已提交
347

348
      type = @content_type.to_s.dup
349
      type << "; charset=#{charset}" if append_charset?
J
Joshua Peek 已提交
350

351
      self[CONTENT_TYPE] = type
352
    end
353

354 355 356 357
    def append_charset?
      !@sending_file && @charset != false
    end

358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387
    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
388 389 390 391

      def to_ary
        nil
      end
392 393
    end

394 395 396
    def rack_response(status, header)
      header[SET_COOKIE] = header[SET_COOKIE].join("\n") if header[SET_COOKIE].respond_to?(:join)

397
      if NO_CONTENT_CODES.include?(@status)
398 399 400
        header.delete CONTENT_TYPE
        [status, header, []]
      else
401
        [status, header, RackBody.new(self)]
402 403
      end
    end
D
Initial  
David Heinemeier Hansson 已提交
404
  end
405
end