Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support problem+json for error messages #583

Merged
merged 5 commits into from
Dec 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Formats a message string into application/problem+json format.

module PactBroker
module Api
module Decorators
class CustomErrorProblemJSONDecorator

# @option title [String]
# @option type [String]
# @option detail [String]
# @option status [Integer] HTTP status code
def initialize(title:, type:, detail:, status: )
@title = title
@type = type
@detail = detail
@status = status
end

# @return [Hash]
def to_hash(decorator_options = {})
{
"title" => @title,
"type" => "#{decorator_options.dig(:user_options, :base_url)}/problem/#{@type}",
"detail" => @detail,
"status" => @status
}
end

# @return [String] JSON
def to_json(decorator_options = {})
to_hash(decorator_options).to_json
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Formats a message string into application/problem+json format.

module PactBroker
module Api
module Decorators
class RuntimeErrorProblemJSONDecorator

# @param message [String]
def initialize(message)
@message = message
end

# @return [Hash]
def to_hash(decorator_options = {})
{
"title" => "Server error",
"type" => "#{decorator_options.dig(:user_options, :base_url)}/problems/server_error",
"detail" => message,
"status" => 500
}
end

# @return [String] JSON
def to_json(decorator_options = {})
to_hash(decorator_options).to_json
end

private

attr_reader :message
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Formats a nested Hash of errors as it comes out of the Dry Validation library
# into application/problem+json format.

module PactBroker
module Api
module Decorators
class ValidationErrorsProblemJSONDecorator

# @param errors [Hash]
def initialize(errors)
@errors = errors
end

# @return [Hash]
def to_hash(decorator_options = {})
error_list = []
walk_errors(errors, error_list, "", decorator_options.dig(:user_options, :base_url))
{
"title" => "Validation errors",
"type" => "#{decorator_options.dig(:user_options, :base_url)}/problems/validation-error",
"status" => 400,
"errors" => error_list
}
end

# @return [String] JSON
def to_json(decorator_options = {})
to_hash(decorator_options).to_json
end

private

attr_reader :errors

def walk_errors(object, list, path, base_url)
if object.is_a?(Hash)
object.each { | key, value | walk_errors(value, list, "#{path}/#{key}", base_url) }
elsif object.is_a?(Array)
object.each { | value | walk_errors(value, list, path, base_url) }
elsif object.is_a?(String)
append_error(list, object, path, base_url)
end
end

def append_error(list, message, path, base_url)
list << {
"type" => "#{base_url}/problems/invalid-body-property-value",
"title" => "Validation error",
"detail" => message,
"instance" => path,
"status" => 400
}
end
end
end
end
end
2 changes: 1 addition & 1 deletion lib/pact_broker/api/resources/badge_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def moved_temporarily?
badge_url
rescue StandardError => e
# Want to render a badge, even if there's an error
badge_service.error_badge_url("error", ErrorResponseBodyGenerator.display_message(e, "reference: #{PactBroker::Errors.generate_error_reference}"))
badge_service.error_badge_url("error", ErrorResponseGenerator.display_message(e, "reference: #{PactBroker::Errors.generate_error_reference}"))
end
end

Expand Down
29 changes: 7 additions & 22 deletions lib/pact_broker/api/resources/base_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
require "pact_broker/api/resources/authentication"
require "pact_broker/api/resources/authorization"
require "pact_broker/errors"
require "pact_broker/api/resources/error_handling_methods"

module PactBroker
module Api
Expand All @@ -22,6 +23,7 @@ class BaseResource < Webmachine::Resource
include PactBroker::Api::PactBrokerUrls
include PactBroker::Api::Resources::Authentication
include PactBroker::Api::Resources::Authorization
include PactBroker::Api::Resources::ErrorHandlingMethods

include PactBroker::Logging

Expand Down Expand Up @@ -111,15 +113,6 @@ def decorator_options options = {}
{ user_options: decorator_context(options) }
end

def handle_exception(error)
error_reference = PactBroker::Errors.generate_error_reference
application_context.error_logger.call(error, error_reference, request.env)
if PactBroker::Errors.reportable_error?(error)
PactBroker::Errors.report(error, error_reference, request.env)
end
response.body = application_context.error_response_body_generator.call(error, error_reference, request.env)
end

# rubocop: disable Metrics/CyclomaticComplexity
def params(options = {})
return options[:default] if options.key?(:default) && request_body.empty?
Expand Down Expand Up @@ -158,16 +151,6 @@ def pact_params
@pact_params ||= PactBroker::Pacts::PactParams.from_request(request, identifier_from_path)
end

def set_json_error_message message
response.headers["Content-Type"] = "application/hal+json;charset=utf-8"
response.body = { error: message }.to_json
end

def set_json_validation_error_messages errors
response.headers["Content-Type"] = "application/hal+json;charset=utf-8"
response.body = { errors: errors }.to_json
end

def request_body
@request_body ||= request.body.to_s
end
Expand Down Expand Up @@ -215,13 +198,13 @@ def invalid_json?
rescue NonUTF8CharacterFound => e
logger.info(e.message) # Don't use the default SemanticLogger error logging method because it will try and print out the cause which will contain non UTF-8 chars in the message
set_json_error_message(e.message)
response.headers["Content-Type"] = "application/hal+json;charset=utf-8"
response.headers["Content-Type"] = error_response_content_type
true
rescue StandardError => e
message = "#{e.cause ? e.cause.class.name : e.class.name} - #{e.message}"
logger.info(message)
set_json_error_message(message)
response.headers["Content-Type"] = "application/hal+json;charset=utf-8"
response.headers["Content-Type"] = error_response_content_type
true
end
end
Expand All @@ -244,7 +227,9 @@ def contract_validation_errors? contract, params

def find_pacticipant name, role
pacticipant_service.find_pacticipant_by_name(name).tap do | pacticipant |
set_json_error_message("No #{role} with name '#{name}' found") if pacticipant.nil?
if pacticipant.nil?
set_json_error_message("No #{role} with name '#{name}' found", title: "Not found", type: "not_found", status: 404)
end
end
end

Expand Down
57 changes: 57 additions & 0 deletions lib/pact_broker/api/resources/error_handling_methods.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
require "pact_broker/api/decorators/validation_errors_problem_json_decorator"
require "pact_broker/api/decorators/custom_error_problem_json_decorator"

module PactBroker
module Api
module Resources
module ErrorHandlingMethods

# @override
def handle_exception(error)
error_reference = PactBroker::Errors.generate_error_reference
application_context.error_logger.call(error, error_reference, request.env)
if PactBroker::Errors.reportable_error?(error)
PactBroker::Errors.report(error, error_reference, request.env)
end
headers, body = application_context.error_response_generator.call(error, error_reference, request.env)
headers.each { | key, value | response.headers[key] = value }
response.body = body
end

def set_json_error_message detail, title: "Server error", type: "server_error", status: 500
response.headers["Content-Type"] = error_response_content_type
response.body = error_response_body(detail, title, type, status)
end

def set_json_validation_error_messages errors
response.headers["Content-Type"] = error_response_content_type
if problem_json_error_content_type?
response.body = PactBroker::Api::Decorators::ValidationErrorsProblemJSONDecorator.new(errors).to_json(decorator_options)
else
response.body = { errors: errors }.to_json
end
end

def error_response_content_type
if problem_json_error_content_type?
"application/problem+json;charset=utf-8"
else
"application/hal+json;charset=utf-8"
end
end

def error_response_body(detail, title, type, status)
if problem_json_error_content_type?
PactBroker::Api::Decorators::CustomErrorProblemJSONDecorator.new(detail: detail, title: title, type: type, status: status).to_json(decorator_options)
else
{ error: detail }.to_json
end
end

def problem_json_error_content_type?
request.headers["Accept"]&.include?("application/problem+json")
end
end
end
end
end
41 changes: 0 additions & 41 deletions lib/pact_broker/api/resources/error_response_body_generator.rb

This file was deleted.

70 changes: 70 additions & 0 deletions lib/pact_broker/api/resources/error_response_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
require "pact_broker/configuration"
require "pact_broker/api/decorators/runtime_error_problem_json_decorator"

module PactBroker
module Api
module Resources
class ErrorResponseGenerator
include PactBroker::Logging

# @param error [StandardError]
# @param error_reference [String] an error reference to display to the user
# @param env [Hash] the rack env
# @return [Hash, String] the response headers to set, the response body to set
def self.call error, error_reference, env = {}
body = response_body_hash(error, error_reference, env, display_message(error, obfuscated_error_message(error_reference)))
return headers(env), body.to_json
end

def self.display_message(error, obfuscated_message)
if PactBroker.configuration.show_backtrace_in_error_response?
error.message || obfuscated_message
else
PactBroker::Errors.reportable_error?(error) ? obfuscated_message : error.message
end
end

private_class_method def self.response_body_hash(error, error_reference, env, message)
if problem_json?(env)
problem_json_response_body(message, env)
else
hal_json_response_body(error, error_reference, message)
end
end

private_class_method def self.hal_json_response_body(error, error_reference, message)
response_body = {
error: {
message: message,
reference: error_reference
}
}
if PactBroker.configuration.show_backtrace_in_error_response?
response_body[:error][:backtrace] = error.backtrace
end
response_body
end

private_class_method def self.problem_json_response_body(message, env)
PactBroker::Api::Decorators::RuntimeErrorProblemJSONDecorator.new(message).to_hash(user_options: { base_url: env["pactbroker.base_url" ] })
end

private_class_method def self.obfuscated_error_message(error_reference)
"An error has occurred. The details have been logged with the reference #{error_reference}"
end

private_class_method def self.headers(env)
if problem_json?(env)
{ "Content-Type" => "application/problem+json;charset=utf-8" }
else
{ "Content-Type" => "application/hal+json;charset=utf-8" }
end
end

private_class_method def self.problem_json?(env)
env["HTTP_ACCEPT"]&.include?("application/problem+json")
end
end
end
end
end
2 changes: 1 addition & 1 deletion lib/pact_broker/api/resources/pact_version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def allowed_methods
["GET", "OPTIONS"]
end

def decorator_options(options)
def decorator_options(options = {})
super(options.merge(consumer_versions: consumer_versions_from_metadata&.reverse))
end
end
Expand Down
Loading