Class: Kronk::Response

Inherits:
Object
  • Object
show all
Defined in:
lib/kronk/response.rb

Overview

Standard Kronk response object.

Defined Under Namespace

Classes: InvalidParser, MissingParser

Constant Summary collapse

ENCODING_MATCHER =
/(^|;\s?)charset=(.*?)\s*(;|$)/
DEFAULT_ENCODING =
"ASCII-8BIT"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(input, opts = {}, &block) ⇒ Response

Create a new Response object from a String or IO. Options supported are:

:request

The Kronk::Request instance for this response.

:timeout

The read timeout value in seconds.

:no_body

Ignore reading the body of the response.

:force_gzip

Force decoding body with gzip.

:force_inflate

Force decoding body with inflate.

:allow_headless

Allow headless responses (won’t raise for invalid HTTP).



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
# File 'lib/kronk/response.rb', line 41

def initialize input, opts={}, &block
  @request = opts[:request]
  @headers = {}
  @encoding = @parser = @body = @gzip = @gzip_io = nil

  @headless = false

  @stringify_opts = {}

  @raw  = ""
  @time = 0

  input ||= ""
  input = StringIO.new(input) if String === input

  @io = BufferedIO === input ? input : BufferedIO.new(input)

  @io.raw_output   = @raw
  @io.response     = self
  @io.read_timeout = opts[:timeout] if opts[:timeout]

  allow_headless = opts.has_key?(:allow_headless) ?
                    opts[:allow_headless] :
                    headless_ok?(@io.io)

  response_from_io @io, allow_headless

  @cookies = []

  Array(@headers['set-cookie']).each do |cvalue|
    cookie = CookieJar::CookieValidation.parse_set_cookie(cvalue).to_hash
    cookie.delete :version
    cookie.keys.each{|k| cookie[k.to_s] = cookie.delete(k) }
    @cookies << cookie
  end

  Array(@headers['set-cookie2']).each do |cvalue|
    cookie = CookieJar::CookieValidation.parse_set_cookie2(cvalue).to_hash
    cookie.keys.each{|k| cookie[k.to_s] = cookie.delete(k) }
    @cookies << cookie
  end

  Kronk.cookie_jar.set_cookies_from_headers uri, @headers unless
    opts[:no_cookies] || !(URI::HTTP === uri)

  self.gzip    = opts[:force_gzip]
  self.inflate = opts[:force_inflate]
  gzip?
  deflated?

  @read = !!opts[:no_body]
  body(&block) if block_given?
end

Instance Attribute Details

#codeObject (readonly)

Returns the value of attribute code.



28
29
30
# File 'lib/kronk/response.rb', line 28

def code
  @code
end

#conn_timeObject

Returns the value of attribute conn_time.



29
30
31
# File 'lib/kronk/response.rb', line 29

def conn_time
  @conn_time
end

#cookiesObject (readonly)

Returns the value of attribute cookies.



28
29
30
# File 'lib/kronk/response.rb', line 28

def cookies
  @cookies
end

#headersObject (readonly) Also known as: to_hash

Returns the value of attribute headers.



28
29
30
# File 'lib/kronk/response.rb', line 28

def headers
  @headers
end

#ioObject (readonly)

Returns the value of attribute io.



28
29
30
# File 'lib/kronk/response.rb', line 28

def io
  @io
end

#readObject

Returns the value of attribute read.



29
30
31
# File 'lib/kronk/response.rb', line 29

def read
  @read
end

#requestObject

Returns the value of attribute request.



29
30
31
# File 'lib/kronk/response.rb', line 29

def request
  @request
end

#stringify_optsObject

Returns the value of attribute stringify_opts.



29
30
31
# File 'lib/kronk/response.rb', line 29

def stringify_opts
  @stringify_opts
end

#timeObject

Returns the value of attribute time.



29
30
31
# File 'lib/kronk/response.rb', line 29

def time
  @time
end

Class Method Details

.read_file(path, opts = {}, &block) ⇒ Object

Read http response from a file and return a Kronk::Response instance.



18
19
20
21
22
23
24
25
# File 'lib/kronk/response.rb', line 18

def self.read_file path, opts={}, &block
  file = File.open(path, "rb")
  resp = new(file, opts)
  resp.body(&block)
  file.close

  resp
end

Instance Method Details

#[](key) ⇒ Object

Accessor for the HTTP headers []



99
100
101
# File 'lib/kronk/response.rb', line 99

def [] key
  @headers[key.to_s.downcase]
end

#[]=(key, value) ⇒ Object

Setter for the HTTP headers []



107
108
109
# File 'lib/kronk/response.rb', line 107

def []= key, value
  @headers[key.to_s.downcase] = value
end

#body {|| ... } ⇒ Object

Returns the body of the response. Will wait for the socket to finish reading if the body hasn’t finished loading.

If a block is given and the body hasn’t been read yet, will iterate yielding a chunk of the body as it becomes available.

Note: Block will yield the full body if the response is compressed using Deflate as the Deflate format does not support streaming.

resp = Kronk::Response.new io
resp.body do |chunk|
  # handle stream
end

Yields:

  • ()

Raises:

  • (IOError)


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
# File 'lib/kronk/response.rb', line 127

def body &block
  return @body if @read

  raise IOError, 'Socket closed.' if @io.closed?

  require 'zlib' if gzip? || deflated?

  error    = false
  last_pos = 0

  begin
    read_body do |chunk|
      chunk = chunk.dup
      chunk = unzip chunk if gzip?

      try_force_encoding chunk
      (@body ||= "") << chunk
      yield chunk if block_given? && !deflated?
    end

  rescue IOError, EOFError => e
    error    = e
    last_pos = @body.to_s.size

    @io.read_all
    @body = headless? ? @raw : @raw.split("\r\n\r\n", 2)[1]
    @body = unzip @body, true if gzip?
  end

  @body = Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(@body) if deflated?

  try_force_encoding @raw unless gzip? || deflated?
  try_force_encoding @body

  @read = true

  yield @body[last_pos..-1] if block_given? && (deflated? || error)

  @body
end

#byterateObject

If time was set, returns bytes-per-second for the whole response, including headers.



173
174
175
176
# File 'lib/kronk/response.rb', line 173

def byterate
  return 0 unless raw && @time.to_f > 0
  @byterate = self.total_bytes / @time.to_f
end

#bytesObject

Current size of the http body in bytes.



182
183
184
# File 'lib/kronk/response.rb', line 182

def bytes
  self.raw_body.bytes.count
end

#chunked?Boolean

Is this a chunked streaming response?

Returns:

  • (Boolean)


190
191
192
193
194
# File 'lib/kronk/response.rb', line 190

def chunked?
  return false unless @headers['transfer-encoding']
  field = @headers['transfer-encoding']
  (/(?:\A|[^\-\w])chunked(?![\-\w])/i =~ field) ? true : false
end

#close?Boolean Also known as: connection_close?

Check if connection should be closed or not.

Returns:

  • (Boolean)


351
352
353
354
# File 'lib/kronk/response.rb', line 351

def close?
  @headers['connection'].to_s.include?('close') ||
  @headers['proxy-connection'].to_s.include?('close')
end

#content_lengthObject

Get the content length header.



200
201
202
203
204
205
# File 'lib/kronk/response.rb', line 200

def content_length
  return nil unless @headers.has_key?('content-length')
  len = @headers['content-length'].slice(/\d+/) or
      raise HTTPHeaderSyntaxError, 'wrong Content-Length format'
  len.to_i
end

#content_length=(len) ⇒ Object

Assign the expected content length.



211
212
213
214
215
216
217
# File 'lib/kronk/response.rb', line 211

def content_length= len
  unless len
    @headers.delete 'content-length'
    return nil
  end
  @headers['content-length'] = len.to_i.to_s
end

#content_rangeObject

Returns a Range object which represents the value of the Content-Range: header field. For a partial entity body, this indicates where this fragment fits inside the full entity body, as range of byte offsets.



226
227
228
229
230
231
# File 'lib/kronk/response.rb', line 226

def content_range
  return nil unless @headers['content-range']
  m = %r<bytes\s+(\d+)-(\d+)/(\d+|\*)>i.match(@headers['content-range']) or
      raise HTTPHeaderSyntaxError, 'wrong Content-Range format'
  m[1].to_i .. m[2].to_i
end

Cookie header accessor.



269
270
271
# File 'lib/kronk/response.rb', line 269

def cookie
  headers['cookie']
end

#data(opts = {}, &block) ⇒ Object

Returns the parsed response with selective headers and/or the body of the response. Supports the following options:

:no_body

Bool - Don’t return the body; default nil

:show_headers

Bool/String/Array - Return headers; default nil

:parser

Object - The parser to use for the body; default nil

:transform

Array - Action/path(s) pairs to modify data.

Deprecated Options:

:ignore_data

String/Array - Removes the data from given data paths

:only_data

String/Array - Extracts the data from given data paths

Example:

response.data :transform => [[:delete, ["foo/0", "bar/1"]]]
response.data do |trans|
  trans.delete "foo/0", "bar/1"
end

See Path::Transaction for supported transform actions in the ruby-path gem.



599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
# File 'lib/kronk/response.rb', line 599

def data opts={}, &block
  data = nil

  unless opts[:no_body]
    data = parsed_body opts[:parser]
  end

  if opts[:show_headers]
    header_data = parsed_header(opts[:show_headers])
    data &&= [header_data, data]
    data ||= header_data
  end

  Path::Transaction.run data, opts do |t|
    # Backward compatibility support
    t.select(*opts[:only_data])   if opts[:only_data]
    t.delete(*opts[:ignore_data]) if opts[:ignore_data]

    t.actions.concat opts[:transform] if opts[:transform]

    yield t if block_given?
  end
end

#deflated?Boolean

Check if content encoding is deflated.

Returns:

  • (Boolean)


277
278
279
280
281
# File 'lib/kronk/response.rb', line 277

def deflated?
  return !gzip? && @use_inflate unless @use_inflate.nil?
  @use_inflate = headers["content-encoding"] == "deflate" if
    headers["content-encoding"]
end

#encodingObject

Return the Ruby-1.9 encoding of the body, or String representation for Ruby-1.8.



296
297
298
299
300
301
302
303
# File 'lib/kronk/response.rb', line 296

def encoding
  return @encoding if @encoding
  c_type = headers["content-type"].to_s =~ ENCODING_MATCHER
  @encoding = $2 if c_type
  @encoding ||= DEFAULT_ENCODING
  @encoding = Encoding.find(@encoding) if defined?(Encoding)
  @encoding
end

#expected_bytesObject

Expected number of bytes to read from the server, including the header.



719
720
721
722
723
# File 'lib/kronk/response.rb', line 719

def expected_bytes
  return raw.bytes.count if @read
  return raw_header.bytes.count unless body_permitted?
  raw_header.to_s.bytes.count + (content_length || range_length).to_i + 2
end

#extObject

The extension or file type corresponding to the body, based on the Content-Type. Defaults to ‘txt’ if none can be determined or Content-Type is text/plain.

application/json    => 'json'
text/html           => 'html'
application/foo+xml => 'xml'


251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/kronk/response.rb', line 251

def ext
  file_ext =
    if headers['content-type']
      types = MIME::Types[headers['content-type'].sub(%r{/\w+\+}, '/')]
      types[0].extensions[0] unless types.empty?

    elsif uri
      File.extname(uri.path)[1..-1]
    end

  file_ext = "txt" if !file_ext || file_ext.strip.empty?
  file_ext
end

#follow_redirect(opts = {}, &block) ⇒ Object

Follow the redirect and return a new Response instance. Returns nil if not redirect-able. Supports all Request#new options, plus:

:trust_location

Forwards HTTP auth to different host when true.



534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
# File 'lib/kronk/response.rb', line 534

def follow_redirect opts={}, &block
  return if !redirect?
  new_opts = @request ? @request.to_hash : {}

  if @code == "303" || @code == "302"
    new_opts[:http_method] = "GET"
    new_opts.delete(:form)
    new_opts.delete(:data)
  end

  new_opts.delete(:headers)
  new_opts.delete(:host)
  new_opts.delete(:path)

  new_opts.delete(:auth) if !opts[:trust_location] &&
    (!@request || self.location.host != self.uri.host)

  new_opts.merge!(opts)

  Request.new(self.location, new_opts).stream(new_opts, &block)
end

#force_encoding(new_encoding) ⇒ Object

Force the encoding of the raw response and body.



309
310
311
312
313
314
315
# File 'lib/kronk/response.rb', line 309

def force_encoding new_encoding
  new_encoding = Encoding.find new_encoding unless Encoding === new_encoding
  @encoding = new_encoding
  try_force_encoding self.body
  try_force_encoding @raw
  @encoding
end

#gzip=(value) ⇒ Object

Require the use of gzip for reading the body.



737
738
739
# File 'lib/kronk/response.rb', line 737

def gzip= value
  @use_gzip = value
end

#gzip?Boolean

Check if gzip should be used.

Returns:

  • (Boolean)


745
746
747
748
749
# File 'lib/kronk/response.rb', line 745

def gzip?
  return @use_gzip unless @use_gzip.nil?
  @use_gzip = headers["content-encoding"] == "gzip" if
    headers["content-encoding"]
end

#headless?Boolean

If there was an error parsing the input as a standard http response, the input is assumed to be a body.

Returns:

  • (Boolean)


325
326
327
# File 'lib/kronk/response.rb', line 325

def headless?
  @headless
end

#http_versionObject

The version of the HTTP protocol returned.



333
334
335
# File 'lib/kronk/response.rb', line 333

def http_version
  @http_version
end

#inflate=(value) ⇒ Object

Force the use of inflate.



287
288
289
# File 'lib/kronk/response.rb', line 287

def inflate= value
  @use_inflate = value
end

#inspectObject

Ruby inspect.



341
342
343
344
345
# File 'lib/kronk/response.rb', line 341

def inspect
  content_type = headers['content-type'] || "text/plain"
  "#<#{self.class}:#{@code} #{content_type} \
#{total_bytes}/#{expected_bytes}bytes>"
end

#keep_alive?Boolean Also known as: connection_keep_alive?

Check if connection should stay alive.

Returns:

  • (Boolean)


362
363
364
365
# File 'lib/kronk/response.rb', line 362

def keep_alive?
  @headers['connection'].to_s.include?('Keep-Alive') ||
  @headers['proxy-connection'].to_s.include?('Keep-Alive')
end

#locationObject

Returns the location to redirect to. Prepends request url if location header is relative.



513
514
515
516
517
# File 'lib/kronk/response.rb', line 513

def location
  return unless @headers['location']
  return @headers['location'] if !@request || !@request.uri
  @request.uri.merge @headers['location']
end

#merge_stringify_opts(opts) ⇒ Object

:nodoc:



664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
# File 'lib/kronk/response.rb', line 664

def merge_stringify_opts opts # :nodoc:
  return @stringify_opts if opts.empty?

  opts = opts.dup
  @stringify_opts.each do |key, val|
    case key
    # Response headers - Boolean, String, or Array
    when :show_headers
      next if opts.has_key?(key) &&
              (opts[key].class != Array || val == true || val == false)

      opts[key] = (val == true || val == false) ? val :
                                  [*opts[key]] | [*val]

    # String or Array
    when :only_data, :ignore_data
      opts[key] = [*opts[key]] | [*val]

    else
      opts[key] = val if opts[key].nil?
    end
  end
  opts
end

#parsed_body(new_parser = nil) ⇒ Object

Returns the body data parsed according to the content type. If no parser is given will look for the default parser based on the Content-Type, or will return the cached parsed body if available.

Raises:



375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# File 'lib/kronk/response.rb', line 375

def parsed_body new_parser=nil
  return unless body
  @parsed_body ||= nil

  return @parsed_body if @parsed_body && !new_parser

  new_parser ||= parser

  begin
    new_parser = Kronk.parser_for(new_parser) ||
                 Kronk.find_const(new_parser)
  rescue NameError
    raise InvalidParser, "No such parser: #{new_parser}"
  end if String === new_parser

  raise MissingParser,
    "No parser for: #{@headers['content-type']}" unless new_parser

  begin
    @parsed_body = new_parser.parse(self.body) or raise RuntimeError

  rescue MissingDependency
    raise

  rescue => e
    msg = ParserError === e ?
            e.message : "#{new_parser} failed parsing body"

    msg << " returned by #{uri}" if uri
    raise ParserError, msg
  end
end

#parsed_header(include_headers = true) ⇒ Object

Returns the parsed header hash.



412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
# File 'lib/kronk/response.rb', line 412

def parsed_header include_headers=true
  out_headers = headers.dup
  out_headers['status']        = @code
  out_headers['http-version']  = http_version
  out_headers['set-cookie']  &&= @cookies.select{|c| c['version'].nil? }
  out_headers['set-cookie2'] &&= @cookies.select{|c| c['version'] == 1 }

  case include_headers
  when nil, false
    nil

  when Array, String
    include_headers = [*include_headers].map{|h| h.to_s.downcase}

    out_headers.keys.each do |key|
      out_headers.delete key unless
        include_headers.include? key.to_s.downcase
    end

    out_headers

  when true
    out_headers
  end
end

#parserObject

The parser to use on the body.



442
443
444
# File 'lib/kronk/response.rb', line 442

def parser
  @parser ||= Kronk.parser_for self.ext
end

#parser=(parser) ⇒ Object

Assign the parser.



450
451
452
453
454
# File 'lib/kronk/response.rb', line 450

def parser= parser
  @parser = Kronk.parser_for(parser) || Kronk.find_const(parser)
rescue NameError
  raise InvalidParser, "No such parser: #{parser}"
end

#range_lengthObject

The length of the range represented in Content-Range: header.



237
238
239
240
# File 'lib/kronk/response.rb', line 237

def range_length
  r = content_range() or return nil
  r.end - r.begin + 1
end

#rawObject

Returns the full raw HTTP response string after the full response has been read.



461
462
463
464
# File 'lib/kronk/response.rb', line 461

def raw
  body
  @raw
end

#raw_bodyObject

Returns the body portion of the raw http response.



470
471
472
# File 'lib/kronk/response.rb', line 470

def raw_body
  headless? ? @raw.to_s : @body.to_s
end

#raw_header(show = true) ⇒ Object

Returns the header portion of the raw http response.



478
479
480
481
482
483
484
485
486
487
488
489
490
# File 'lib/kronk/response.rb', line 478

def raw_header show=true
  return if !show || headless?
  headers = "#{@raw.split("\r\n\r\n", 2)[0]}\r\n"

  case show
  when Array, String
    includes = [*show].join("|")
    headers.scan(%r{^((?:#{includes}): [^\n]*\n)}im).flatten.join

  when true
    headers
  end
end

#read?Boolean

Check if the Response body has been read.

Returns:

  • (Boolean)


701
702
703
# File 'lib/kronk/response.rb', line 701

def read?
  @read
end

#read_timeoutObject

Maximum time to wait on IO.



496
497
498
# File 'lib/kronk/response.rb', line 496

def read_timeout
  @io.read_timeout
end

#read_timeout=(val) ⇒ Object

Assign maximum time to wait for IO data.



504
505
506
# File 'lib/kronk/response.rb', line 504

def read_timeout= val
  @io.read_timeout = val
end

#redirect?Boolean

Check if this is a redirect response.

Returns:

  • (Boolean)


523
524
525
# File 'lib/kronk/response.rb', line 523

def redirect?
  @code.to_s =~ /^30\d$/
end

#stringify(opts = {}, &block) ⇒ Object

Returns a String representation of the response, the response body, or the response headers, parsed or in raw format. Options supported are:

:parser

Object/String - the parser for the body; default nil (raw)

:struct

Boolean - Return data types instead of values

:only_data

String/Array - extracts the data from given data paths

:ignore_data

String/Array - defines which data points to exclude

:raw

Boolean - Force using the unparsed raw response

:keep_indicies

Boolean - indicies of modified arrays display as hashes.

:show_headers

Boolean/String/Array - defines which headers to include

If block is given, yields a Path::Transaction instance to make transformations on the data. See Kronk::Response#data



639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
# File 'lib/kronk/response.rb', line 639

def stringify opts={}, &block
  opts = merge_stringify_opts opts

  if !opts[:raw] && (opts[:parser] || parser || opts[:no_body])
    data = self.data opts, &block
    DataString.new data, opts

  else
    self.to_s :body    => !opts[:no_body],
              :headers => (opts[:show_headers] || false),
              :raw     => opts[:raw]
  end

rescue MissingParser
  if defined?(Cmd)
    ctype = @headers['content-type']
    Cmd.verbose "Warning: No parser for #{ctype} [#{uri}]"
  end

  self.to_s :body    => !opts[:no_body],
            :headers => (opts[:show_headers] || false),
            :raw     => opts[:raw]
end

#success?Boolean

Check if this is a 2XX response.

Returns:

  • (Boolean)


693
694
695
# File 'lib/kronk/response.rb', line 693

def success?
  @code.to_s =~ /^2\d\d$/
end

#to_s(opts = {}) ⇒ Object

Returns the raw response with selective headers and/or the body of the response. Supports the following options:

:body

Bool - Return the body; default true

:headers

Bool/String/Array - Return headers; default true



563
564
565
566
567
568
569
570
571
572
573
574
575
# File 'lib/kronk/response.rb', line 563

def to_s opts={}
  return raw if opts[:raw] &&
    (opts[:headers].nil? || opts[:headers] == true)

  str = opts[:raw] ? self.raw_body : self.body unless opts[:body] == false

  if opts[:headers] || opts[:headers].nil?
    hstr = raw_header(opts[:headers] || true)
    str  = [hstr, str].compact.join "\r\n"
  end

  str.to_s
end

#total_bytesObject

Number of bytes of the response including the header.



709
710
711
712
713
# File 'lib/kronk/response.rb', line 709

def total_bytes
  return raw.bytes.count if @read
  return raw_header.bytes.count unless body_permitted?
  raw_header.to_s.bytes.count + bytes + 2
end

#uriObject

The URI of the request if or the file read if available.



729
730
731
# File 'lib/kronk/response.rb', line 729

def uri
  @request && @request.uri || File === @io.io && URI.parse(@io.io.path)
end