File: abstract_response.rb

package info (click to toggle)
ruby-rest-client 2.1.0-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 860 kB
  • sloc: ruby: 3,844; makefile: 5
file content (252 lines) | stat: -rw-r--r-- 6,798 bytes parent folder | download | duplicates (3)
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
require 'cgi'
require 'http-cookie'

module RestClient

  module AbstractResponse

    attr_reader :net_http_res, :request, :start_time, :end_time, :duration

    def inspect
      raise NotImplementedError.new('must override in subclass')
    end

    # Logger from the request, potentially nil.
    def log
      request.log
    end

    def log_response
      return unless log

      code = net_http_res.code
      res_name = net_http_res.class.to_s.gsub(/\ANet::HTTP/, '')
      content_type = (net_http_res['Content-type'] || '').gsub(/;.*\z/, '')

      log << "# => #{code} #{res_name} | #{content_type} #{size} bytes, #{sprintf('%.2f', duration)}s\n"
    end

    # HTTP status code
    def code
      @code ||= @net_http_res.code.to_i
    end

    def history
      @history ||= request.redirection_history || []
    end

    # A hash of the headers, beautified with symbols and underscores.
    # e.g. "Content-type" will become :content_type.
    def headers
      @headers ||= AbstractResponse.beautify_headers(@net_http_res.to_hash)
    end

    # The raw headers.
    def raw_headers
      @raw_headers ||= @net_http_res.to_hash
    end

    # @param [Net::HTTPResponse] net_http_res
    # @param [RestClient::Request] request
    # @param [Time] start_time
    def response_set_vars(net_http_res, request, start_time)
      @net_http_res = net_http_res
      @request = request
      @start_time = start_time
      @end_time = Time.now

      if @start_time
        @duration = @end_time - @start_time
      else
        @duration = nil
      end

      # prime redirection history
      history
    end

    # Hash of cookies extracted from response headers.
    #
    # NB: This will return only cookies whose domain matches this request, and
    # may not even return all of those cookies if there are duplicate names.
    # Use the full cookie_jar for more nuanced access.
    #
    # @see #cookie_jar
    #
    # @return [Hash]
    #
    def cookies
      hash = {}

      cookie_jar.cookies(@request.uri).each do |cookie|
        hash[cookie.name] = cookie.value
      end

      hash
    end

    # Cookie jar extracted from response headers.
    #
    # @return [HTTP::CookieJar]
    #
    def cookie_jar
      return @cookie_jar if defined?(@cookie_jar) && @cookie_jar

      jar = @request.cookie_jar.dup
      headers.fetch(:set_cookie, []).each do |cookie|
        jar.parse(cookie, @request.uri)
      end

      @cookie_jar = jar
    end

    # Return the default behavior corresponding to the response code:
    #
    # For 20x status codes: return the response itself
    #
    # For 30x status codes:
    #   301, 302, 307: redirect GET / HEAD if there is a Location header
    #   303: redirect, changing method to GET, if there is a Location header
    #
    # For all other responses, raise a response exception
    #
    def return!(&block)
      case code
      when 200..207
        self
      when 301, 302, 307
        case request.method
        when 'get', 'head'
          check_max_redirects
          follow_redirection(&block)
        else
          raise exception_with_response
        end
      when 303
        check_max_redirects
        follow_get_redirection(&block)
      else
        raise exception_with_response
      end
    end

    def to_i
      warn('warning: calling Response#to_i is not recommended')
      super
    end

    def description
      "#{code} #{STATUSES[code]} | #{(headers[:content_type] || '').gsub(/;.*$/, '')} #{size} bytes\n"
    end

    # Follow a redirection response by making a new HTTP request to the
    # redirection target.
    def follow_redirection(&block)
      _follow_redirection(request.args.dup, &block)
    end

    # Follow a redirection response, but change the HTTP method to GET and drop
    # the payload from the original request.
    def follow_get_redirection(&block)
      new_args = request.args.dup
      new_args[:method] = :get
      new_args.delete(:payload)

      _follow_redirection(new_args, &block)
    end

    # Convert headers hash into canonical form.
    #
    # Header names will be converted to lowercase symbols with underscores
    # instead of hyphens.
    #
    # Headers specified multiple times will be joined by comma and space,
    # except for Set-Cookie, which will always be an array.
    #
    # Per RFC 2616, if a server sends multiple headers with the same key, they
    # MUST be able to be joined into a single header by a comma. However,
    # Set-Cookie (RFC 6265) cannot because commas are valid within cookie
    # definitions. The newer RFC 7230 notes (3.2.2) that Set-Cookie should be
    # handled as a special case.
    #
    # http://tools.ietf.org/html/rfc2616#section-4.2
    # http://tools.ietf.org/html/rfc7230#section-3.2.2
    # http://tools.ietf.org/html/rfc6265
    #
    # @param headers [Hash]
    # @return [Hash]
    #
    def self.beautify_headers(headers)
      headers.inject({}) do |out, (key, value)|
        key_sym = key.tr('-', '_').downcase.to_sym

        # Handle Set-Cookie specially since it cannot be joined by comma.
        if key.downcase == 'set-cookie'
          out[key_sym] = value
        else
          out[key_sym] = value.join(', ')
        end

        out
      end
    end

    private

    # Follow a redirection
    #
    # @param new_args [Hash] Start with this hash of arguments for the
    #   redirection request. The hash will be mutated, so be sure to dup any
    #   existing hash that should not be modified.
    #
    def _follow_redirection(new_args, &block)

      # parse location header and merge into existing URL
      url = headers[:location]

      # cannot follow redirection if there is no location header
      unless url
        raise exception_with_response
      end

      # handle relative redirects
      unless url.start_with?('http')
        url = URI.parse(request.url).merge(url).to_s
      end
      new_args[:url] = url

      new_args[:password] = request.password
      new_args[:user] = request.user
      new_args[:headers] = request.headers
      new_args[:max_redirects] = request.max_redirects - 1

      # pass through our new cookie jar
      new_args[:cookies] = cookie_jar

      # prepare new request
      new_req = Request.new(new_args)

      # append self to redirection history
      new_req.redirection_history = history + [self]

      # execute redirected request
      new_req.execute(&block)
    end

    def check_max_redirects
      if request.max_redirects <= 0
        raise exception_with_response
      end
    end

    def exception_with_response
      begin
        klass = Exceptions::EXCEPTIONS_MAP.fetch(code)
      rescue KeyError
        raise RequestFailed.new(self, code)
      end

      raise klass.new(self, code)
    end
  end
end