http 0.7.4 → 0.8.0.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +0 -1
  3. data/.rubocop.yml +5 -2
  4. data/CHANGES.md +24 -7
  5. data/CONTRIBUTING.md +25 -0
  6. data/Gemfile +24 -22
  7. data/Guardfile +2 -2
  8. data/README.md +34 -4
  9. data/Rakefile +7 -7
  10. data/examples/parallel_requests_with_celluloid.rb +2 -2
  11. data/http.gemspec +12 -12
  12. data/lib/http.rb +11 -10
  13. data/lib/http/cache.rb +146 -0
  14. data/lib/http/cache/headers.rb +100 -0
  15. data/lib/http/cache/null_cache.rb +13 -0
  16. data/lib/http/chainable.rb +14 -3
  17. data/lib/http/client.rb +64 -80
  18. data/lib/http/connection.rb +139 -0
  19. data/lib/http/content_type.rb +2 -2
  20. data/lib/http/errors.rb +7 -1
  21. data/lib/http/headers.rb +21 -8
  22. data/lib/http/headers/mixin.rb +1 -1
  23. data/lib/http/mime_type.rb +2 -2
  24. data/lib/http/mime_type/adapter.rb +2 -2
  25. data/lib/http/mime_type/json.rb +4 -4
  26. data/lib/http/options.rb +65 -74
  27. data/lib/http/redirector.rb +3 -3
  28. data/lib/http/request.rb +20 -13
  29. data/lib/http/request/caching.rb +95 -0
  30. data/lib/http/request/writer.rb +5 -5
  31. data/lib/http/response.rb +15 -9
  32. data/lib/http/response/body.rb +21 -8
  33. data/lib/http/response/caching.rb +142 -0
  34. data/lib/http/response/io_body.rb +63 -0
  35. data/lib/http/response/parser.rb +1 -1
  36. data/lib/http/response/status.rb +4 -12
  37. data/lib/http/response/status/reasons.rb +53 -53
  38. data/lib/http/response/string_body.rb +53 -0
  39. data/lib/http/version.rb +1 -1
  40. data/spec/lib/http/cache/headers_spec.rb +77 -0
  41. data/spec/lib/http/cache_spec.rb +182 -0
  42. data/spec/lib/http/client_spec.rb +123 -95
  43. data/spec/lib/http/content_type_spec.rb +25 -25
  44. data/spec/lib/http/headers/mixin_spec.rb +8 -8
  45. data/spec/lib/http/headers_spec.rb +213 -173
  46. data/spec/lib/http/options/body_spec.rb +5 -5
  47. data/spec/lib/http/options/form_spec.rb +3 -3
  48. data/spec/lib/http/options/headers_spec.rb +7 -7
  49. data/spec/lib/http/options/json_spec.rb +3 -3
  50. data/spec/lib/http/options/merge_spec.rb +26 -22
  51. data/spec/lib/http/options/new_spec.rb +10 -10
  52. data/spec/lib/http/options/proxy_spec.rb +8 -8
  53. data/spec/lib/http/options_spec.rb +2 -2
  54. data/spec/lib/http/redirector_spec.rb +32 -32
  55. data/spec/lib/http/request/caching_spec.rb +133 -0
  56. data/spec/lib/http/request/writer_spec.rb +26 -26
  57. data/spec/lib/http/request_spec.rb +63 -58
  58. data/spec/lib/http/response/body_spec.rb +13 -13
  59. data/spec/lib/http/response/caching_spec.rb +201 -0
  60. data/spec/lib/http/response/io_body_spec.rb +35 -0
  61. data/spec/lib/http/response/status_spec.rb +25 -25
  62. data/spec/lib/http/response/string_body_spec.rb +35 -0
  63. data/spec/lib/http/response_spec.rb +64 -45
  64. data/spec/lib/http_spec.rb +103 -76
  65. data/spec/spec_helper.rb +10 -12
  66. data/spec/support/connection_reuse_shared.rb +100 -0
  67. data/spec/support/create_certs.rb +12 -12
  68. data/spec/support/dummy_server.rb +11 -11
  69. data/spec/support/dummy_server/servlet.rb +43 -31
  70. data/spec/support/proxy_server.rb +31 -25
  71. metadata +57 -8
  72. data/spec/support/example_server.rb +0 -30
  73. data/spec/support/example_server/servlet.rb +0 -102
@@ -32,10 +32,10 @@ module HTTP
32
32
  # Adds the headers to the header array for the given request body we are working
33
33
  # with
34
34
  def add_body_type_headers
35
- if @body.is_a?(String) && !@headers['Content-Length']
35
+ if @body.is_a?(String) && !@headers["Content-Length"]
36
36
  @request_header << "Content-Length: #{@body.bytesize}"
37
- elsif @body.is_a?(Enumerable) && 'chunked' != @headers['Transfer-Encoding']
38
- fail(RequestError, 'invalid transfer encoding')
37
+ elsif @body.is_a?(Enumerable) && "chunked" != @headers["Transfer-Encoding"]
38
+ fail(RequestError, "invalid transfer encoding")
39
39
  end
40
40
  end
41
41
 
@@ -64,11 +64,11 @@ module HTTP
64
64
  @socket << chunk << CRLF
65
65
  end
66
66
 
67
- @socket << '0' << CRLF * 2
67
+ @socket << "0" << CRLF * 2
68
68
  end
69
69
  end
70
70
 
71
- private
71
+ private
72
72
 
73
73
  def validate_body_type!
74
74
  return if VALID_BODY_TYPES.any? { |type| @body.is_a? type }
data/lib/http/response.rb CHANGED
@@ -1,9 +1,11 @@
1
- require 'forwardable'
1
+ require "forwardable"
2
2
 
3
- require 'http/headers'
4
- require 'http/content_type'
5
- require 'http/mime_type'
6
- require 'http/response/status'
3
+ require "http/headers"
4
+ require "http/content_type"
5
+ require "http/mime_type"
6
+ require "http/response/caching"
7
+ require "http/response/status"
8
+ require "time"
7
9
 
8
10
  module HTTP
9
11
  class Response
@@ -16,7 +18,7 @@ module HTTP
16
18
  STATUS_CODES = Status::REASONS
17
19
 
18
20
  # @deprecated Will be removed in 1.0.0
19
- SYMBOL_TO_STATUS_CODE = Hash[STATUS_CODES.map { |k, v| [v.downcase.gsub(/\s|-/, '_').to_sym, k] }].freeze
21
+ SYMBOL_TO_STATUS_CODE = Hash[STATUS_CODES.map { |k, v| [v.downcase.gsub(/\s|-/, "_").to_sym, k] }].freeze
20
22
 
21
23
  # @return [Status]
22
24
  attr_reader :status
@@ -29,7 +31,6 @@ module HTTP
29
31
 
30
32
  def initialize(status, version, headers, body, uri = nil) # rubocop:disable ParameterLists
31
33
  @version, @body, @uri = version, body, uri
32
-
33
34
  @status = HTTP::Response::Status.new status
34
35
  @headers = HTTP::Headers.coerce(headers || {})
35
36
  end
@@ -73,7 +74,7 @@ module HTTP
73
74
  #
74
75
  # @return [HTTP::ContentType]
75
76
  def content_type
76
- @content_type ||= ContentType.parse headers['Content-Type']
77
+ @content_type ||= ContentType.parse headers["Content-Type"]
77
78
  end
78
79
 
79
80
  # MIME type of response (if any)
@@ -102,7 +103,12 @@ module HTTP
102
103
 
103
104
  # Inspect a response
104
105
  def inspect
105
- "#<#{self.class}/#{@version} #{code} #{reason} #{headers.inspect}>"
106
+ "#<#{self.class}/#{@version} #{code} #{reason} #{headers.to_h.inspect}>"
107
+ end
108
+
109
+ # @return [HTTP::Response::Caching]
110
+ def caching
111
+ Caching.new self
106
112
  end
107
113
  end
108
114
  end
@@ -1,5 +1,5 @@
1
- require 'forwardable'
2
- require 'http/client'
1
+ require "forwardable"
2
+ require "http/client"
3
3
 
4
4
  module HTTP
5
5
  class Response
@@ -10,13 +10,16 @@ module HTTP
10
10
  def_delegator :to_s, :empty?
11
11
 
12
12
  def initialize(client)
13
- @client = client
14
- @streaming = nil
15
- @contents = nil
13
+ @client = client
14
+ @streaming = nil
15
+ @contents = nil
16
+ @active_seq = client.sequence_id
16
17
  end
17
18
 
18
19
  # (see HTTP::Client#readpartial)
19
20
  def readpartial(*args)
21
+ check_sequence!
22
+
20
23
  stream!
21
24
  @client.readpartial(*args)
22
25
  end
@@ -31,11 +34,13 @@ module HTTP
31
34
  # @return [String] eagerly consume the entire body as a string
32
35
  def to_s
33
36
  return @contents if @contents
34
- fail StateError, 'body is being streamed' unless @streaming.nil?
37
+
38
+ fail StateError, "body is being streamed" unless @streaming.nil?
39
+ check_sequence!
35
40
 
36
41
  begin
37
42
  @streaming = false
38
- @contents = ''
43
+ @contents = ""
39
44
  while (chunk = @client.readpartial)
40
45
  @contents << chunk
41
46
  end
@@ -48,9 +53,17 @@ module HTTP
48
53
  end
49
54
  alias_method :to_str, :to_s
50
55
 
56
+ def check_sequence!
57
+ return unless @active_seq != @client.sequence_id
58
+
59
+ fail StateError, "Sequence ID #{@active_seq} does not match #{@client.sequence_id}. You must read the entire request off."
60
+ end
61
+
62
+ private :check_sequence!
63
+
51
64
  # Assert that the body is actively being streamed
52
65
  def stream!
53
- fail StateError, 'body has already been consumed' if @streaming == false
66
+ fail StateError, "body has already been consumed" if @streaming == false
54
67
  @streaming = true
55
68
  end
56
69
 
@@ -0,0 +1,142 @@
1
+ require "http/cache/headers"
2
+ require "http/response/string_body"
3
+ require "http/response/io_body"
4
+
5
+ module HTTP
6
+ class Response
7
+ # Decorator class for responses to provide convenience methods
8
+ # related to caching.
9
+ class Caching < DelegateClass(HTTP::Response)
10
+ CACHEABLE_RESPONSE_CODES = [200, 203, 300, 301, 410].freeze
11
+
12
+ def initialize(obj)
13
+ super
14
+ @requested_at = nil
15
+ @received_at = nil
16
+ end
17
+
18
+ # @return [HTTP::Response::Caching]
19
+ def caching
20
+ self
21
+ end
22
+
23
+ # @return [Boolean] true iff this response is stale
24
+ def stale?
25
+ expired? || cache_headers.must_revalidate?
26
+ end
27
+
28
+ # @returns [Boolean] true iff this response has expired
29
+ def expired?
30
+ current_age >= cache_headers.max_age
31
+ end
32
+
33
+ # @return [Boolean] true iff this response is cacheable
34
+ #
35
+ # ---
36
+ # A Vary header field-value of "*" always fails to match and
37
+ # subsequent requests on that resource can only be properly
38
+ # interpreted by the
39
+ def cacheable?
40
+ @cacheable ||=
41
+ begin
42
+ CACHEABLE_RESPONSE_CODES.include?(code) \
43
+ && !(cache_headers.vary_star? ||
44
+ cache_headers.no_store? ||
45
+ cache_headers.no_cache?)
46
+ end
47
+ end
48
+
49
+ # @return [Numeric] the current age (in seconds) of this response
50
+ #
51
+ # ---
52
+ # Algo from https://tools.ietf.org/html/rfc2616#section-13.2.3
53
+ def current_age
54
+ now = Time.now
55
+ age_value = headers.get("Age").map(&:to_i).max || 0
56
+
57
+ apparent_age = [0, received_at - server_response_time].max
58
+ corrected_received_age = [apparent_age, age_value].max
59
+ response_delay = [0, received_at - requested_at].max
60
+ corrected_initial_age = corrected_received_age + response_delay
61
+ resident_time = [0, now - received_at].max
62
+
63
+ corrected_initial_age + resident_time
64
+ end
65
+
66
+ # @return [Time] the time at which this response was requested
67
+ def requested_at
68
+ @requested_at ||= received_at
69
+ end
70
+ attr_writer :requested_at
71
+
72
+ # @return [Time] the time at which this response was received
73
+ def received_at
74
+ @received_at ||= Time.now
75
+ end
76
+ attr_writer :received_at
77
+
78
+ # Update self based on this response being revalidated by the
79
+ # server.
80
+ def validated!(validating_response)
81
+ headers.merge!(validating_response.headers)
82
+ self.requested_at = validating_response.requested_at
83
+ self.received_at = validating_response.received_at
84
+ end
85
+
86
+ # @return [HTTP::Cache::Headers] cache control headers helper object.
87
+ def cache_headers
88
+ @cache_headers ||= HTTP::Cache::Headers.new headers
89
+ end
90
+
91
+ def body
92
+ @body ||= if __getobj__.body.respond_to? :each
93
+ __getobj__.body
94
+ else
95
+ StringBody.new(__getobj__.body.to_s)
96
+ end
97
+ end
98
+
99
+ def body=(new_body)
100
+ @body = if new_body.respond_to?(:readpartial) && new_body.respond_to?(:read)
101
+ # IO-ish, probably a rack cache response body
102
+ IoBody.new(new_body)
103
+
104
+ elsif new_body.respond_to? :join
105
+ # probably an array of body parts (rack cache does this sometimes)
106
+ StringBody.new(new_body.join(""))
107
+
108
+ elsif new_body.respond_to? :readpartial
109
+ # normal body, just use it.
110
+ new_body
111
+
112
+ else
113
+ # backstop, just to_s it
114
+ StringBody.new(new_body.to_s)
115
+ end
116
+ end
117
+
118
+ def vary
119
+ headers.get("Vary").first
120
+ end
121
+
122
+ protected
123
+
124
+ # @return [Time] the time at which the server generated this response.
125
+ def server_response_time
126
+ headers.get("Date")
127
+ .map(&method(:to_time_or_epoch))
128
+ .max || begin
129
+ # set it if it is not already set
130
+ headers["Date"] = received_at.httpdate
131
+ received_at
132
+ end
133
+ end
134
+
135
+ def to_time_or_epoch(t_str)
136
+ Time.httpdate(t_str)
137
+ rescue ArgumentError
138
+ Time.at(0)
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,63 @@
1
+ module HTTP
2
+ class Response
3
+ # A Body class that wraps an IO, rather than a the client
4
+ # object.
5
+ class IoBody
6
+ include Enumerable
7
+ extend Forwardable
8
+
9
+ # @return [String,nil] the next `size` octets part of the
10
+ # body, or nil if whole body has already been read.
11
+ def readpartial(size = HTTP::Connection::BUFFER_SIZE)
12
+ stream!
13
+ return nil if stream.eof?
14
+
15
+ stream.readpartial(size)
16
+ end
17
+
18
+ # Iterate over the body, allowing it to be enumerable
19
+ def each
20
+ while (part = readpartial)
21
+ yield part
22
+ end
23
+ end
24
+
25
+ # @return [String] eagerly consume the entire body as a string
26
+ def to_s
27
+ @contents ||= readall
28
+ end
29
+ alias_method :to_str, :to_s
30
+
31
+ def_delegator :to_s, :empty?
32
+
33
+ # Assert that the body is actively being streamed
34
+ def stream!
35
+ fail StateError, "body has already been consumed" if @streaming == false
36
+ @streaming = true
37
+ end
38
+
39
+ # Easier to interpret string inspect
40
+ def inspect
41
+ "#<#{self.class}:#{object_id.to_s(16)} @streaming=#{!!@streaming}>"
42
+ end
43
+
44
+ protected
45
+
46
+ def initialize(an_io)
47
+ @streaming = nil
48
+ @stream = an_io
49
+ end
50
+
51
+ attr_reader :contents, :stream
52
+
53
+ def readall
54
+ fail StateError, "body is being streamed" unless @streaming.nil?
55
+
56
+ @streaming = false
57
+ "".tap do |buf|
58
+ buf << stream.read until stream.eof?
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -18,7 +18,7 @@ module HTTP
18
18
  end
19
19
 
20
20
  def http_version
21
- @parser.http_version.join('.')
21
+ @parser.http_version.join(".")
22
22
  end
23
23
 
24
24
  def status_code
@@ -1,6 +1,6 @@
1
- require 'delegate'
1
+ require "delegate"
2
2
 
3
- require 'http/response/status/reasons'
3
+ require "http/response/status/reasons"
4
4
 
5
5
  module HTTP
6
6
  class Response
@@ -22,7 +22,6 @@ module HTTP
22
22
  when object.is_a?(String) then SYMBOL_CODES[symbolize object]
23
23
  when object.is_a?(Symbol) then SYMBOL_CODES[object]
24
24
  when object.is_a?(Numeric) then object.to_i
25
- else nil
26
25
  end
27
26
 
28
27
  return new code if code
@@ -31,7 +30,7 @@ module HTTP
31
30
  end
32
31
  alias_method :[], :coerce
33
32
 
34
- private
33
+ private
35
34
 
36
35
  # Symbolizes given string
37
36
  #
@@ -44,7 +43,7 @@ module HTTP
44
43
  # @param [#to_s] str
45
44
  # @return [Symbol]
46
45
  def symbolize(str)
47
- str.to_s.downcase.gsub(/-/, ' ').gsub(/[^a-z ]/, '').gsub(/\s+/, '_').to_sym
46
+ str.to_s.downcase.gsub(/-/, " ").gsub(/[^a-z ]/, "").gsub(/\s+/, "_").to_sym
48
47
  end
49
48
  end
50
49
 
@@ -73,13 +72,6 @@ module HTTP
73
72
  # @return [Fixnum] status code
74
73
  attr_reader :code
75
74
 
76
- if RUBY_VERSION < '1.9.0'
77
- # @param [#to_i] code
78
- def initialize(code)
79
- super __setobj__ code
80
- end
81
- end
82
-
83
75
  # @see REASONS
84
76
  # @return [String, nil] status message
85
77
  def reason
@@ -1,4 +1,4 @@
1
- require 'delegate'
1
+ require "delegate"
2
2
 
3
3
  module HTTP
4
4
  class Response
@@ -13,59 +13,59 @@ module HTTP
13
13
  #
14
14
  # @return [Hash<Fixnum => String>]
15
15
  REASONS = {
16
- 100 => 'Continue',
17
- 101 => 'Switching Protocols',
18
- 102 => 'Processing',
19
- 200 => 'OK',
20
- 201 => 'Created',
21
- 202 => 'Accepted',
22
- 203 => 'Non-Authoritative Information',
23
- 204 => 'No Content',
24
- 205 => 'Reset Content',
25
- 206 => 'Partial Content',
26
- 207 => 'Multi-Status',
27
- 226 => 'IM Used',
28
- 300 => 'Multiple Choices',
29
- 301 => 'Moved Permanently',
30
- 302 => 'Found',
31
- 303 => 'See Other',
32
- 304 => 'Not Modified',
33
- 305 => 'Use Proxy',
34
- 306 => 'Reserved',
35
- 307 => 'Temporary Redirect',
36
- 308 => 'Permanent Redirect',
37
- 400 => 'Bad Request',
38
- 401 => 'Unauthorized',
39
- 402 => 'Payment Required',
40
- 403 => 'Forbidden',
41
- 404 => 'Not Found',
42
- 405 => 'Method Not Allowed',
43
- 406 => 'Not Acceptable',
44
- 407 => 'Proxy Authentication Required',
45
- 408 => 'Request Timeout',
46
- 409 => 'Conflict',
47
- 410 => 'Gone',
48
- 411 => 'Length Required',
49
- 412 => 'Precondition Failed',
50
- 413 => 'Request Entity Too Large',
51
- 414 => 'Request-URI Too Long',
52
- 415 => 'Unsupported Media Type',
53
- 416 => 'Requested Range Not Satisfiable',
54
- 417 => 'Expectation Failed',
16
+ 100 => "Continue",
17
+ 101 => "Switching Protocols",
18
+ 102 => "Processing",
19
+ 200 => "OK",
20
+ 201 => "Created",
21
+ 202 => "Accepted",
22
+ 203 => "Non-Authoritative Information",
23
+ 204 => "No Content",
24
+ 205 => "Reset Content",
25
+ 206 => "Partial Content",
26
+ 207 => "Multi-Status",
27
+ 226 => "IM Used",
28
+ 300 => "Multiple Choices",
29
+ 301 => "Moved Permanently",
30
+ 302 => "Found",
31
+ 303 => "See Other",
32
+ 304 => "Not Modified",
33
+ 305 => "Use Proxy",
34
+ 306 => "Reserved",
35
+ 307 => "Temporary Redirect",
36
+ 308 => "Permanent Redirect",
37
+ 400 => "Bad Request",
38
+ 401 => "Unauthorized",
39
+ 402 => "Payment Required",
40
+ 403 => "Forbidden",
41
+ 404 => "Not Found",
42
+ 405 => "Method Not Allowed",
43
+ 406 => "Not Acceptable",
44
+ 407 => "Proxy Authentication Required",
45
+ 408 => "Request Timeout",
46
+ 409 => "Conflict",
47
+ 410 => "Gone",
48
+ 411 => "Length Required",
49
+ 412 => "Precondition Failed",
50
+ 413 => "Request Entity Too Large",
51
+ 414 => "Request-URI Too Long",
52
+ 415 => "Unsupported Media Type",
53
+ 416 => "Requested Range Not Satisfiable",
54
+ 417 => "Expectation Failed",
55
55
  418 => "I'm a Teapot",
56
- 422 => 'Unprocessable Entity',
57
- 423 => 'Locked',
58
- 424 => 'Failed Dependency',
59
- 426 => 'Upgrade Required',
60
- 500 => 'Internal Server Error',
61
- 501 => 'Not Implemented',
62
- 502 => 'Bad Gateway',
63
- 503 => 'Service Unavailable',
64
- 504 => 'Gateway Timeout',
65
- 505 => 'HTTP Version Not Supported',
66
- 506 => 'Variant Also Negotiates',
67
- 507 => 'Insufficient Storage',
68
- 510 => 'Not Extended'
56
+ 422 => "Unprocessable Entity",
57
+ 423 => "Locked",
58
+ 424 => "Failed Dependency",
59
+ 426 => "Upgrade Required",
60
+ 500 => "Internal Server Error",
61
+ 501 => "Not Implemented",
62
+ 502 => "Bad Gateway",
63
+ 503 => "Service Unavailable",
64
+ 504 => "Gateway Timeout",
65
+ 505 => "HTTP Version Not Supported",
66
+ 506 => "Variant Also Negotiates",
67
+ 507 => "Insufficient Storage",
68
+ 510 => "Not Extended"
69
69
  }.each { |_, v| v.freeze }.freeze
70
70
  end
71
71
  end