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

Added new exception page #864

Closed
wants to merge 9 commits into from
Closed
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
2 changes: 1 addition & 1 deletion src/amber/cli/templates/error.cr
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ module Amber::CLI
end

private def add_plugs
add_plugs :web, "plug Amber::Pipe::Error.new"
add_plugs :web, "plug #{class_name}.new"
end

private def add_dependencies
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ class <%= class_name %>ControllerTest < GarnetSpec::Controller::Test
def initialize
@handler = Amber::Pipe::Pipeline.new
@handler.build :web do
plug Amber::Pipe::Error.new
plug <%= class_name %>.new
end
@handler.build :static do
plug Amber::Pipe::Error.new
plug <%= class_name %>.new
end
@handler.prepare_pipelines
end
Expand Down
46 changes: 23 additions & 23 deletions src/amber/cli/templates/error/src/pipes/error.cr.ecr
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
module Amber
module Pipe
# The Error pipe catches RouteNotFound and returns a 404. It responds based
# on the `Accepts` header as JSON or HTML. It also catches any runtime
# Exceptions and returns a backtrace in text/html format.
class Error < Base
def call(context : HTTP::Server::Context)
raise Amber::Exceptions::RouteNotFound.new(context.request) unless context.valid_route?
call_next(context)
rescue ex : Amber::Exceptions::Forbidden
context.response.status_code = 403
error = <%= class_name %>Controller.new(context, ex)
context.response.print(error.forbidden)
rescue ex : Amber::Exceptions::RouteNotFound
context.response.status_code = 404
error = <%= class_name %>Controller.new(context, ex)
context.response.print(error.not_found)
rescue ex : Exception
context.response.status_code = 500
error = <%= class_name %>Controller.new(context, ex)
context.response.print(error.internal_server_error)
end
end
class <%= class_name %> < Amber::Pipe::Error
def error(context, ex : ValidationFailed | InvalidParam)
context.response.status_code = 400
action = <%= class_name %>Controller.new(context, ex)
context.response.print(action.bad_request)
end

def error(context, ex : Forbidden)
context.response.status_code = 403
action = <%= class_name %>Controller.new(context, ex)
context.response.print(action.forbidden)
end

def error(context, ex : RouteNotFound)
context.response.status_code = 404
action = <%= class_name %>Controller.new(context, ex)
context.response.print(action.not_found)
end

def error(context, ex)
context.response.status_code = 500
action = <%= class_name %>Controller.new(context, ex)
context.response.print(action.internal_server_error)
end
end
42 changes: 16 additions & 26 deletions src/amber/controller/error.cr
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "./base"
require "../exceptions/exception_page"

module Amber::Controller
class Error < Base
Expand All @@ -7,49 +8,38 @@ module Amber::Controller
@context.response.content_type = content_type
end

def not_found
response_format(@ex.message)
def bad_request
response_format
end

def internal_server_error
response_format("ERROR: #{internal_server_error_message}")
def forbidden
response_format
end

def forbidden
response_format(@ex.message)
def not_found
response_format
end

def internal_server_error
response_format
end

private def content_type
if context.request.headers["Accept"]?
request.headers["Accept"].split(",").first
else
"text/html"
"text/plain"
end
end

private def internal_server_error_message
# IMPORTANT: #inspect_with_backtrace will fail in some situations which breaks the tests.
# Even if you call @ex.callstack you'll notice that backtrace is nil.
# #backtrace? is supposed to be safe but it exceptions anyway.
# Please don't remove this without verifying that crystal core has been fixed first.
@ex.inspect_with_backtrace
rescue ex : IndexError
@ex.message
rescue ex
<<-ERROR
Original Error: #{@ex.message}
Error during 'inspect_with_backtrace': #{ex.message}
ERROR
end

private def response_format(message)
private def response_format
case content_type
when "application/json"
{"error": message}.to_json
{"error": @ex.message}.to_json
when "text/html"
"<html><body><pre>#{message}</pre></body></html>"
Amber::Exceptions::ExceptionPageClient.new(@context, @ex).to_s
else
message
@ex.message
end
end
end
Expand Down
113 changes: 113 additions & 0 deletions src/amber/exceptions/exception_page.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
require "ecr"
require "../../environment/settings"

module Amber::Exceptions
include Environment

class ExceptionPage
struct Frame
property app : String,
args : String,
context : String,
index : Int32,
file : String,
line : Int32,
info : String,
snippet = [] of Tuple(Int32, String, Bool)

def initialize(@app, @context, @index, @file, @args, @line, @info, @snippet)
end
end

@params : Hash(String, String)
@headers : Hash(String, Array(String))
@session : Hash(String, HTTP::Cookie)
@method : String
@path : String
@message : String
@query : String
@reload_code = ""
@frames = [] of Frame

def initialize(context : HTTP::Server::Context, @message : String)
@params = context.request.query_params.to_h
@headers = context.response.headers.to_h
@method = context.request.method
@path = context.request.path
@url = "#{context.request.host_with_port}#{context.request.path}"
@query = context.request.query_params.to_s
@session = context.response.cookies.to_h
end

def generate_frames_from(message : String)
generated_frames = [] of Frame
if frames = message.scan(/\s([^\s\:]+):(\d+)([^\n]+)/)
frames.each_with_index do |frame, index|
snippets = [] of Tuple(Int32, String, Bool)
file = frame[1]
filename = file.split('/').last
linenumber = frame[2].to_i
linemsg = "#{file}:#{linenumber}#{frame[3]}"
if File.exists?(file)
lines = File.read_lines(file)
lines.each_with_index do |code, codeindex|
if (codeindex + 1) <= (linenumber + 5) && (codeindex + 1) >= (linenumber - 5)
highlight = (codeindex + 1 == linenumber) ? true : false
snippets << {codeindex + 1, code, highlight}
end
end
end
context = "all"
app = case file
when .includes?("/crystal/")
"crystal"
when .includes?("/amber/")
"amber"
when .includes?("lib/")
"shards"
else
context = "app"
Amber.settings.name.as(String)
end
generated_frames << Frame.new(app, context, index, file, linemsg, linenumber, filename, snippets)
end
end
if self.class.name == "ExceptionPageServer"
generated_frames.reverse
else
generated_frames
end
end

ECR.def_to_s "#{__DIR__}/exception_page.ecr"
end

class ExceptionPageClient < ExceptionPage
EX_ECR_SCRIPT = "lib/amber/src/amber/exceptions/exception_page_client_script.js"

def initialize(context : HTTP::Server::Context, @ex : Exception)
super(context, @ex.message)
@title = "Error #{context.response.status_code}"
@frames = generate_frames_from(@ex.inspect_with_backtrace)
@reload_code = File.read(File.join(Dir.current, EX_ECR_SCRIPT))
end
end

class ExceptionPageServer < ExceptionPage
def initialize(context : HTTP::Server::Context, message : String, @error_id : String)
super(context, message)
@title = "Build Error"
@method = "Server"
@path = Dir.current
@frames = generate_frames_from(message)
@reload_code = ExceptionPageServerScript.new(@error_id).to_s
end
end

class ExceptionPageServerScript
def initialize(@error_id : String)
end

ECR.def_to_s "#{__DIR__}/exception_page_server_script.js"
end
end
Loading