# frozen_string_literal: truerequire'verbal_expressions'require'filesize'require'cgi'moduleJekyllmoduleAlgolia# Catch API errors and display messagesmoduleErrorHandlerincludeJekyll::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 displaydefself.stop(error,context={})Logger.verbose("E:[jekyll-algolia] Raw error: #{error}")identified_error=identify(error,context)ifidentified_error==falseLogger.log('E:[jekyll-algolia] Error:')Logger.log("E:#{error}")elseLogger.known_message(identified_error[:name],identified_error[:details])endexit1end# 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.defself.identify(error,context={})known_errors=%w[
unknown_application_id
invalid_credentials
record_too_big
unknown_settings
invalid_index_name
]# Checking the errors against our known listknown_errors.eachdo|potential_error|error_check=send("#{potential_error}?",error,context)nextiferror_check==falsereturn{name: potential_error,details: error_check}endfalseend# 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 matchersdefself.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.newdofind'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')doanything_but('/')endfind'/'capturedocapture('api_action'){word}maybe'?'capture('query_parameters')doanything_but(':')endendfind': 'capture('json')dofind'{'anything_but('}')find'}'endfind' ('capture('http_error'){word}find')'endmatches=regex.match(message)returnfalseunlessmatches# Convert matches to a hashhash={}matches.names.eachdo|name|hash[name]=matches[name]endhash['api_version']=hash['api_version'].to_ihash['http_error']=hash['http_error'].to_i# Merging the JSON key directly in the answerhash=hash.merge(JSON.parse(hash['json']))hash.delete('json')# Merging the query parameters in the answerCGI.parse(hash['query_parameters']).eachdo|key,values|hash[key]=values[0]endhash.delete('query_parameters')hashend# 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 specdefself.unknown_application_id?(error,_context={})message=error.messagereturnfalseifmessage!~/^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 errorsapp_id=matches[1].gsub(/-dsn$/,''){'application_id'=>app_id}end# Public: Check if the credentials are working## _context - Not used## Application ID and API key submitted don't match any credentials knowndefself.invalid_credentials?(error,_context={})details=error_hash(error.message)ifdetails['message']!='Invalid Application-ID or API key'returnfalseend{'application_id'=>details['application_id']}end# Public: Returns a string explaining which attributes are the largest in# the record## record - The record hash to analyze## This will be used on the `record_too_big` error, to guide users in# finding which record is causing troubledefself.readable_largest_record_keys(record)keys=Hash[record.map{|key,value|[key,value.to_s.length]}]largest_keys=keys.sort_by{|_,value|value}.reverse[0..2]output=[]largest_keys.eachdo|key,size|size=Filesize.from("#{size} B").to_s('Kb')output<<"#{key} (#{size})"endoutput.join(', ')end# Public: Check if the sent records are not too big## context[:records] - list of records sent in the batch## 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.defself.record_too_big?(error,context={})details=error_hash(error.message)message=details['message']returnfalseifmessage!~/^Record .* is too big .*/# Getting the record sizesize,=/.*size=(.*) bytes.*/.match(message).capturessize=Filesize.from("#{size} B").to_s('Kb')object_id=details['objectID']# Getting record detailsrecord=Utils.find_by_key(context[:records],:objectID,object_id)probable_wrong_keys=readable_largest_record_keys(record)# Writing the full record to disk for inspectionrecord_log_path=Logger.write_to_file("jekyll-algolia-record-too-big-#{object_id}.log",JSON.pretty_generate(record)){'object_id'=>object_id,'object_title'=>record[:title],'object_url'=>record[:url],'probable_wrong_keys'=>probable_wrong_keys,'record_log_path'=>record_log_path,'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.defself.unknown_settings?(error,context={})details=error_hash(error.message)message=details['message']returnfalseifmessage!~/^Invalid object attributes.*/# Getting the unknown setting nameregex=/^Invalid object attributes: (.*) near line.*/setting_name,=regex.match(message).capturessetting_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 namesdefself.invalid_index_name?(error,_context={})details=error_hash(error.message)message=details['message']returnfalseifmessage!~/^indexName is not valid.*/{'index_name'=>Configurator.index_name}endendendend