zerobounce-sdk 2.1.7 → 2.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +146 -19
- data/README.md +472 -410
- data/lib/zerobounce/api_urls.rb +9 -0
- data/lib/zerobounce/base_request.rb +208 -8
- data/lib/zerobounce/client.rb +22 -0
- data/lib/zerobounce/configuration.rb +24 -3
- data/lib/zerobounce/error.rb +18 -0
- data/lib/zerobounce/get_file_helper.rb +3 -7
- data/lib/zerobounce/mock_request.rb +31 -12
- data/lib/zerobounce/priority3.rb +331 -0
- data/lib/zerobounce/request.rb +77 -38
- data/lib/zerobounce/response_error.rb +117 -0
- data/lib/zerobounce/validate_sub_status.rb +1 -0
- data/lib/zerobounce/version.rb +1 -1
- data/lib/zerobounce/warmup.rb +467 -0
- data/lib/zerobounce.rb +101 -14
- metadata +8 -33
- data/.dockerignore +0 -17
- data/.env.example +0 -3
- data/.github/auto_assign.yml +0 -5
- data/.github/dependabot.yml +0 -35
- data/.github/workflows/auto_assign_ci.yaml +0 -21
- data/.github/workflows/codeql.yml +0 -105
- data/.github/workflows/publish.yml +0 -131
- data/.github/workflows/sdk_ci.yml +0 -29
- data/.gitignore +0 -70
- data/.rspec +0 -2
- data/.ruby-version +0 -1
- data/CODE_OF_CONDUCT.md +0 -69
- data/CONTRIBUTING.md +0 -112
- data/Dockerfile +0 -17
- data/Gemfile +0 -13
- data/Gemfile.lock +0 -135
- data/Rakefile +0 -26
- data/SECURITY.md +0 -21
- data/bin/bundle +0 -105
- data/bin/console +0 -7
- data/bin/setup +0 -6
- data/data/zerobounce-ai-scoring.csv +0 -31
- data/data/zerobounce-batch-validation.csv +0 -31
- data/documentation.md +0 -530
- data/files/scoring.csv +0 -1
- data/files/validation.csv +0 -1
- data/files/zerobounce-ai-scoring.csv +0 -31
- data/files/zerobounce-batch-validation.csv +0 -31
- data/zerobounce.gemspec +0 -49
data/lib/zerobounce/api_urls.rb
CHANGED
|
@@ -14,5 +14,14 @@ module Zerobounce
|
|
|
14
14
|
|
|
15
15
|
# Bulk API URLs
|
|
16
16
|
BULK_DEFAULT_URL = 'https://bulkapi.zerobounce.net/v2/'
|
|
17
|
+
|
|
18
|
+
# The S3 and SFTP/FTP bulk jobs live on the bulkapi host WITHOUT the /v2
|
|
19
|
+
# path. See https://www.zerobounce.net/docs/api-integrations.
|
|
20
|
+
BULK_ROOT_URL = 'https://bulkapi.zerobounce.net/'
|
|
21
|
+
|
|
22
|
+
# The list evaluator and the Inbox Placement Tester have their own base
|
|
23
|
+
# URLs, not paths built from the bulk one.
|
|
24
|
+
LIST_EVALUATOR_URL = 'https://bulkapi.zerobounce.net/v2/listevaluator/'
|
|
25
|
+
INBOX_PLACEMENT_URL = 'https://bulkapi.zerobounce.net/api/ipt'
|
|
17
26
|
end
|
|
18
27
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
+
require 'zerobounce/api_urls'
|
|
5
|
+
require 'zerobounce/response_error'
|
|
4
6
|
|
|
5
7
|
# https://github.com/rest-client/rest-client
|
|
6
8
|
require 'rest-client'
|
|
@@ -41,36 +43,111 @@ module Zerobounce
|
|
|
41
43
|
canonical
|
|
42
44
|
end
|
|
43
45
|
|
|
46
|
+
def self.__require_https__(url)
|
|
47
|
+
unless url.to_s.downcase.start_with?('https://')
|
|
48
|
+
raise ArgumentError, 'API URL must be an https:// URL'
|
|
49
|
+
end
|
|
50
|
+
url
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Removes the API key from error text. The key the request actually used
|
|
54
|
+
# (a Zerobounce::Client's or with_api_key's, which is thread-local) is
|
|
55
|
+
# redacted as well as the global one, in its raw and URL-encoded forms.
|
|
56
|
+
def self.__redact_api_key__(message)
|
|
57
|
+
text = message.to_s
|
|
58
|
+
[current_api_key, Zerobounce.config.apikey].uniq.each do |key|
|
|
59
|
+
next if key.nil? || key.to_s.strip.empty?
|
|
60
|
+
|
|
61
|
+
[key.to_s, URI.encode_www_form_component(key.to_s)].uniq.each do |form|
|
|
62
|
+
text = text.gsub(form, '***')
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
text
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.current_api_key
|
|
69
|
+
key = Thread.current[:zerobounce_api_key]
|
|
70
|
+
key.nil? ? Zerobounce.config.apikey : key
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The message for an HTTP error status.
|
|
74
|
+
def self.__http_error_message__(error)
|
|
75
|
+
code = error.http_code.to_i
|
|
76
|
+
# max_redirects is 0, so a 3xx is never followed; say so rather than
|
|
77
|
+
# reporting its (usually empty) body as an "Empty response".
|
|
78
|
+
if code.between?(300, 399)
|
|
79
|
+
return "ZeroBounce answered with a redirect (HTTP #{code}), which is not followed"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
ResponseError.message_from(error.response)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.__execute_request__(**kwargs)
|
|
86
|
+
kwargs[:max_redirects] = 0
|
|
87
|
+
user_agent = Zerobounce.config.headers && Zerobounce.config.headers[:user_agent]
|
|
88
|
+
kwargs[:headers] = (kwargs[:headers] || {}).merge(user_agent: user_agent) if user_agent
|
|
89
|
+
RestClient::Request.execute(**kwargs)
|
|
90
|
+
rescue RestClient::ExceptionWithResponse => e
|
|
91
|
+
# Re-raised as the same RestClient class (RestClient::NotFound and so
|
|
92
|
+
# on), so `rescue RestClient::...` and #http_code keep working as they
|
|
93
|
+
# did up to 2.1.8. Only the message changes: the API's short error text,
|
|
94
|
+
# with the key redacted.
|
|
95
|
+
e.message = __redact_api_key__(__http_error_message__(e))
|
|
96
|
+
raise e
|
|
97
|
+
rescue RestClient::Exception => e
|
|
98
|
+
e.message = __redact_api_key__(e.message)
|
|
99
|
+
raise e
|
|
100
|
+
rescue StandardError => e
|
|
101
|
+
# Same class and backtrace, redacted message. cause: nil, because the
|
|
102
|
+
# original (unredacted) error would otherwise ride along as #cause and
|
|
103
|
+
# be printed by #full_message.
|
|
104
|
+
raise e.exception(__redact_api_key__(e.message)), cause: nil
|
|
105
|
+
end
|
|
106
|
+
|
|
44
107
|
def self._get(root, path, params, content_type='application/json')
|
|
45
108
|
|
|
46
109
|
# puts path
|
|
47
110
|
# puts Zerobounce.config.apikey
|
|
48
111
|
|
|
49
|
-
|
|
112
|
+
api_key = current_api_key
|
|
113
|
+
raise ('API key must be assigned') if api_key.nil? || api_key.to_s.strip.empty?
|
|
50
114
|
|
|
51
|
-
params[:api_key] =
|
|
115
|
+
params[:api_key] = api_key
|
|
52
116
|
url = "#{Zerobounce::BaseRequest.__root_without_trailing_slashes__(root)}/#{path}"
|
|
117
|
+
Zerobounce::BaseRequest.__require_https__(url)
|
|
53
118
|
|
|
54
|
-
response =
|
|
119
|
+
response = __execute_request__(method: :get, url: url, timeout: 120, headers: { params: params })
|
|
55
120
|
return response
|
|
56
121
|
end
|
|
57
122
|
|
|
58
123
|
def self._post(root, path, params, content_type='application/json', filepath=nil)
|
|
59
124
|
|
|
60
|
-
|
|
125
|
+
api_key = current_api_key
|
|
126
|
+
raise ('API key must be assigned') if api_key.nil? || api_key.to_s.strip.empty?
|
|
61
127
|
|
|
62
|
-
params[:api_key] =
|
|
128
|
+
params[:api_key] = api_key
|
|
63
129
|
url = "#{Zerobounce::BaseRequest.__root_without_trailing_slashes__(root)}/#{path}"
|
|
130
|
+
Zerobounce::BaseRequest.__require_https__(url)
|
|
64
131
|
response = nil
|
|
65
132
|
|
|
66
133
|
if filepath or content_type == 'multipart/form-data'
|
|
134
|
+
params.each do |key, value|
|
|
135
|
+
params[key] = value ? 'true' : 'false' if value == true || value == false
|
|
136
|
+
end
|
|
67
137
|
params[:file] = File.new(Zerobounce::BaseRequest.__safe_file_path__(filepath), 'rb')
|
|
68
138
|
params[:multipart] = true
|
|
69
|
-
response =
|
|
139
|
+
response = __execute_request__(method: :post, url: url, payload: params, timeout: 120)
|
|
70
140
|
|
|
71
141
|
elsif content_type == 'application/json'
|
|
72
|
-
response =
|
|
73
|
-
|
|
142
|
+
response = __execute_request__(
|
|
143
|
+
method: :post,
|
|
144
|
+
url: url,
|
|
145
|
+
payload: params.to_json,
|
|
146
|
+
timeout: 120,
|
|
147
|
+
headers: { content_type: :json, accept: :json }
|
|
148
|
+
)
|
|
149
|
+
elsif content_type == 'application/x-www-form-urlencoded'
|
|
150
|
+
response = __execute_request__(method: :post, url: url, payload: params, timeout: 120)
|
|
74
151
|
else
|
|
75
152
|
# this shouldn't happen
|
|
76
153
|
raise Error.new('Unknown content type specified in request.'\
|
|
@@ -79,5 +156,128 @@ module Zerobounce
|
|
|
79
156
|
return response
|
|
80
157
|
end
|
|
81
158
|
|
|
159
|
+
# Sends a request with an explicit method and an optional form or JSON body.
|
|
160
|
+
#
|
|
161
|
+
# The endpoints ported from the Python SDK need shapes _get and _post cannot
|
|
162
|
+
# express: a form-encoded POST on an arbitrary base URL, and the Inbox
|
|
163
|
+
# Placement Tester's PUT and DELETE with the API key on the query string.
|
|
164
|
+
# +bearer+ sends the key in an Authorization header instead of a query
|
|
165
|
+
# parameter, which the Inbox Placement Tester requires and which keeps the
|
|
166
|
+
# key out of request logs.
|
|
167
|
+
def self.__execute_with_method__(method, url, params, form: nil, json: nil, bearer: false)
|
|
168
|
+
api_key = current_api_key
|
|
169
|
+
raise ('API key must be assigned') if api_key.nil? || api_key.to_s.strip.empty?
|
|
170
|
+
|
|
171
|
+
params = (params || {}).dup
|
|
172
|
+
extra = {}
|
|
173
|
+
if bearer
|
|
174
|
+
extra[:authorization] = "Bearer #{api_key}"
|
|
175
|
+
else
|
|
176
|
+
params = params.merge(api_key: api_key)
|
|
177
|
+
end
|
|
178
|
+
Zerobounce::BaseRequest.__require_https__(url)
|
|
179
|
+
|
|
180
|
+
options = { method: method, url: url, timeout: 120 }
|
|
181
|
+
if form
|
|
182
|
+
options[:payload] = form
|
|
183
|
+
options[:headers] = extra.merge(params: params, content_type: 'application/x-www-form-urlencoded')
|
|
184
|
+
elsif json
|
|
185
|
+
options[:payload] = json.to_json
|
|
186
|
+
options[:headers] = extra.merge(params: params, content_type: :json, accept: :json)
|
|
187
|
+
else
|
|
188
|
+
options[:headers] = extra.merge(params: params)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
__execute_request__(**options)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# A GET or form-encoded POST against an arbitrary base URL, for the list
|
|
195
|
+
# evaluator, which has its own host.
|
|
196
|
+
def self.root_request(method, root, path, params, form: nil)
|
|
197
|
+
url = "#{__root_without_trailing_slashes__(root)}/#{path.to_s.sub(%r{\A/}, '')}"
|
|
198
|
+
__parse_json_response__(__execute_with_method__(method, url, params, form: form))
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# One Inbox Placement Tester call.
|
|
202
|
+
def self.ipt_request(method, path, params = {}, json: nil)
|
|
203
|
+
root = Zerobounce.configuration.inbox_placement_root_url
|
|
204
|
+
url = "#{__root_without_trailing_slashes__(root)}/#{path.to_s.sub(%r{\A/}, '')}"
|
|
205
|
+
# LIVE: the Inbox Placement Tester authenticates with a Bearer header.
|
|
206
|
+
# Sent as an api_key parameter the request is rejected as unauthorized.
|
|
207
|
+
response = __execute_with_method__(method, url, params, json: json, bearer: true)
|
|
208
|
+
return nil if response.body.to_s.strip.empty?
|
|
209
|
+
|
|
210
|
+
__parse_json_response__(response)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Whether a Content-Type names a JSON body.
|
|
214
|
+
#
|
|
215
|
+
# The API types most responses 'application/json' but some
|
|
216
|
+
# 'application/json; charset=utf-8'. Compared with ==, the second kind
|
|
217
|
+
# skipped JSON parsing entirely, so an {"error": ...} payload came back as
|
|
218
|
+
# a raw String and read as a success. Only the media type decides; the
|
|
219
|
+
# parameters after the semicolon are ignored.
|
|
220
|
+
def self.__json_content_type__(content_type)
|
|
221
|
+
media_type = content_type.to_s.split(';', 2).first.to_s.strip.downcase
|
|
222
|
+
media_type == 'application/json' || media_type.end_with?('+json')
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# The root for the S3 and SFTP/FTP jobs: the bulkapi host WITHOUT /v2.
|
|
226
|
+
#
|
|
227
|
+
# +bulk_root_url+ (ZEROBOUNCE_BULK_ROOT_URL) wins when it is set, so these
|
|
228
|
+
# endpoints can be pointed at a mock server on their own. Left unset it is
|
|
229
|
+
# derived from +bulk_api_root_url+ with the /v2 stripped, so overriding
|
|
230
|
+
# that one alone still moves these endpoints with it.
|
|
231
|
+
def self.bulk_api_root_without_v2
|
|
232
|
+
configured = Zerobounce.configuration.bulk_root_url.to_s.strip
|
|
233
|
+
return __root_without_trailing_slashes__(configured) unless configured.empty?
|
|
234
|
+
|
|
235
|
+
derived = Zerobounce.configuration.bulk_api_root_url.to_s.sub(%r{/v2/?$}, '')
|
|
236
|
+
derived = ApiUrls::BULK_ROOT_URL if derived.strip.empty?
|
|
237
|
+
__root_without_trailing_slashes__(derived)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Whether a "success" field means failure. The API sends it as a JSON
|
|
241
|
+
# false, "false" or "False" depending on the endpoint.
|
|
242
|
+
def self.__success_means_failure__(value)
|
|
243
|
+
return !value if value == true || value == false
|
|
244
|
+
|
|
245
|
+
value.is_a?(String) && value.strip.casecmp('false').zero?
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# LIVE: {"success": false} on an HTTP 200 is a failure, not a result. The
|
|
249
|
+
# API answers an unknown bulk file id that way. Applied to the main API
|
|
250
|
+
# and the bulk API alike.
|
|
251
|
+
def self.__raise_if_success_false__(response, parsed)
|
|
252
|
+
return unless parsed.is_a?(Hash) && parsed.key?('success') && __success_means_failure__(parsed['success'])
|
|
253
|
+
|
|
254
|
+
raise ResponseError.message_from_parts(response.code, response.headers[:content_type].to_s, response.body.to_s)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def self.__parse_json_response__(response)
|
|
258
|
+
body = response.body.to_s
|
|
259
|
+
content_type = (response.headers[:content_type] || '').to_s
|
|
260
|
+
if ResponseError.html?(body, content_type) || body.strip.empty?
|
|
261
|
+
raise ResponseError.message_from_parts(response.code, content_type, body)
|
|
262
|
+
end
|
|
263
|
+
begin
|
|
264
|
+
parsed = JSON.parse(body)
|
|
265
|
+
rescue JSON::ParserError
|
|
266
|
+
raise 'ZeroBounce returned a response that is not valid JSON.'
|
|
267
|
+
end
|
|
268
|
+
return parsed unless parsed.is_a?(Hash)
|
|
269
|
+
|
|
270
|
+
__raise_if_success_false__(response, parsed)
|
|
271
|
+
|
|
272
|
+
raise ResponseError.strip_prefix(parsed['error']) if parsed.key?('error') && !parsed['error'].nil?
|
|
273
|
+
# The Inbox Placement Tester is a Django REST framework app and reports
|
|
274
|
+
# its errors under "detail" rather than "error"/"message".
|
|
275
|
+
raise ResponseError.strip_prefix(parsed['detail']) if parsed.key?('detail') && !parsed['detail'].nil?
|
|
276
|
+
if parsed.key?('errors') && parsed['errors'].is_a?(Array) && !parsed['errors'].empty?
|
|
277
|
+
raise ResponseError.strip_prefix(parsed['errors'][0]['error'])
|
|
278
|
+
end
|
|
279
|
+
parsed
|
|
280
|
+
end
|
|
281
|
+
|
|
82
282
|
end
|
|
83
283
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zerobounce
|
|
4
|
+
# One API key. Package methods share Zerobounce.config; a Client does not.
|
|
5
|
+
class Client
|
|
6
|
+
def initialize(apikey)
|
|
7
|
+
@apikey = apikey
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def validate(email, ip_address = nil, **options)
|
|
11
|
+
Zerobounce.with_api_key(@apikey) { Zerobounce.validate(email, ip_address, **options) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def credits
|
|
15
|
+
Zerobounce.with_api_key(@apikey) { Zerobounce.credits }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def validate_batch(emails, ip_addresses = [], **options)
|
|
19
|
+
Zerobounce.with_api_key(@apikey) { Zerobounce.validate_batch(emails, ip_addresses, **options) }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'rest-client'
|
|
4
|
-
require 'dotenv'
|
|
5
4
|
require_relative 'api_urls'
|
|
6
5
|
|
|
7
6
|
module Zerobounce
|
|
@@ -24,19 +23,41 @@ module Zerobounce
|
|
|
24
23
|
# @attr [String] bulk_api_root_url
|
|
25
24
|
# The Zerobounce bulk API root URL.
|
|
26
25
|
#
|
|
26
|
+
# @attr [String, nil] bulk_root_url
|
|
27
|
+
# The root for the S3 and SFTP/FTP bulk jobs: the bulkapi host without the
|
|
28
|
+
# /v2 path. Defaults to nil, which derives it from {#bulk_api_root_url}.
|
|
29
|
+
#
|
|
27
30
|
# @attr [Array<Symbol>] valid_statues
|
|
28
31
|
# The statuses that are considered valid by {Response#valid?}.
|
|
29
32
|
class Configuration
|
|
30
|
-
attr_accessor :headers, :apikey, :api_root_url, :bulk_api_root_url, :
|
|
33
|
+
attr_accessor :headers, :apikey, :api_root_url, :bulk_api_root_url, :bulk_root_url,
|
|
34
|
+
:list_evaluator_root_url, :inbox_placement_root_url, :valid_statuses, :mock
|
|
31
35
|
|
|
32
36
|
def initialize(mock = false)
|
|
33
|
-
File.file?('.env') ? Dotenv.load('.env') : Dotenv.load
|
|
34
37
|
self.apikey = ENV['ZEROBOUNCE_API_KEY']
|
|
35
38
|
self.api_root_url = ENV['ZEROBOUNCE_API_URL'] || ApiUrls::DEFAULT_URL
|
|
36
39
|
self.bulk_api_root_url = ENV['ZEROBOUNCE_BULK_API_URL'] || ApiUrls::BULK_DEFAULT_URL
|
|
40
|
+
# The S3/SFTP root is the only one that can be derived from another base
|
|
41
|
+
# URL, so it is left unset unless it is asked for: BaseRequest then takes
|
|
42
|
+
# bulk_api_root_url with the /v2 stripped. Set it - in code or through
|
|
43
|
+
# ZEROBOUNCE_BULK_ROOT_URL - to point those jobs somewhere on their own.
|
|
44
|
+
self.bulk_root_url = ENV['ZEROBOUNCE_BULK_ROOT_URL']
|
|
45
|
+
# These two are separate base URLs rather than paths built from
|
|
46
|
+
# bulk_api_root_url, so overriding that alone leaves them on the live API.
|
|
47
|
+
self.list_evaluator_root_url = ENV['ZEROBOUNCE_LIST_EVALUATOR_URL'] || ApiUrls::LIST_EVALUATOR_URL
|
|
48
|
+
self.inbox_placement_root_url = ENV['ZEROBOUNCE_INBOX_PLACEMENT_URL'] || ApiUrls::INBOX_PLACEMENT_URL
|
|
37
49
|
self.valid_statuses = %i[valid catch_all accept_all]
|
|
38
50
|
self.headers = { user_agent: "ZerobounceRubyGem/#{Zerobounce::VERSION}" }
|
|
39
51
|
self.mock = mock
|
|
40
52
|
end
|
|
53
|
+
|
|
54
|
+
# Keep the API key out of logs, consoles and error pages that call #inspect.
|
|
55
|
+
def inspect
|
|
56
|
+
vars = instance_variables.map do |var|
|
|
57
|
+
value = var == :@apikey && apikey ? '[FILTERED]' : instance_variable_get(var).inspect
|
|
58
|
+
"#{var}=#{value}"
|
|
59
|
+
end
|
|
60
|
+
"#<#{self.class.name} #{vars.join(', ')}>"
|
|
61
|
+
end
|
|
41
62
|
end
|
|
42
63
|
end
|
data/lib/zerobounce/error.rb
CHANGED
|
@@ -54,6 +54,24 @@ module Zerobounce
|
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# A failed API response.
|
|
58
|
+
#
|
|
59
|
+
# Carries the HTTP status so callers can tell a 403 (Cloudflare) from a
|
|
60
|
+
# 400/404 (API) or a 429 (the Warmup rate limit). The Warmup client raises
|
|
61
|
+
# it. The validation client raises the RestClient exception for an HTTP
|
|
62
|
+
# error status (status in #http_code), as it did up to 2.1.8, and a
|
|
63
|
+
# RuntimeError for an error reported with an HTTP 200; both carry the same
|
|
64
|
+
# short message.
|
|
65
|
+
class ApiResponseError < StandardError
|
|
66
|
+
# @return [Integer] the HTTP status of the failed response, or 0
|
|
67
|
+
attr_reader :status_code
|
|
68
|
+
|
|
69
|
+
def initialize(message, status_code = 0)
|
|
70
|
+
super(message)
|
|
71
|
+
@status_code = status_code.to_i
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
57
75
|
# Server returned a 500 error.
|
|
58
76
|
#
|
|
59
77
|
# @author Aaron Frase
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'stringio'
|
|
5
|
+
require 'zerobounce/response_error'
|
|
5
6
|
|
|
6
7
|
module Zerobounce
|
|
7
8
|
# Bulk getfile response handling (v2): HTTP errors, JSON error bodies (including HTTP 200).
|
|
@@ -69,13 +70,8 @@ module Zerobounce
|
|
|
69
70
|
body_str = response.body.to_s
|
|
70
71
|
ct = (response.headers[:content_type] || response.headers['Content-Type'] || '').to_s
|
|
71
72
|
|
|
72
|
-
if code > 299
|
|
73
|
-
|
|
74
|
-
format_error_message(body_str)
|
|
75
|
-
else
|
|
76
|
-
body_str.empty? ? "HTTP #{code}" : body_str
|
|
77
|
-
end
|
|
78
|
-
raise msg
|
|
73
|
+
if code > 299 || ResponseError.html?(body_str, ct)
|
|
74
|
+
raise ResponseError.message_from_parts(code, ct, body_str)
|
|
79
75
|
end
|
|
80
76
|
|
|
81
77
|
if should_treat_as_error?(body_str, ct)
|
|
@@ -10,7 +10,7 @@ module Zerobounce
|
|
|
10
10
|
|
|
11
11
|
def self.get(path, params, content_type='application/json')
|
|
12
12
|
response = self._get(Zerobounce.configuration.api_root_url, path, params, content_type)
|
|
13
|
-
if response.headers[:content_type]
|
|
13
|
+
if __json_content_type__(response.headers[:content_type])
|
|
14
14
|
response_body = response.body
|
|
15
15
|
response_body_json = JSON.parse(response_body)
|
|
16
16
|
|
|
@@ -27,9 +27,10 @@ module Zerobounce
|
|
|
27
27
|
|
|
28
28
|
def self.bulk_get(path, params, content_type='application/json')
|
|
29
29
|
response = self._get(Zerobounce.configuration.bulk_api_root_url, path, params, content_type)
|
|
30
|
-
if response.headers[:content_type]
|
|
30
|
+
if __json_content_type__(response.headers[:content_type])
|
|
31
31
|
response_body = response.body
|
|
32
32
|
response_body_json = JSON.parse(response_body)
|
|
33
|
+
return response_body_json unless response_body_json.is_a?(Hash)
|
|
33
34
|
|
|
34
35
|
raise (response_body_json['error']) if response_body_json.key?('error')
|
|
35
36
|
raise (response_body_json['errors'][0]['error']) \
|
|
@@ -50,7 +51,7 @@ module Zerobounce
|
|
|
50
51
|
def self.post(path, params, content_type='application/json', filepath=nil)
|
|
51
52
|
response = self._post(Zerobounce.configuration.api_root_url, path, params, \
|
|
52
53
|
content_type, filepath)
|
|
53
|
-
if response.headers[:content_type]
|
|
54
|
+
if __json_content_type__(response.headers[:content_type])
|
|
54
55
|
response_body = response.body
|
|
55
56
|
response_body_json = JSON.parse(response_body)
|
|
56
57
|
|
|
@@ -67,18 +68,36 @@ module Zerobounce
|
|
|
67
68
|
def self.bulk_post(path, params, content_type='application/json', filepath=nil)
|
|
68
69
|
response = self._post(Zerobounce.configuration.bulk_api_root_url, path, params, \
|
|
69
70
|
content_type, filepath)
|
|
70
|
-
if response.headers[:content_type]
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
if __json_content_type__(response.headers[:content_type])
|
|
72
|
+
return __parse_json_response__(response)
|
|
73
|
+
end
|
|
74
|
+
response.body
|
|
75
|
+
end
|
|
73
76
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
def self.bulk_root_get(path, params, content_type='application/json')
|
|
78
|
+
response = self._get(bulk_api_root_without_v2, path, params, content_type)
|
|
79
|
+
if __json_content_type__(response.headers[:content_type])
|
|
80
|
+
return __parse_json_response__(response)
|
|
81
|
+
end
|
|
82
|
+
response.body
|
|
83
|
+
end
|
|
78
84
|
|
|
79
|
-
|
|
85
|
+
def self.bulk_root_getfile(path, params)
|
|
86
|
+
response = self._get(bulk_api_root_without_v2, path, params, 'application/json')
|
|
87
|
+
GetFileHelper.process_getfile_response(response)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def self.bulk_root_post(path, params, content_type='application/json', filepath=nil)
|
|
91
|
+
response = self._post(bulk_api_root_without_v2, path, params, content_type, filepath)
|
|
92
|
+
if __json_content_type__(response.headers[:content_type])
|
|
93
|
+
return __parse_json_response__(response)
|
|
80
94
|
end
|
|
81
|
-
|
|
95
|
+
response.body
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.bulk_root_form_post(path, params)
|
|
99
|
+
response = self._post(bulk_api_root_without_v2, path, params, 'application/x-www-form-urlencoded')
|
|
100
|
+
__parse_json_response__(response)
|
|
82
101
|
end
|
|
83
102
|
|
|
84
103
|
end
|