summaryrefslogtreecommitdiff
path: root/lib/rack/response.rb
blob: fb9d734b6f69a131c491c17dffa1701c31e940f8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# frozen_string_literal: true

require 'rack/request'
require 'rack/utils'
require 'rack/body_proxy'
require 'rack/media_type'
require 'time'

module Rack
  # Rack::Response provides a convenient interface to create a Rack
  # response.
  #
  # It allows setting of headers and cookies, and provides useful
  # defaults (an OK response with empty headers and body).
  #
  # You can use Response#write to iteratively generate your response,
  # but note that this is buffered by Rack::Response until you call
  # +finish+.  +finish+ however can take a block inside which calls to
  # +write+ are synchronous with the Rack response.
  #
  # Your application's +call+ should end returning Response#finish.
  class Response
    def self.[](status, headers, body)
      self.new(body, status, headers)
    end

    CHUNKED = 'chunked'
    STATUS_WITH_NO_ENTITY_BODY = Utils::STATUS_WITH_NO_ENTITY_BODY

    # Initialize the response object with the specified body, status
    # and headers.
    #
    # @param body [nil, #each, #to_str] the response body.
    # @param status [Integer] the integer status as defined by the
    # HTTP protocol RFCs.
    # @param headers [#each] a list of key-value header pairs which
    # conform to the HTTP protocol RFCs.
    #
    # Providing a body which responds to #to_str is legacy behaviour.
    def initialize(body = nil, status = 200, headers = {})
      @status = status.to_i
      @headers = Utils::HeaderHash.new(headers)

      @writer = self.method(:append)

      @block = nil
      @length = 0

      # Keep track of whether we have expanded the user supplied body.
      if body.nil?
        @body = []
        @buffered = true
      elsif body.respond_to?(:to_str)
        @body = [body]
        @buffered = true
      else
        @body = body
        @buffered = false
      end

      yield self if block_given?
    end

    attr_accessor :length, :status, :body
    attr_reader :headers

    # @deprecated Use {#headers} instead.
    alias header headers

    def redirect(target, status = 302)
      self.status = status
      self.location = target
    end

    def chunked?
      CHUNKED == get_header(TRANSFER_ENCODING)
    end

    # Generate a response array consistent with the requirements of the SPEC.
    # @return [Array] a 3-tuple suitable of `[status, headers, body]`
    # which is suitable to be returned from the middleware `#call(env)` method.
    def finish(&block)
      if STATUS_WITH_NO_ENTITY_BODY[status.to_i]
        delete_header CONTENT_TYPE
        delete_header CONTENT_LENGTH
        close
        [status.to_i, header, []]
      else
        if block_given?
          @block = block
          [status.to_i, header, self]
        else
          [status.to_i, header, @body]
        end
      end
    end

    alias to_a finish           # For *response

    def each(&callback)
      @body.each(&callback)
      @buffered = true

      if @block
        @writer = callback
        @block.call(self)
      end
    end

    # Append to body and update Content-Length.
    #
    # NOTE: Do not mix #write and direct #body access!
    #
    def write(chunk)
      buffered_body!

      @writer.call(chunk.to_s)
    end

    def close
      @body.close if @body.respond_to?(:close)
    end

    def empty?
      @block == nil && @body.empty?
    end

    def has_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

    alias :[] :get_header
    alias :[]= :set_header

    module Helpers
      def invalid?;             status < 100 || status >= 600;        end

      def informational?;       status >= 100 && status < 200;        end
      def successful?;          status >= 200 && status < 300;        end
      def redirection?;         status >= 300 && status < 400;        end
      def client_error?;        status >= 400 && status < 500;        end
      def server_error?;        status >= 500 && status < 600;        end

      def ok?;                  status == 200;                        end
      def created?;             status == 201;                        end
      def accepted?;            status == 202;                        end
      def no_content?;          status == 204;                        end
      def moved_permanently?;   status == 301;                        end
      def bad_request?;         status == 400;                        end
      def unauthorized?;        status == 401;                        end
      def forbidden?;           status == 403;                        end
      def not_found?;           status == 404;                        end
      def method_not_allowed?;  status == 405;                        end
      def precondition_failed?; status == 412;                        end
      def unprocessable?;       status == 422;                        end

      def redirect?;            [301, 302, 303, 307, 308].include? status; end

      def include?(header)
        has_header? header
      end

      # Add a header that may have multiple values.
      #
      # Example:
      #   response.add_header 'Vary', 'Accept-Encoding'
      #   response.add_header 'Vary', 'Cookie'
      #
      #   assert_equal 'Accept-Encoding,Cookie', response.get_header('Vary')
      #
      # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
      def add_header key, v
        if v.nil?
          get_header key
        elsif has_header? key
          set_header key, "#{get_header key},#{v}"
        else
          set_header key, v
        end
      end

      def content_type
        get_header CONTENT_TYPE
      end

      def media_type
        MediaType.type(content_type)
      end

      def media_type_params
        MediaType.params(content_type)
      end

      def content_length
        cl = get_header CONTENT_LENGTH
        cl ? cl.to_i : cl
      end

      def location
        get_header "Location"
      end

      def location=(location)
        set_header "Location", location
      end

      def set_cookie(key, value)
        cookie_header = get_header SET_COOKIE
        set_header SET_COOKIE, ::Rack::Utils.add_cookie_to_header(cookie_header, key, value)
      end

      def delete_cookie(key, value = {})
        set_header SET_COOKIE, ::Rack::Utils.add_remove_cookie_to_header(get_header(SET_COOKIE), key, value)
      end

      def set_cookie_header
        get_header SET_COOKIE
      end

      def set_cookie_header= v
        set_header SET_COOKIE, v
      end

      def cache_control
        get_header CACHE_CONTROL
      end

      def cache_control= v
        set_header CACHE_CONTROL, v
      end

      def etag
        get_header ETAG
      end

      def etag= v
        set_header ETAG, v
      end

    protected

      def buffered_body!
        return if @buffered

        if @body.is_a?(Array)
          # The user supplied body was an array:
          @body = @body.compact
        else
          # Turn the user supplied body into a buffered array:
          body = @body
          @body = Array.new

          body.each do |part|
            @writer.call(part.to_s)
          end

          body.close if body.respond_to?(:close)
        end

        @buffered = true
      end

      def append(chunk)
        @body << chunk

        unless chunked?
          @length += chunk.bytesize
          set_header(CONTENT_LENGTH, @length.to_s)
        end

        return chunk
      end
    end

    include Helpers

    class Raw
      include Helpers

      attr_reader :headers
      attr_accessor :status

      def initialize status, headers
        @status = status
        @headers = headers
      end

      def has_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
    end
  end
end