859a147c49
Currently, when a query can't be parsed, the error is: - logged to Sentry (which is useless to us), - returned as a generic 'Internal Server Error' (which is useless to the user who made the query). With this commit, the error is instead ignored from our logs (because it is a user error), but the parse error details are returned to the user, with the following format: > {'errors': [{'message': 'Parse error on ")" (RPAREN) at [3, 23]'}]}
130 lines
3.1 KiB
Ruby
130 lines
3.1 KiB
Ruby
class API::V2::GraphqlController < API::V2::BaseController
|
|
def execute
|
|
variables = ensure_hash(params[:variables])
|
|
|
|
result = API::V2::Schema.execute(params[:query],
|
|
variables: variables,
|
|
context: context,
|
|
operation_name: params[:operationName])
|
|
|
|
render json: result
|
|
rescue GraphQL::ParseError => exception
|
|
handle_parse_error(exception)
|
|
rescue => exception
|
|
if Rails.env.production?
|
|
handle_error_in_production(exception)
|
|
else
|
|
handle_error_in_development(exception)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def append_info_to_payload(payload)
|
|
super
|
|
|
|
payload.merge!({
|
|
graphql_operation: operation_log(params[:query], params[:operationName], params[:variables])
|
|
})
|
|
end
|
|
|
|
def operation_log(query, operation_name, variables)
|
|
return "NoQuery" if query.nil?
|
|
|
|
operation = GraphQL.parse(query).children.find do |node|
|
|
if node.is_a?(GraphQL::Language::Nodes::OperationDefinition)
|
|
node.name == operation_name
|
|
end
|
|
end
|
|
|
|
return "InvalidQuery" if operation.nil?
|
|
return "IntrospectionQuery" if operation.name == "IntrospectionQuery"
|
|
|
|
message = operation.operation_type
|
|
if operation.name
|
|
message += ": #{operation.name} { "
|
|
end
|
|
message += operation.selections.map(&:name).join(', ')
|
|
message += " }"
|
|
if variables.present?
|
|
message += " "
|
|
message += variables.to_unsafe_h.flat_map do |(name, value)|
|
|
if name == "input"
|
|
value.map do |(name, value)|
|
|
"#{name}: \"#{value.to_s.truncate(10)}\""
|
|
end
|
|
else
|
|
"#{name}: \"#{value.to_s.truncate(10)}\""
|
|
end
|
|
end.join(', ')
|
|
end
|
|
message
|
|
end
|
|
|
|
def process_action(*args)
|
|
super
|
|
rescue ActionDispatch::Http::Parameters::ParseError => exception
|
|
render json: {
|
|
errors: [
|
|
{ message: exception.cause.message }
|
|
],
|
|
data: nil
|
|
}, status: 400
|
|
end
|
|
|
|
# Handle form data, JSON body, or a blank value
|
|
def ensure_hash(ambiguous_param)
|
|
case ambiguous_param
|
|
when String
|
|
if ambiguous_param.present?
|
|
ensure_hash(JSON.parse(ambiguous_param))
|
|
else
|
|
{}
|
|
end
|
|
when Hash, ActionController::Parameters
|
|
ambiguous_param
|
|
when nil
|
|
{}
|
|
else
|
|
raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
|
|
end
|
|
end
|
|
|
|
def handle_parse_error(exception)
|
|
render json: {
|
|
errors: [
|
|
{ message: exception.message }
|
|
],
|
|
data: nil
|
|
}, status: 400
|
|
end
|
|
|
|
def handle_error_in_development(exception)
|
|
logger.error exception.message
|
|
logger.error exception.backtrace.join("\n")
|
|
|
|
render json: {
|
|
errors: [
|
|
{ message: exception.message, backtrace: exception.backtrace }
|
|
],
|
|
data: nil
|
|
}, status: 500
|
|
end
|
|
|
|
def handle_error_in_production(exception)
|
|
id = SecureRandom.uuid
|
|
Sentry.capture_exception(exception, extra: { exception_id: id })
|
|
|
|
render json: {
|
|
errors: [
|
|
{
|
|
message: "Internal Server Error",
|
|
extensions: {
|
|
exception: { id: id }
|
|
}
|
|
}
|
|
],
|
|
data: nil
|
|
}, status: 500
|
|
end
|
|
end
|