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
# 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 = []

  if URI::HTTP === uri
    jar = CookieJar::Jar.new
    jar.set_cookies_from_headers uri, @headers

    jar.to_a.each do |cookie|
      @cookies << cookie.to_hash
      Kronk.cookie_jar.add_cookie cookie unless opts[:no_cookies]
    end
  end

  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 []



93
94
95
# File 'lib/kronk/response.rb', line 93

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

#[]=(key, value) ⇒ Object

Setter for the HTTP headers []



101
102
103
# File 'lib/kronk/response.rb', line 101

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)


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

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.



167
168
169
170
# File 'lib/kronk/response.rb', line 167

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.



176
177
178
# File 'lib/kronk/response.rb', line 176

def bytes
  self.raw_body.bytes.count
end

#chunked?Boolean

Is this a chunked streaming response?

Returns:

  • (Boolean)


184
185
186
187
188
# File 'lib/kronk/response.rb', line 184

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)


345
346
347
348
# File 'lib/kronk/response.rb', line 345

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

#content_lengthObject

Get the content length header.



194
195
196
197
198
199
# File 'lib/kronk/response.rb', line 194

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.



205
206
207
208
209
210
211
# File 'lib/kronk/response.rb', line 205

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.



220
221
222
223
224
225
# File 'lib/kronk/response.rb', line 220

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.



263
264
265
# File 'lib/kronk/response.rb', line 263

def cookie
  headers['cookie']
end

#data(opts = {}) ⇒ 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.



590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
# File 'lib/kronk/response.rb', line 590

def data opts={}
  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)


271
272
273
274
275
# File 'lib/kronk/response.rb', line 271

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.



290
291
292
293
294
295
296
297
# File 'lib/kronk/response.rb', line 290

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.



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

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'


245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/kronk/response.rb', line 245

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.



525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
# File 'lib/kronk/response.rb', line 525

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).retrieve(new_opts, &block)
end

#force_encoding(new_encoding) ⇒ Object

Force the encoding of the raw response and body.



303
304
305
306
307
308
309
# File 'lib/kronk/response.rb', line 303

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.



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

def gzip= value
  @use_gzip = value
end

#gzip?Boolean

Check if gzip should be used.

Returns:

  • (Boolean)


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

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)


319
320
321
# File 'lib/kronk/response.rb', line 319

def headless?
  @headless
end

#http_versionObject

The version of the HTTP protocol returned.



327
328
329
# File 'lib/kronk/response.rb', line 327

def http_version
  @http_version
end

#inflate=(value) ⇒ Object

Force the use of inflate.



281
282
283
# File 'lib/kronk/response.rb', line 281

def inflate= value
  @use_inflate = value
end

#inspectObject

Ruby inspect.



335
336
337
338
339
# File 'lib/kronk/response.rb', line 335

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)


356
357
358
359
# File 'lib/kronk/response.rb', line 356

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.



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

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:



655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
# File 'lib/kronk/response.rb', line 655

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:



369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/kronk/response.rb', line 369

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 => 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.



403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
# File 'lib/kronk/response.rb', line 403

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.



433
434
435
# File 'lib/kronk/response.rb', line 433

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

#parser=(parser) ⇒ Object

Assign the parser.



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

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.



231
232
233
234
# File 'lib/kronk/response.rb', line 231

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.



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

def raw
  body
  @raw
end

#raw_bodyObject

Returns the body portion of the raw http response.



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

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.



469
470
471
472
473
474
475
476
477
478
479
480
481
# File 'lib/kronk/response.rb', line 469

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)


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

def read?
  @read
end

#read_timeoutObject

Maximum time to wait on IO.



487
488
489
# File 'lib/kronk/response.rb', line 487

def read_timeout
  @io.read_timeout
end

#read_timeout=(val) ⇒ Object

Assign maximum time to wait for IO data.



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

def read_timeout= val
  @io.read_timeout = val
end

#redirect?Boolean

Check if this is a redirect response.

Returns:

  • (Boolean)


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

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



630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
# File 'lib/kronk/response.rb', line 630

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)


684
685
686
# File 'lib/kronk/response.rb', line 684

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



554
555
556
557
558
559
560
561
562
563
564
565
566
# File 'lib/kronk/response.rb', line 554

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.



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

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.



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

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