response.rb 8.2 KB
Newer Older
W
wycats 已提交
1
require 'active_support/core_ext/class/attribute_accessors'
2
require 'monitor'
3

4
module ActionDispatch # :nodoc:
5 6 7 8 9
  # 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 已提交
10
  #
11
  # \Response is mostly a Ruby on \Rails framework implementation detail, and
12 13 14 15
  # 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 已提交
16
  #
17
  # Nevertheless, integration tests may want to inspect controller responses in
18
  # more detail, and that's when \Response can be useful for application
19
  # developers. Integration test methods such as
J
Joshua Peek 已提交
20 21
  # ActionDispatch::Integration::Session#get and
  # ActionDispatch::Integration::Session#post return objects of type
22
  # TestResponse (which are of course also of type \Response).
P
Pratik Naik 已提交
23
  #
24
  # For example, the following demo integration test prints the body of the
P
Pratik Naik 已提交
25 26
  # controller response to the console:
  #
J
Joshua Peek 已提交
27
  #  class DemoControllerTest < ActionDispatch::IntegrationTest
P
Pratik Naik 已提交
28 29
  #    def test_print_root_path_to_console
  #      get('/')
A
Alexey Vakhov 已提交
30
  #      puts response.body
P
Pratik Naik 已提交
31 32
  #    end
  #  end
33
  class Response
34 35 36 37
    # The request that the response is responding to.
    attr_accessor :request

    # The HTTP status code.
38
    attr_reader :status
39

40
    attr_writer :sending_file
P
Pratik Naik 已提交
41

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

45
    alias_method :headers=, :header=
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 63 64
    # 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

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 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
    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)
        @buf.each(&block)
      end

      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 = [])
110 111
      super()

112
      header = merge_default_headers(header, self.class.default_headers)
E
Egor Homakov 已提交
113

114
      self.body, self.header, self.status = body, header, status
115

116
      @sending_file = false
117 118 119
      @blank        = false
      @cv           = new_cond
      @committed    = false
120 121
      @content_type = nil
      @charset      = nil
J
Joshua Peek 已提交
122

123
      if content_type = self[CONTENT_TYPE]
124 125
        type, charset = content_type.split(/;\s*charset=/)
        @content_type = Mime::Type.lookup(type)
126
        @charset = charset || self.class.default_charset
127
      end
128

129 130 131 132
      prepare_cache_control!

      yield self if block_given?
    end
133

134 135 136 137 138 139 140 141 142 143 144 145 146 147
    def await_commit
      synchronize do
        @cv.wait_until { @committed }
      end
    end

    def commit!
      synchronize do
        @committed = true
        @cv.broadcast
      end
    end

    def committed?
148
      @committed
149 150
    end

151
    # Sets the HTTP status code.
152
    def status=(status)
153
      @status = Rack::Utils.status_code(status)
154 155
    end

156
    # Sets the HTTP content type.
S
Santiago Pastorino 已提交
157 158 159 160
    def content_type=(content_type)
      @content_type = content_type.to_s
    end

161
    # The response code of the request.
162
    def response_code
163
      @status
164 165
    end

166
    # Returns a string to ensure compatibility with <tt>Net::HTTPResponse</tt>.
167
    def code
168
      @status.to_s
169 170
    end

171 172
    # Returns the corresponding message for the current HTTP status code:
    #
173 174 175 176 177
    #   response.status = 200
    #   response.message # => "OK"
    #
    #   response.status = 404
    #   response.message # => "Not Found"
178
    #
179
    def message
180
      Rack::Utils::HTTP_STATUS_CODES[@status]
181
    end
182
    alias_method :status_message, :message
183

184
    def respond_to?(method, include_private = false)
S
Santiago Pastorino 已提交
185
      if method.to_s == 'to_path'
186
        stream.respond_to?(method)
187 188 189 190 191 192
      else
        super
      end
    end

    def to_path
193
      stream.to_path
194 195
    end

196
    # Returns the content of the response as a string. This contains the contents
197
    # of any calls to <tt>render</tt>.
198
    def body
199 200 201
      strings = []
      each { |part| strings << part.to_s }
      strings.join
202
    end
203

Y
Yehuda Katz 已提交
204 205
    EMPTY = " "

206
    # Allows you to manually set or override the response body.
207
    def body=(body)
Y
Yehuda Katz 已提交
208
      @blank = true if body == EMPTY
209

210 211 212
      if body.respond_to?(:to_path)
        @stream = body
      else
213 214 215
        synchronize do
          @stream = build_buffer self, munge_body_object(body)
        end
216
      end
217 218 219
    end

    def body_parts
220 221 222
      parts = []
      @stream.each { |x| parts << x }
      parts
D
Initial  
David Heinemeier Hansson 已提交
223 224
    end

225 226 227 228 229 230 231 232
    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

233
    # The location header we'll be responding with.
234
    def location
235
      headers[LOCATION]
236 237
    end
    alias_method :redirect_url, :location
238

239
    # Sets the location header we'll be responding with.
240
    def location=(url)
241
      headers[LOCATION] = url
242
    end
243

244
    def close
245
      stream.close if stream.respond_to?(:close)
246 247
    end

248 249
    # Turns the Response into a Rack-compatible array of the status, headers,
    # and body.
250
    def to_a
251
      rack_response @status, @header.to_hash
252
    end
253
    alias prepare! to_a
254
    alias to_ary   to_a
255

256 257 258 259 260
    # Returns the response cookies, converted to a Hash of (name => value) pairs
    #
    #   assert_equal 'AuthorOfNewPage', r.cookies['author']
    def cookies
      cookies = {}
261
      if header = self[SET_COOKIE]
262 263 264 265 266 267 268
        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
269 270 271 272
      end
      cookies
    end

273
  private
J
Joshua Peek 已提交
274

275 276 277 278 279 280
    def merge_default_headers(original, default)
      return original unless default.respond_to?(:merge)

      default.merge(original)
    end

281 282 283 284 285 286 287 288
    def build_buffer(response, body)
      Buffer.new response, body
    end

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

289
    def assign_default_content_type_and_charset!(headers)
290
      return if headers[CONTENT_TYPE].present?
J
Joshua Peek 已提交
291

292
      @content_type ||= Mime::HTML
293
      @charset      ||= self.class.default_charset unless @charset == false
J
Joshua Peek 已提交
294

295
      type = @content_type.to_s.dup
296
      type << "; charset=#{@charset}" if append_charset?
J
Joshua Peek 已提交
297

298 299
      headers[CONTENT_TYPE] = type
    end
300

301 302 303 304
    def append_charset?
      !@sending_file && @charset != false
    end

305 306 307 308 309 310
    def rack_response(status, header)
      assign_default_content_type_and_charset!(header)
      handle_conditional_get!

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

311
      if NO_CONTENT_CODES.include?(@status)
312 313 314 315 316 317
        header.delete CONTENT_TYPE
        [status, header, []]
      else
        [status, header, self]
      end
    end
D
Initial  
David Heinemeier Hansson 已提交
318
  end
319
end