jekyll-algolia/lib/jekyll/algolia/error_handler.rb

271 lines
8.4 KiB
Ruby

# frozen_string_literal: true
require 'verbal_expressions'
require 'filesize'
require 'cgi'
module Jekyll
module Algolia
# Catch API errors and display messages
module ErrorHandler
include Jekyll::Algolia
# Public: Stop the execution of the plugin and display if possible
# a human-readable error message
#
# error - The caught error
# context - A hash of values that will be passed from where the error
# happened to the display
def self.stop(error, context = {})
Logger.verbose("E:[jekyll-algolia] Raw error: #{error}")
Logger.verbose("E:[jekyll-algolia] Context: #{context}")
identified_error = identify(error, context)
if identified_error == false
Logger.log('E:[jekyll-algolia] Error:')
Logger.log("E:#{error}")
else
Logger.known_message(
identified_error[:name],
identified_error[:details]
)
end
exit 1
end
# Public: Will identify the error and return its internal name
#
# error - The caught error
# context - A hash of additional information that can be passed from the
# code intercepting the user
#
# It will parse in order all potential known issues until it finds one
# that matches. Returns false if no match, or a hash of :name and :details
# further identifying the issue.
def self.identify(error, context = {})
known_errors = %w[
unknown_application_id
invalid_credentials_for_tmp_index
invalid_credentials
record_too_big
unknown_settings
invalid_index_name
]
# Checking the errors against our known list
known_errors.each do |potential_error|
error_check = send("#{potential_error}?", error, context)
next if error_check == false
return {
name: potential_error,
details: error_check
}
end
false
end
# Public: Parses an Algolia error message into a hash of its content
#
# message - The raw message as returned by the API
#
# Returns a hash of all parts of the message, to be more easily consumed
# by our error matchers
def self.error_hash(message)
message = message.delete("\n")
# Ex: Cannot PUT to https://appid.algolia.net/1/indexes/index_name/settings:
# {"message":"Invalid Application-ID or API key","status":403} (403)
regex = VerEx.new do
find 'Cannot '
capture('verb') { word }
find ' to '
capture('scheme') { word }
find '://'
capture('application_id') { word }
anything_but '/'
find '/'
capture('api_version') { digit }
find '/'
capture('api_section') { word }
find '/'
capture('index_name') do
anything_but('/')
end
find '/'
capture do
capture('api_action') { word }
maybe '?'
capture('query_parameters') do
anything_but(':')
end
end
find ': '
capture('json') do
find '{'
anything_but('}')
find '}'
end
find ' ('
capture('http_error') { word }
find ')'
end
matches = regex.match(message)
return false unless matches
# Convert matches to a hash
hash = {}
matches.names.each do |name|
hash[name] = matches[name]
end
hash['api_version'] = hash['api_version'].to_i
hash['http_error'] = hash['http_error'].to_i
# Merging the JSON key directly in the answer
hash = hash.merge(JSON.parse(hash['json']))
hash.delete('json')
# Merging the query parameters in the answer
CGI.parse(hash['query_parameters']).each do |key, values|
hash[key] = values[0]
end
hash.delete('query_parameters')
hash
end
# Public: Check if the application id is available
#
# _context - Not used
#
# If the call to the cluster fails, chances are that the application ID
# is invalid. As we cannot actually contact the server, the error is raw
# and does not follow our error spec
def self.unknown_application_id?(error, _context = {})
message = error.message
return false if message !~ /^Cannot reach any host/
matches = /.*\((.*)\.algolia.net.*/.match(message)
# The API will browse on APP_ID-dsn, but push/delete on APP_ID only
# We need to catch both potential errors
app_id = matches[1].gsub(/-dsn$/, '')
{ 'application_id' => app_id }
end
# Public: Check if credentials specifically can't access the _tmp index
#
# _context - Not used
#
# If the error happens on a _tmp folder, it might mean that the key does
# not have access to the _tmp indices and the error message will reflect
# that.
def self.invalid_credentials_for_tmp_index?(error, _context = {})
details = error_hash(error.message)
index_name_tmp = details['index_name']
if details['message'] != 'Index not allowed with this API key' ||
index_name_tmp !~ /_tmp$/
return false
end
{
'application_id' => Configurator.application_id,
'index_name' => Configurator.index_name,
'index_name_tmp' => index_name_tmp
}
end
# Public: Check if the credentials are working
#
# _context - Not used
#
# Application ID and API key submitted don't match any credentials known
def self.invalid_credentials?(error, _context = {})
details = error_hash(error.message)
if details['message'] != 'Invalid Application-ID or API key'
return false
end
{
'application_id' => details['application_id']
}
end
# Public: Check if the sent records are not too big
#
# context[:records] - list of records to push
#
# Records cannot weight more that 10Kb. If we're getting this error it
# means that one of the records is too big, so we'll try to give
# informations about it so the user can debug it.
def self.record_too_big?(error, context = {})
details = error_hash(error.message)
message = details['message']
return false if message !~ /^Record .* is too big .*/
# Getting the record size
size, = /.*size=(.*) bytes.*/.match(message).captures
size = Filesize.from("#{size} B").pretty
object_id = details['objectID']
# Getting record details
record = Utils.find_by_key(context[:records], :objectID, object_id)
{
'object_id' => object_id,
'object_title' => record[:title],
'object_url' => record[:url],
'object_hint' => record[:content][0..100],
'nodes_to_index' => Configurator.algolia('nodes_to_index'),
'size' => size,
'size_limit' => '10 Kb'
}
end
# Public: Check if one of the index settings is invalid
#
# context[:settings] - The settings passed to update the index
#
# The API will block any call that tries to update a setting value that is
# not available. We'll tell the user which one so they can fix their
# issue.
def self.unknown_settings?(error, context = {})
details = error_hash(error.message)
message = details['message']
return false if message !~ /^Invalid object attributes.*/
# Getting the unknown setting name
regex = /^Invalid object attributes: (.*) near line.*/
setting_name, = regex.match(message).captures
setting_value = context[:settings][setting_name]
{
'setting_name' => setting_name,
'setting_value' => setting_value
}
end
# Public: Check if the index name is invalid
#
# Some characters are forbidden in index names
def self.invalid_index_name?(error, _context = {})
details = error_hash(error.message)
message = details['message']
return false if message !~ /^indexName is not valid.*/
{
'index_name' => Configurator.index_name
}
end
end
end
end