koala 1.0.0.rc → 1.1.0

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 (40) hide show
  1. data/.autotest +12 -0
  2. data/.gitignore +3 -1
  3. data/.travis.yml +8 -0
  4. data/CHANGELOG +40 -18
  5. data/Gemfile +4 -0
  6. data/LICENSE +1 -1
  7. data/autotest/discover.rb +1 -0
  8. data/koala.gemspec +8 -8
  9. data/lib/koala/batch_operation.rb +74 -0
  10. data/lib/koala/graph_api.rb +103 -102
  11. data/lib/koala/graph_batch_api.rb +87 -0
  12. data/lib/koala/graph_collection.rb +54 -0
  13. data/lib/koala/http_services/net_http_service.rb +92 -0
  14. data/lib/koala/http_services/typhoeus_service.rb +37 -0
  15. data/lib/koala/http_services.rb +13 -113
  16. data/lib/koala/oauth.rb +181 -0
  17. data/lib/koala/realtime_updates.rb +5 -14
  18. data/lib/koala/rest_api.rb +13 -8
  19. data/lib/koala/uploadable_io.rb +137 -77
  20. data/lib/koala.rb +39 -215
  21. data/readme.md +58 -32
  22. data/spec/cases/api_base_spec.rb +4 -4
  23. data/spec/cases/graph_api_batch_spec.rb +609 -0
  24. data/spec/cases/http_services/http_service_spec.rb +87 -12
  25. data/spec/cases/http_services/net_http_service_spec.rb +259 -77
  26. data/spec/cases/http_services/typhoeus_service_spec.rb +29 -21
  27. data/spec/cases/koala_spec.rb +55 -0
  28. data/spec/cases/oauth_spec.rb +40 -5
  29. data/spec/cases/realtime_updates_spec.rb +3 -3
  30. data/spec/cases/test_users_spec.rb +1 -1
  31. data/spec/cases/uploadable_io_spec.rb +56 -14
  32. data/spec/fixtures/cat.m4v +0 -0
  33. data/spec/fixtures/mock_facebook_responses.yml +110 -13
  34. data/spec/spec_helper.rb +2 -1
  35. data/spec/support/graph_api_shared_examples.rb +108 -37
  36. data/spec/support/json_testing_fix.rb +18 -0
  37. data/spec/support/mock_http_service.rb +57 -56
  38. data/spec/support/rest_api_shared_examples.rb +131 -7
  39. data/spec/support/setup_mocks_or_live.rb +3 -4
  40. metadata +33 -49
@@ -2,9 +2,9 @@ require 'koala'
2
2
 
3
3
  module Koala
4
4
  class UploadableIO
5
- attr_reader :io_or_path, :content_type
5
+ attr_reader :io_or_path, :content_type, :requires_base_http_service
6
6
 
7
- def initialize(io_or_path_or_mixed, content_type = nil)
7
+ def initialize(io_or_path_or_mixed, content_type = nil, filename = nil)
8
8
  # see if we got the right inputs
9
9
  if content_type.nil?
10
10
  parse_init_mixed_param io_or_path_or_mixed
@@ -13,103 +13,163 @@ module Koala
13
13
  @content_type = content_type
14
14
  end
15
15
 
16
+ # Probably a StringIO or similar object, which won't work with Typhoeus
17
+ @requires_base_http_service = @io_or_path.respond_to?(:read) && !@io_or_path.kind_of?(File)
18
+
19
+ # filename is used in the Ads API
20
+ @filename = filename || "koala-io-file.dum"
21
+
16
22
  raise KoalaError.new("Invalid arguments to initialize an UploadableIO") unless @io_or_path
17
23
  raise KoalaError.new("Unable to determine MIME type for UploadableIO") if !@content_type && Koala.multipart_requires_content_type?
18
24
  end
19
25
 
20
26
  def to_upload_io
21
- UploadIO.new(@io_or_path, @content_type, "koala-io-file.dum")
27
+ UploadIO.new(@io_or_path, @content_type, @filename)
22
28
  end
23
29
 
24
30
  def to_file
25
31
  @io_or_path.is_a?(String) ? File.open(@io_or_path) : @io_or_path
26
32
  end
33
+
34
+ def self.binary_content?(content)
35
+ content.is_a?(UploadableIO) || DETECTION_STRATEGIES.detect {|method| send(method, content)}
36
+ end
27
37
 
28
38
  private
29
- PARSE_STRATEGIES = [
30
- :parse_rails_3_param,
31
- :parse_sinatra_param,
32
- :parse_file_object,
33
- :parse_string_path
34
- ]
35
-
36
- def parse_init_mixed_param(mixed)
37
- PARSE_STRATEGIES.each do |method|
38
- send(method, mixed)
39
- return if @io_or_path && @content_type
40
- end
39
+ DETECTION_STRATEGIES = [
40
+ :sinatra_param?,
41
+ :rails_3_param?,
42
+ :file_param?
43
+ ]
44
+
45
+ PARSE_STRATEGIES = [
46
+ :parse_rails_3_param,
47
+ :parse_sinatra_param,
48
+ :parse_file_object,
49
+ :parse_string_path
50
+ ]
51
+
52
+ def parse_init_mixed_param(mixed)
53
+ PARSE_STRATEGIES.each do |method|
54
+ send(method, mixed)
55
+ return if @io_or_path && @content_type
41
56
  end
42
-
43
- # Expects a parameter of type ActionDispatch::Http::UploadedFile
44
- def parse_rails_3_param(uploaded_file)
45
- if uploaded_file.respond_to?(:content_type) and uploaded_file.respond_to?(:tempfile) and uploaded_file.tempfile.respond_to?(:path)
46
- @io_or_path = uploaded_file.tempfile.path
47
- @content_type = uploaded_file.content_type
48
- end
57
+ end
58
+
59
+ # Expects a parameter of type ActionDispatch::Http::UploadedFile
60
+ def self.rails_3_param?(uploaded_file)
61
+ uploaded_file.respond_to?(:content_type) and uploaded_file.respond_to?(:tempfile) and uploaded_file.tempfile.respond_to?(:path)
62
+ end
63
+
64
+ def parse_rails_3_param(uploaded_file)
65
+ if UploadableIO.rails_3_param?(uploaded_file)
66
+ @io_or_path = uploaded_file.tempfile.path
67
+ @content_type = uploaded_file.content_type
49
68
  end
50
-
51
- # Expects a Sinatra hash of file info
52
- def parse_sinatra_param(file_hash)
53
- if file_hash.kind_of?(Hash) and file_hash.has_key?(:type) and file_hash.has_key?(:tempfile)
54
- @io_or_path = file_hash[:tempfile]
55
- @content_type = file_hash[:type] || detect_mime_type(tempfile)
56
- end
69
+ end
70
+
71
+ # Expects a Sinatra hash of file info
72
+ def self.sinatra_param?(file_hash)
73
+ file_hash.kind_of?(Hash) and file_hash.has_key?(:type) and file_hash.has_key?(:tempfile)
74
+ end
75
+
76
+ def parse_sinatra_param(file_hash)
77
+ if UploadableIO.sinatra_param?(file_hash)
78
+ @io_or_path = file_hash[:tempfile]
79
+ @content_type = file_hash[:type] || detect_mime_type(tempfile)
57
80
  end
58
-
59
- # takes a file object
60
- def parse_file_object(file)
61
- if file.kind_of?(File)
62
- @io_or_path = file
63
- @content_type = detect_mime_type(file.path)
64
- end
81
+ end
82
+
83
+ # takes a file object
84
+ def self.file_param?(file)
85
+ file.kind_of?(File)
86
+ end
87
+
88
+ def parse_file_object(file)
89
+ if UploadableIO.file_param?(file)
90
+ @io_or_path = file
91
+ @content_type = detect_mime_type(file.path)
65
92
  end
66
-
67
- def parse_string_path(path)
68
- if path.kind_of?(String)
69
- @io_or_path = path
70
- @content_type = detect_mime_type(path)
71
- end
93
+ end
94
+
95
+ def parse_string_path(path)
96
+ if path.kind_of?(String)
97
+ @io_or_path = path
98
+ @content_type = detect_mime_type(path)
72
99
  end
73
-
74
- MIME_TYPE_STRATEGIES = [
75
- :use_mime_module,
76
- :use_simple_detection
77
- ]
78
-
79
- def detect_mime_type(filename)
80
- if filename
81
- MIME_TYPE_STRATEGIES.each do |method|
82
- result = send(method, filename)
83
- return result if result
84
- end
100
+ end
101
+
102
+ MIME_TYPE_STRATEGIES = [
103
+ :use_mime_module,
104
+ :use_simple_detection
105
+ ]
106
+
107
+ def detect_mime_type(filename)
108
+ if filename
109
+ MIME_TYPE_STRATEGIES.each do |method|
110
+ result = send(method, filename)
111
+ return result if result
85
112
  end
86
- nil # if we can't find anything
87
113
  end
88
-
89
- def use_mime_module(filename)
90
- # if the user has installed mime/types, we can use that
91
- # if not, rescue and return nil
92
- begin
93
- type = MIME::Types.type_for(filename).first
94
- type ? type.to_s : nil
95
- rescue
96
- nil
97
- end
114
+ nil # if we can't find anything
115
+ end
116
+
117
+ def use_mime_module(filename)
118
+ # if the user has installed mime/types, we can use that
119
+ # if not, rescue and return nil
120
+ begin
121
+ type = MIME::Types.type_for(filename).first
122
+ type ? type.to_s : nil
123
+ rescue
124
+ nil
98
125
  end
99
-
100
- def use_simple_detection(filename)
101
- # very rudimentary extension analysis for images
102
- # first, get the downcased extension, or an empty string if it doesn't exist
103
- extension = ((filename.match(/\.([a-zA-Z0-9]+)$/) || [])[1] || "").downcase
104
- if extension == ""
126
+ end
127
+
128
+ def use_simple_detection(filename)
129
+ # very rudimentary extension analysis for images
130
+ # first, get the downcased extension, or an empty string if it doesn't exist
131
+ extension = ((filename.match(/\.([a-zA-Z0-9]+)$/) || [])[1] || "").downcase
132
+ case extension
133
+ when ""
105
134
  nil
106
- elsif extension == "jpg" || extension == "jpeg"
135
+ # images
136
+ when "jpg", "jpeg"
107
137
  "image/jpeg"
108
- elsif extension == "png"
138
+ when "png"
109
139
  "image/png"
110
- elsif extension == "gif"
140
+ when "gif"
111
141
  "image/gif"
112
- end
113
- end
142
+
143
+ # video
144
+ when "3g2"
145
+ "video/3gpp2"
146
+ when "3gp", "3gpp"
147
+ "video/3gpp"
148
+ when "asf"
149
+ "video/x-ms-asf"
150
+ when "avi"
151
+ "video/x-msvideo"
152
+ when "flv"
153
+ "video/x-flv"
154
+ when "m4v"
155
+ "video/x-m4v"
156
+ when "mkv"
157
+ "video/x-matroska"
158
+ when "mod"
159
+ "video/mod"
160
+ when "mov", "qt"
161
+ "video/quicktime"
162
+ when "mp4", "mpeg4"
163
+ "video/mp4"
164
+ when "mpe", "mpeg", "mpg", "tod", "vob"
165
+ "video/mpeg"
166
+ when "nsv"
167
+ "application/x-winamp"
168
+ when "ogm", "ogv"
169
+ "video/ogg"
170
+ when "wmv"
171
+ "video/x-ms-wmv"
172
+ end
173
+ end
114
174
  end
115
- end
175
+ end
data/lib/koala.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'cgi'
2
2
  require 'digest/md5'
3
3
 
4
- require 'json'
4
+ require 'multi_json'
5
5
 
6
6
  # OpenSSL and Base64 are required to support signed_request
7
7
  require 'openssl'
@@ -9,10 +9,16 @@ require 'base64'
9
9
 
10
10
  # include koala modules
11
11
  require 'koala/http_services'
12
+ require 'koala/http_services/net_http_service'
13
+ require 'koala/oauth'
12
14
  require 'koala/graph_api'
15
+ require 'koala/graph_batch_api'
16
+ require 'koala/batch_operation'
17
+ require 'koala/graph_collection'
13
18
  require 'koala/rest_api'
14
19
  require 'koala/realtime_updates'
15
20
  require 'koala/test_users'
21
+ require 'koala/http_services'
16
22
 
17
23
  # add KoalaIO class
18
24
  require 'koala/uploadable_io'
@@ -21,25 +27,9 @@ module Koala
21
27
 
22
28
  module Facebook
23
29
  # Ruby client library for the Facebook Platform.
24
- # Copyright 2010 Facebook
25
- # Adapted from the Python library by Alex Koppel, Rafi Jacoby, and the team at Context Optional
26
- #
27
- # Licensed under the Apache License, Version 2.0 (the "License"); you may
28
- # not use this file except in compliance with the License. You may obtain
29
- # a copy of the License at
30
- # http://www.apache.org/licenses/LICENSE-2.0
31
- #
32
- # Unless required by applicable law or agreed to in writing, software
33
- # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
34
- # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
35
- # License for the specific language governing permissions and limitations
36
- # under the License.
37
- #
38
- # This client library is designed to support the Graph API and the official
39
- # Facebook JavaScript SDK, which is the canonical way to implement
40
- # Facebook authentication. Read more about the Graph API at
41
- # http://developers.facebook.com/docs/api. You can download the Facebook
42
- # JavaScript SDK at http://github.com/facebook/connect-js/.
30
+ # Copyright 2010-2011 Alex Koppel
31
+ # Contributors: Alex Koppel, Chris Baclig, Rafi Jacoby, and the team at Context Optional
32
+ # http://github.com/arsduo/koala
43
33
 
44
34
  class API
45
35
  # initialize with an access token
@@ -63,27 +53,27 @@ module Koala
63
53
  # in the case of a server error
64
54
  raise APIError.new({"type" => "HTTP #{result.status.to_s}", "message" => "Response body: #{result.body}"}) if result.status >= 500
65
55
 
66
- # Parse the body as JSON and check for errors if provided a mechanism to do so
56
+ # parse the body as JSON and run it through the error checker (if provided)
67
57
  # Note: Facebook sometimes sends results like "true" and "false", which aren't strictly objects
68
- # and cause JSON.parse to fail -- so we account for that by wrapping the result in []
69
- body = response = JSON.parse("[#{result.body.to_s}]")[0]
70
- if error_checking_block
71
- yield(body)
72
- end
58
+ # and cause MultiJson.decode to fail -- so we account for that by wrapping the result in []
59
+ body = MultiJson.decode("[#{result.body.to_s}]")[0]
60
+ yield body if error_checking_block
73
61
 
74
- # now return the desired information
75
- if options[:http_component]
76
- result.send(options[:http_component])
77
- else
78
- body
79
- end
62
+ # if we want a component other than the body (e.g. redirect header for images), return that
63
+ options[:http_component] ? result.send(options[:http_component]) : body
80
64
  end
81
65
  end
82
66
 
67
+ # APIs
68
+
83
69
  class GraphAPI < API
84
70
  include GraphAPIMethods
85
71
  end
86
-
72
+
73
+ class GraphBatchAPI < GraphAPI
74
+ include GraphBatchAPIMethods
75
+ end
76
+
87
77
  class RestAPI < API
88
78
  include RestAPIMethods
89
79
  end
@@ -103,6 +93,8 @@ module Koala
103
93
  attr_reader :graph_api
104
94
  end
105
95
 
96
+ # Errors
97
+
106
98
  class APIError < StandardError
107
99
  attr_accessor :fb_error_type
108
100
  def initialize(details = {})
@@ -110,201 +102,33 @@ module Koala
110
102
  super("#{fb_error_type}: #{details["message"]}")
111
103
  end
112
104
  end
105
+ end
113
106
 
107
+ class KoalaError < StandardError; end
114
108
 
115
- class OAuth
116
- attr_reader :app_id, :app_secret, :oauth_callback_url
117
- def initialize(app_id, app_secret, oauth_callback_url = nil)
118
- @app_id = app_id
119
- @app_secret = app_secret
120
- @oauth_callback_url = oauth_callback_url
121
- end
122
-
123
- def get_user_info_from_cookie(cookie_hash)
124
- # Parses the cookie set by the official Facebook JavaScript SDK.
125
- #
126
- # cookies should be a Hash, like the one Rails provides
127
- #
128
- # If the user is logged in via Facebook, we return a dictionary with the
129
- # keys "uid" and "access_token". The former is the user's Facebook ID,
130
- # and the latter can be used to make authenticated requests to the Graph API.
131
- # If the user is not logged in, we return None.
132
- #
133
- # Download the official Facebook JavaScript SDK at
134
- # http://github.com/facebook/connect-js/. Read more about Facebook
135
- # authentication at http://developers.facebook.com/docs/authentication/.
136
-
137
- if fb_cookie = cookie_hash["fbs_" + @app_id.to_s]
138
- # remove the opening/closing quote
139
- fb_cookie = fb_cookie.gsub(/\"/, "")
140
-
141
- # since we no longer get individual cookies, we have to separate out the components ourselves
142
- components = {}
143
- fb_cookie.split("&").map {|param| param = param.split("="); components[param[0]] = param[1]}
144
-
145
- # generate the signature and make sure it matches what we expect
146
- auth_string = components.keys.sort.collect {|a| a == "sig" ? nil : "#{a}=#{components[a]}"}.reject {|a| a.nil?}.join("")
147
- sig = Digest::MD5.hexdigest(auth_string + @app_secret)
148
- sig == components["sig"] && (components["expires"] == "0" || Time.now.to_i < components["expires"].to_i) ? components : nil
149
- end
150
- end
151
- alias_method :get_user_info_from_cookies, :get_user_info_from_cookie
152
-
153
- def get_user_from_cookie(cookies)
154
- if info = get_user_info_from_cookies(cookies)
155
- string = info["uid"]
156
- end
157
- end
158
- alias_method :get_user_from_cookies, :get_user_from_cookie
159
-
160
- # URLs
161
-
162
- def url_for_oauth_code(options = {})
163
- # for permissions, see http://developers.facebook.com/docs/authentication/permissions
164
- permissions = options[:permissions]
165
- scope = permissions ? "&scope=#{permissions.is_a?(Array) ? permissions.join(",") : permissions}" : ""
166
- display = options.has_key?(:display) ? "&display=#{options[:display]}" : ""
167
-
168
- callback = options[:callback] || @oauth_callback_url
169
- raise ArgumentError, "url_for_oauth_code must get a callback either from the OAuth object or in the options!" unless callback
170
-
171
- # Creates the URL for oauth authorization for a given callback and optional set of permissions
172
- "https://#{GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}#{scope}#{display}"
173
- end
174
-
175
- def url_for_access_token(code, options = {})
176
- # Creates the URL for the token corresponding to a given code generated by Facebook
177
- callback = options[:callback] || @oauth_callback_url
178
- raise ArgumentError, "url_for_access_token must get a callback either from the OAuth object or in the parameters!" unless callback
179
- "https://#{GRAPH_SERVER}/oauth/access_token?client_id=#{@app_id}&redirect_uri=#{callback}&client_secret=#{@app_secret}&code=#{code}"
180
- end
181
-
182
- def get_access_token_info(code)
183
- # convenience method to get a parsed token from Facebook for a given code
184
- # should this require an OAuth callback URL?
185
- get_token_from_server(:code => code, :redirect_uri => @oauth_callback_url)
186
- end
187
-
188
- def get_access_token(code)
189
- # upstream methods will throw errors if needed
190
- if info = get_access_token_info(code)
191
- string = info["access_token"]
192
- end
193
- end
194
-
195
- def get_app_access_token_info
196
- # convenience method to get a the application's sessionless access token
197
- get_token_from_server({:type => 'client_cred'}, true)
198
- end
199
-
200
- def get_app_access_token
201
- if info = get_app_access_token_info
202
- string = info["access_token"]
203
- end
204
- end
205
-
206
- # Originally provided directly by Facebook, however this has changed
207
- # as their concept of crypto changed. For historic purposes, this is their proposal:
208
- # https://developers.facebook.com/docs/authentication/canvas/encryption_proposal/
209
- # Currently see https://github.com/facebook/php-sdk/blob/master/src/facebook.php#L758
210
- # for a more accurate reference implementation strategy.
211
- def parse_signed_request(input)
212
- encoded_sig, encoded_envelope = input.split('.', 2)
213
- signature = base64_url_decode(encoded_sig).unpack("H*").first
214
- envelope = JSON.parse(base64_url_decode(encoded_envelope))
215
-
216
- raise "SignedRequest: Unsupported algorithm #{envelope['algorithm']}" if envelope['algorithm'] != 'HMAC-SHA256'
217
-
218
- # now see if the signature is valid (digest, key, data)
219
- hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, @app_secret, encoded_envelope.tr("-_", "+/"))
220
- raise 'SignedRequest: Invalid signature' if (signature != hmac)
221
-
222
- return envelope
223
- end
224
-
225
- # from session keys
226
- def get_token_info_from_session_keys(sessions)
227
- # fetch the OAuth tokens from Facebook
228
- response = fetch_token_string({
229
- :type => 'client_cred',
230
- :sessions => sessions.join(",")
231
- }, true, "exchange_sessions")
232
-
233
- # Facebook returns an empty body in certain error conditions
234
- if response == ""
235
- raise APIError.new({
236
- "type" => "ArgumentError",
237
- "message" => "get_token_from_session_key received an error (empty response body) for sessions #{sessions.inspect}!"
238
- })
239
- end
240
-
241
- JSON.parse(response)
242
- end
243
-
244
- def get_tokens_from_session_keys(sessions)
245
- # get the original hash results
246
- results = get_token_info_from_session_keys(sessions)
247
- # now recollect them as just the access tokens
248
- results.collect { |r| r ? r["access_token"] : nil }
249
- end
250
-
251
- def get_token_from_session_key(session)
252
- # convenience method for a single key
253
- # gets the overlaoded strings automatically
254
- get_tokens_from_session_keys([session])[0]
255
- end
256
-
257
- protected
258
-
259
- def get_token_from_server(args, post = false)
260
- # fetch the result from Facebook's servers
261
- result = fetch_token_string(args, post)
262
-
263
- # if we have an error, parse the error JSON and raise an error
264
- raise APIError.new((JSON.parse(result)["error"] rescue nil) || {}) if result =~ /error/
265
-
266
- # otherwise, parse the access token
267
- parse_access_token(result)
268
- end
269
-
270
- def parse_access_token(response_text)
271
- components = response_text.split("&").inject({}) do |hash, bit|
272
- key, value = bit.split("=")
273
- hash.merge!(key => value)
274
- end
275
- components
276
- end
277
-
278
- def fetch_token_string(args, post = false, endpoint = "access_token")
279
- Koala.make_request("/oauth/#{endpoint}", {
280
- :client_id => @app_id,
281
- :client_secret => @app_secret
282
- }.merge!(args), post ? "post" : "get", :use_ssl => true).body
283
- end
284
-
285
- # base 64
286
- # directly from https://github.com/facebook/crypto-request-examples/raw/master/sample.rb
287
- def base64_url_decode(str)
288
- str += '=' * (4 - str.length.modulo(4))
289
- Base64.decode64(str.tr('-_', '+/'))
290
- end
291
- end
109
+ # Make an api request using the provided api service or one passed by the caller
110
+ def self.make_request(path, args, verb, options = {})
111
+ http_service = options.delete(:http_service) || Koala.http_service
112
+ options = options.merge(:use_ssl => true) if @always_use_ssl
113
+ http_service.make_request(path, args, verb, options)
292
114
  end
293
115
 
294
- class KoalaError< StandardError; end
295
-
296
116
  # finally, set up the http service Koala methods used to make requests
297
117
  # you can use your own (for HTTParty, etc.) by calling Koala.http_service = YourModule
298
- def self.http_service=(service)
299
- self.send(:include, service)
118
+ class << self
119
+ attr_accessor :http_service
120
+ attr_accessor :always_use_ssl
121
+ attr_accessor :base_http_service
300
122
  end
123
+ Koala.base_http_service = NetHTTPService
301
124
 
302
125
  # by default, try requiring Typhoeus -- if that works, use it
303
126
  # if you have Typheous and don't want to use it (or want another service),
304
127
  # you can run Koala.http_service = NetHTTPService (or MyHTTPService)
305
128
  begin
129
+ require 'koala/http_services/typhoeus_service'
306
130
  Koala.http_service = TyphoeusService
307
131
  rescue LoadError
308
- Koala.http_service = NetHTTPService
132
+ Koala.http_service = Koala.base_http_service
309
133
  end
310
134
  end