http 0.7.4 → 0.8.0.pre

Sign up to get free protection for your applications and to get access to all the features.
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