Class: EventMachine::HttpClient

Inherits:
Object
  • Object
show all
Includes:
Deferrable, HttpEncoding, HttpStatus
Defined in:
lib/em-http/client.rb

Defined Under Namespace

Classes: CookieJar

Constant Summary collapse

TRANSFER_ENCODING =
"TRANSFER_ENCODING"
CONTENT_ENCODING =
"CONTENT_ENCODING"
CONTENT_LENGTH =
"CONTENT_LENGTH"
CONTENT_TYPE =
"CONTENT_TYPE"
LAST_MODIFIED =
"LAST_MODIFIED"
KEEP_ALIVE =
"CONNECTION"
"SET_COOKIE"
LOCATION =
"LOCATION"
HOST =
"HOST"
ETAG =
"ETAG"
CRLF =
"\r\n"

Constants included from HttpStatus

EventMachine::HttpStatus::CODE

Constants included from HttpEncoding

EventMachine::HttpEncoding::FIELD_ENCODING, EventMachine::HttpEncoding::HTTP_REQUEST_HEADER

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from HttpEncoding

#bytesize, #encode_auth, #encode_cookie, #encode_field, #encode_headers, #encode_host, #encode_param, #encode_query, #encode_request, #escape, #form_encode_body, #munge_header_keys, #unescape

Constructor Details

#initialize(conn, options) ⇒ HttpClient

Returns a new instance of HttpClient.



27
28
29
30
31
32
33
34
35
36
37
# File 'lib/em-http/client.rb', line 27

def initialize(conn, options)
  @conn = conn
  @req  = options

  @stream    = nil
  @headers   = nil
  @cookies   = []
  @cookiejar = CookieJar.new

  reset!
end

Instance Attribute Details

#connObject

Returns the value of attribute conn.



24
25
26
# File 'lib/em-http/client.rb', line 24

def conn
  @conn
end

#content_charsetObject (readonly)

Returns the value of attribute content_charset.



25
26
27
# File 'lib/em-http/client.rb', line 25

def content_charset
  @content_charset
end

#cookiesObject (readonly)

Returns the value of attribute cookies.



25
26
27
# File 'lib/em-http/client.rb', line 25

def cookies
  @cookies
end

#errorObject (readonly)

Returns the value of attribute error.



25
26
27
# File 'lib/em-http/client.rb', line 25

def error
  @error
end

#reqObject (readonly)

Returns the value of attribute req.



25
26
27
# File 'lib/em-http/client.rb', line 25

def req
  @req
end

#responseObject

Returns the value of attribute response.



24
25
26
# File 'lib/em-http/client.rb', line 24

def response
  @response
end

#response_headerObject (readonly)

Returns the value of attribute response_header.



25
26
27
# File 'lib/em-http/client.rb', line 25

def response_header
  @response_header
end

#stateObject

Returns the value of attribute state.



24
25
26
# File 'lib/em-http/client.rb', line 24

def state
  @state
end

Instance Method Details

#build_requestObject



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
# File 'lib/em-http/client.rb', line 134

def build_request
  head    = @req.headers ? munge_header_keys(@req.headers) : {}

  if @conn.connopts.http_proxy?
    proxy = @conn.connopts.proxy
    head['proxy-authorization'] = proxy[:authorization] if proxy[:authorization]
  end

  # Set the cookie header if provided
  if cookie = head['cookie']
    @cookies << encode_cookie(cookie) if cookie
  end
  head['cookie'] = @cookies.compact.uniq.join("; ").squeeze(";") unless @cookies.empty?

  # Set connection close unless keepalive
  if !@req.keepalive
    head['connection'] = 'close'
  end

  # Set the Host header if it hasn't been specified already
  head['host'] ||= encode_host

  # Set the User-Agent if it hasn't been specified
  if !head.key?('user-agent')
    head['user-agent'] = 'EventMachine HttpClient'
  elsif head['user-agent'].nil?
    head.delete('user-agent')
  end

  # Set the Accept-Encoding header if none is provided
  if !head.key?('accept-encoding') && req.compressed
    head['accept-encoding'] = 'gzip, compressed'
  end

  # Set the auth from the URI if given
  head['Authorization'] = @req.uri.userinfo.split(/:/, 2) if @req.uri.userinfo

  head
end

#connection_completedObject



53
54
55
56
57
58
59
60
61
62
# File 'lib/em-http/client.rb', line 53

def connection_completed
  @state = :response_header

  head, body = build_request, @req.body
  @conn.middleware.each do |m|
    head, body = m.request(self, head, body) if m.respond_to?(:request)
  end

  send_request(head, body)
end

#continue?Boolean

Returns:

  • (Boolean)


74
75
76
# File 'lib/em-http/client.rb', line 74

def continue?
  @response_header.status == 100 && (@req.method == 'POST' || @req.method == 'PUT')
end

#finished?Boolean

Returns:

  • (Boolean)


78
79
80
# File 'lib/em-http/client.rb', line 78

def finished?
  @state == :finished || (@state == :body && @response_header.content_length.nil?)
end

#headers(&blk) ⇒ Object



128
# File 'lib/em-http/client.rb', line 128

def headers(&blk); @headers = blk; end

#last_effective_urlObject



49
# File 'lib/em-http/client.rb', line 49

def last_effective_url; @req.uri; end

#normalize_body(body) ⇒ Object



130
131
132
# File 'lib/em-http/client.rb', line 130

def normalize_body(body)
  body.is_a?(Hash) ? form_encode_body(body) : body
end

#on_body_data(data) ⇒ Object



205
206
207
208
209
210
211
212
213
214
215
# File 'lib/em-http/client.rb', line 205

def on_body_data(data)
  if @content_decoder
    begin
      @content_decoder << data
    rescue HttpDecoders::DecoderError
      on_error "Content-decoder error"
    end
  else
    on_decoded_body_data(data)
  end
end

#on_decoded_body_data(data) ⇒ Object



217
218
219
220
221
222
223
224
# File 'lib/em-http/client.rb', line 217

def on_decoded_body_data(data)
  data.force_encoding @content_charset if @content_charset
  if @stream
    @stream.call(data)
  else
    @response << data
  end
end

#on_error(msg = nil) ⇒ Object Also known as: close



121
122
123
124
# File 'lib/em-http/client.rb', line 121

def on_error(msg = nil)
  @error = msg
  fail(self)
end

#on_request_completeObject



64
65
66
67
68
69
70
71
72
# File 'lib/em-http/client.rb', line 64

def on_request_complete
  begin
    @content_decoder.finalize! if @content_decoder
  rescue HttpDecoders::DecoderError
    on_error "Content-decoder error"
  end

  unbind
end

#parse_response_header(header, version, status) ⇒ Object



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
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/em-http/client.rb', line 248

def parse_response_header(header, version, status)
  @response_header.raw = header
  header.each do |key, val|
    @response_header[key.upcase.gsub('-','_')] = val
  end

  @response_header.http_version = version.join('.')
  @response_header.http_status  = status
  @response_header.http_reason  = CODE[status] || 'unknown'

  # invoke headers callback after full parse
  # if one is specified by the user
  @headers.call(@response_header) if @headers

  unless @response_header.http_status and @response_header.http_reason
    @state = :invalid
    on_error "no HTTP response"
    return
  end

  # add set-cookie's to cookie list
  if @response_header.cookie && @req.pass_cookies
    [@response_header.cookie].flatten.each {|cookie| @cookiejar.set(cookie, @req.uri)}
  end

  # correct location header - some servers will incorrectly give a relative URI
  if @response_header.location
    begin
      location = Addressable::URI.parse(@response_header.location)
      location.path = "/" if location.path.empty?

      if location.relative?
        location = @req.uri.join(location)
      else
        # if redirect is to an absolute url, check for correct URI structure
        raise if location.host.nil?
      end

      @response_header[LOCATION] = location.to_s

    rescue
      on_error "Location header format error"
      return
    end
  end

  # Fire callbacks immediately after recieving header requests
  # if the request method is HEAD. In case of a redirect, terminate
  # current connection and reinitialize the process.
  if @req.method == "HEAD"
    @state = :finished
    return
  end

  if @response_header.chunked_encoding?
    @state = :chunk_header
  elsif @response_header.content_length
    @state = :body
  else
    @state = :body
  end

  if @req.decoding && decoder_class = HttpDecoders.decoder_for_encoding(response_header[CONTENT_ENCODING])
    begin
      @content_decoder = decoder_class.new do |s| on_decoded_body_data(s) end
    rescue HttpDecoders::DecoderError
      on_error "Content-decoder error"
    end
  end

  # handle malformed header - Content-Type repetitions.
  content_type = [response_header[CONTENT_TYPE]].flatten.first

  if String.method_defined?(:force_encoding) && /;\s*charset=\s*(.+?)\s*(;|$)/.match(content_type)
    @content_charset = Encoding.find($1.gsub(/^\"|\"$/, '')) rescue Encoding.default_external
  end
end

#peerObject



51
# File 'lib/em-http/client.rb', line 51

def peer; @conn.peer; end

#redirect?Boolean

Returns:

  • (Boolean)


82
83
84
# File 'lib/em-http/client.rb', line 82

def redirect?
  @response_header.redirection? && @req.follow_redirect?
end

#redirectsObject



50
# File 'lib/em-http/client.rb', line 50

def redirects; @req.followed; end

#request_body_pending?Boolean

Returns:

  • (Boolean)


226
227
228
# File 'lib/em-http/client.rb', line 226

def request_body_pending?
  !!@req_body
end

#reset!Object



39
40
41
42
43
44
45
46
47
# File 'lib/em-http/client.rb', line 39

def reset!
  @response_header = HttpResponseHeader.new
  @state = :response_header

  @response = ''
  @error = nil
  @content_decoder = nil
  @content_charset = nil
end

#send_request(head, body) ⇒ Object



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
# File 'lib/em-http/client.rb', line 174

def send_request(head, body)
  body    = normalize_body(body)
  file    = @req.file
  query   = @req.query

  # Set the Content-Length if file is given
  head['content-length'] = File.size(file) if file

  # Set the Content-Length if body is given,
  # or we're doing an empty post or put
  if body
    head['content-length'] ||= body.respond_to?(:bytesize) ? body.bytesize : body.size
  elsif @req.method == 'POST' or @req.method == 'PUT'
    # wont happen if body is set and we already set content-length above
    head['content-length'] ||= 0
  end

  # Set content-type header if missing and body is a Ruby hash
  if !head['content-type'] and @req.body.is_a? Hash
    head['content-type'] = 'application/x-www-form-urlencoded'
  end

  request_header ||= encode_request(@req.method, @req.uri, query, @conn.connopts)
  request_header << encode_headers(head)
  request_header << CRLF
  @conn.send_data request_header

  @req_body = body || (@req.file && Pathname.new(@req.file))
  send_request_body unless @req.headers['expect'] == '100-continue'
end

#send_request_bodyObject



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/em-http/client.rb', line 230

def send_request_body
  return  if @req_body.nil?

  if @req_body.is_a?(String)
    @conn.send_data @req_body

  elsif @req_body.is_a?(Pathname)
    @conn.stream_file_data @req_body.to_path, http_chunks: false

  elsif @req_body.respond_to?(:read) && @req_body.respond_to?(:eof?)   # IO or IO-like object
    @conn.stream_data @req_body

  else
    raise "Don't know how to send request body: #{@req_body.inspect}"
  end
  @req_body = nil
end

#stream(&blk) ⇒ Object



127
# File 'lib/em-http/client.rb', line 127

def stream(&blk); @stream = blk; end

#unbind(reason = nil) ⇒ Object



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
# File 'lib/em-http/client.rb', line 86

def unbind(reason = nil)
  if finished?
    if redirect?

      begin
        @conn.middleware.each do |m|
          m.response(self) if m.respond_to?(:response)
        end

        # one of the injected middlewares could have changed
        # our redirect settings, check if we still want to
        # follow the location header
        if redirect?
          @req.followed += 1

          @cookies.clear
          @cookies = @cookiejar.get(@response_header.location).map(&:to_s) if @req.pass_cookies

          @conn.redirect(self, @response_header.location)
        else
          succeed(self)
        end

      rescue => e
        on_error(e.message)
      end
    else
      succeed(self)
    end

  else
    on_error(reason || 'connection closed by server')
  end
end