Skip to content

Commit e48e02f

Browse files
authored
Merge pull request #16 from serpapi/enhance-error-reporting
Enhance error reporting
2 parents 26af432 + 0acdd33 commit e48e02f

File tree

7 files changed

+158
-51
lines changed

7 files changed

+158
-51
lines changed

.rubocop.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ Style/FetchEnvVar:
4444
Metrics/CyclomaticComplexity:
4545
Max: 12
4646

47+
# There is a tradeoff between line length and line count.
48+
Metrics/ClassLength:
49+
Max: 140
50+
51+
# Keyword args are readable.
52+
Metrics/ParameterLists:
53+
CountKeywordArgs: false
54+
4755
# this rule doesn't always work well with Ruby
4856
Layout/FirstHashElementIndentation:
4957
Enabled: false
@@ -59,4 +67,4 @@ AllCops:
5967
- 'spec/**/*_spec.rb'
6068
- 'spec/spec_helper.rb'
6169
- 'Gemfile'
62-
- 'Rakefile'
70+
- 'Rakefile'

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Other versions, such as Ruby 1.9, Ruby 2.x, and JRuby, are compatible with [lega
2222

2323
### Bundler
2424
```ruby
25-
gem 'serpapi', '~> 1.0', '>= 1.0.2'
25+
gem 'serpapi', '~> 1.0', '>= 1.0.3'
2626
```
2727

2828
### Gem

README.md.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Other versions, such as Ruby 1.9, Ruby 2.x, and JRuby, are compatible with [lega
4242

4343
### Bundler
4444
```ruby
45-
gem 'serpapi', '~> 1.0', '>= 1.0.2'
45+
gem 'serpapi', '~> 1.0', '>= 1.0.3'
4646
```
4747

4848
### Gem

lib/serpapi/client.rb

Lines changed: 67 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -215,47 +215,79 @@ def persistent?
215215
# @param [Hash] params custom search inputs
216216
# @return [String|Hash] raw HTML or decoded response as JSON / Hash
217217
def get(endpoint, decoder = :json, params = {})
218-
# execute get via open socket
219-
response = if persistent?
220-
@socket.get(endpoint, params: query(params))
221-
else
222-
HTTP.timeout(timeout).get("https://#{BACKEND}#{endpoint}", params: query(params))
223-
end
224-
225-
# decode response using JSON native parser
218+
response = execute_request(endpoint, params)
219+
handle_response(response, decoder, endpoint, params)
220+
end
221+
222+
def execute_request(endpoint, params)
223+
if persistent?
224+
@socket.get(endpoint, params: query(params))
225+
else
226+
url = "https://#{BACKEND}#{endpoint}"
227+
HTTP.timeout(timeout).get(url, params: query(params))
228+
end
229+
end
230+
231+
def handle_response(response, decoder, endpoint, params)
226232
case decoder
227233
when :json
228-
# read http response
229-
begin
230-
# user can turn on/off JSON keys to symbols
231-
# this is more memory efficient, but not always needed
232-
symbolize_names = params.key?(:symbolize_names) ? params[:symbolize_names] : true
233-
234-
# parse JSON response with Ruby standard library
235-
data = JSON.parse(response.body, symbolize_names: symbolize_names)
236-
if data.instance_of?(Hash) && data.key?(:error)
237-
raise SerpApiError, "HTTP request failed with error: #{data[:error]} from url: https://#{BACKEND}#{endpoint}, params: #{params}, decoder: #{decoder}, response status: #{response.status} "
238-
elsif response.status != 200
239-
raise SerpApiError, "HTTP request failed with response status: #{response.status} reponse: #{data} on get url: https://#{BACKEND}#{endpoint}, params: #{params}, decoder: #{decoder}"
240-
end
241-
rescue JSON::ParserError
242-
raise SerpApiError, "JSON parse error: #{response.body} on get url: https://#{BACKEND}#{endpoint}, params: #{params}, decoder: #{decoder}, response status: #{response.status}"
243-
end
244-
245-
# discard response body
246-
response.flush if persistent?
247-
248-
data
234+
process_json_response(response, endpoint, params)
249235
when :html
250-
# html decoder
251-
if response.status != 200
252-
raise SerpApiError, "HTTP request failed with response status: #{response.status} reponse: #{data} on get url: https://#{BACKEND}#{endpoint}, params: #{params}, decoder: #{decoder}"
253-
end
254-
255-
response.body
236+
process_html_response(response, endpoint, params)
256237
else
257238
raise SerpApiError, "not supported decoder: #{decoder}, available: :json, :html"
258239
end
259240
end
241+
242+
def process_json_response(response, endpoint, params)
243+
symbolize = params.fetch(:symbolize_names, true)
244+
245+
begin
246+
data = JSON.parse(response.body, symbolize_names: symbolize)
247+
validate_json_content!(data, response, endpoint, params)
248+
rescue JSON::ParserError
249+
raise_parser_error(response, endpoint, params)
250+
end
251+
252+
response.flush if persistent?
253+
data
254+
end
255+
256+
def process_html_response(response, endpoint, params)
257+
raise_http_error(response, nil, endpoint, params, decoder: :html) if response.status != 200
258+
response.body
259+
end
260+
261+
def validate_json_content!(data, response, endpoint, params)
262+
if data.is_a?(Hash) && data.key?(:error)
263+
raise_http_error(response, data, endpoint, params, explicit_error: data[:error])
264+
elsif response.status != 200
265+
raise_http_error(response, data, endpoint, params)
266+
end
267+
end
268+
269+
# Centralized error raising to clean up the logic methods
270+
def raise_http_error(response, data, endpoint, params, explicit_error: nil, decoder: :json)
271+
msg = "HTTP request failed with status: #{response.status}"
272+
msg += " error: #{explicit_error}" if explicit_error
273+
274+
raise SerpApiError.new(
275+
"#{msg} from url: https://#{BACKEND}#{endpoint}",
276+
serpapi_error: explicit_error || (data.is_a?(Hash) ? data[:error] : nil),
277+
search_params: params,
278+
response_status: response.status,
279+
search_id: data.is_a?(Hash) ? data&.dig(:search_metadata, :id) : nil,
280+
decoder: decoder
281+
)
282+
end
283+
284+
def raise_parser_error(response, endpoint, params)
285+
raise SerpApiError.new(
286+
"JSON parse error: #{response.body} on get url: https://#{BACKEND}#{endpoint}",
287+
search_params: params,
288+
response_status: response.status,
289+
decoder: :json
290+
)
291+
end
260292
end
261293
end

lib/serpapi/error.rb

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,81 @@
22
# frozen_string_literal: true
33

44
module SerpApi
5-
# SerpApiError wraps any errors related to the SerpApi client.
5+
# Custom error class for SerpApi-related errors.
6+
# Inherits from StandardError.
7+
# Includes optional attributes for detailed error context.
8+
# Attributes:
9+
# - serpapi_error: String error message from SerpApi (optional)
10+
# - search_params: Hash of search parameters used (optional)
11+
# - response_status: Integer HTTP or response status code (optional)
12+
# - search_id: String id returned by the service for the search (optional)
13+
# - decoder: Symbol representing the decoder/format used (optional) (e.g. :json)
614
class SerpApiError < StandardError
7-
# List the specific types of errors handled by the Error class.
8-
# - HTTP response errors from SerpApi.com
9-
# - Missing API key
10-
# - Credit limit
11-
# - Incorrect query
12-
# - more ...
15+
attr_reader :serpapi_error, :search_params, :response_status, :search_id, :decoder
16+
17+
# All attributes are optional keyword arguments.
18+
#
19+
# @param message [String, nil] an optional human message passed to StandardError
20+
# @param serpapi_error [String, nil] optional error string coming from SerpAPI
21+
# @param search_params [Hash, nil] optional hash of the search parameters used
22+
# @param response_status [Integer, nil] optional HTTP or response status code
23+
# @param search_id [String, nil] optional id returned by the service for the search
24+
# @param decoder [Symbol, nil] optional decoder/format used (e.g. :json)
25+
def initialize(message = nil,
26+
serpapi_error: nil,
27+
search_params: nil,
28+
response_status: nil,
29+
search_id: nil,
30+
decoder: nil)
31+
super(message)
32+
33+
@serpapi_error = validate_optional_string(serpapi_error, :serpapi_error)
34+
@search_params = search_params.dup
35+
@response_status = validate_optional_integer(response_status, :response_status)
36+
@search_id = validate_optional_string(search_id, :search_id)
37+
@decoder = validate_optional_symbol(decoder, :decoder)
38+
end
39+
40+
# Return a compact hash representation (omits nil values).
41+
#
42+
# @return [Hash]
43+
def to_h
44+
{
45+
message: message,
46+
serpapi_error: serpapi_error,
47+
search_params: search_params.dup,
48+
response_status: response_status,
49+
search_id: search_id,
50+
decoder: decoder
51+
}.compact
52+
end
53+
54+
private
55+
56+
def validate_optional_string(value, name = nil)
57+
return nil if value.nil?
58+
raise TypeError, "expected #{name || 'value'} to be a String, got #{value.class}" unless value.is_a?(String)
59+
60+
value.freeze
61+
end
62+
63+
def validate_optional_integer(value, name = nil)
64+
return nil if value.nil?
65+
return value if value.is_a?(Integer)
66+
67+
# Accept numeric-like strings (e.g. "200") by converting; fail otherwise.
68+
begin
69+
Integer(value)
70+
rescue ArgumentError, TypeError
71+
raise TypeError, "expected #{name || 'value'} to be an Integer (or integer-like), got #{value.inspect}"
72+
end
73+
end
74+
75+
def validate_optional_symbol(value, name = nil)
76+
return nil if value.nil?
77+
raise TypeError, "expected #{name || 'value'} to be a Symbol, got #{value.class}" unless value.is_a?(Symbol)
78+
79+
value
80+
end
1381
end
1482
end

lib/serpapi/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
module SerpApi
22
# Current version of the gem
3-
VERSION = '1.0.2'.freeze
3+
VERSION = '1.0.3'.freeze
44
end

spec/serpapi/client/client_spec.rb

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@
7171
end
7272

7373
it 'get endpoint error' do
74-
expect {
75-
client.send(:get, '/search', :json, {})
76-
}.to raise_error(SerpApi::SerpApiError).with_message(/HTTP request failed with error: Missing query `q` parameter./)
74+
expect {
75+
client.send(:get, '/search', :json, {})
76+
}.to raise_error(SerpApi::SerpApiError).with_message(/HTTP request failed with status: 400.* error: Missing query `q` parameter./)
7777
end
7878

7979
it 'get bad endpoint' do
@@ -90,7 +90,7 @@
9090
begin
9191
client.send(:get, '/invalid', :html, {})
9292
rescue SerpApi::SerpApiError => e
93-
expect(e.message).to include(/HTTP request failed with response status: 404 Not Found reponse/), "got #{e.message}"
93+
expect(e.message).to include("HTTP request failed with status: 404")
9494
rescue => e
9595
raise("wrong exception: #{e}")
9696
end
@@ -133,7 +133,6 @@
133133
allow(client).to receive(:search).and_raise(SerpApi::SerpApiError)
134134
expect { client.search(q: 'Invalid Query') }.to raise_error(SerpApi::SerpApiError)
135135
end
136-
137136
end
138137

139138
describe 'SerpApi client with persitency disabled' do

0 commit comments

Comments
 (0)