From 000fa4897a4e7402d6a3861d7831011e31d878d2 Mon Sep 17 00:00:00 2001 From: Nathan Ortega Date: Mon, 2 Sep 2024 13:08:01 -0400 Subject: [PATCH] feature/add-custom-validation-error (#214) * added custom validation error handling * cleaned up the string interpolation in the server welcome function --- src/core.jl | 5 +++-- src/errors.jl | 15 +++++++++++++++ src/extractors.jl | 5 +++-- src/utilities/misc.jl | 16 ++++++++++++---- test/extractortests.jl | 8 ++++---- 5 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 src/errors.jl diff --git a/src/core.jl b/src/core.jl index aeb46e55..b8e24392 100644 --- a/src/core.jl +++ b/src/core.jl @@ -12,6 +12,7 @@ using RelocatableFolders using DataStructures: CircularDeque import Base.Threads: lock, nthreads +include("errors.jl"); @reexport using .Errors include("util.jl"); @reexport using .Util include("types.jl"); @reexport using .Types include("constants.jl"); @reexport using .Constants @@ -44,10 +45,10 @@ function serverwelcome(external_url::String, docs::Bool, metrics::Bool, parallel @info "📦 Version 1.5.12 (2024-06-18)" @info "✅ Started server: $external_url" if docs - @info "📖 Documentation: $external_url$docspath" + @info "📖 Documentation: $external_url" * docspath end if docs && metrics - @info "📊 Metrics: $external_url$docspath/metrics" + @info "📊 Metrics: $external_url" * "$docspath/metrics" end if parallel @info "🚀 Running in parallel mode with $(Threads.nthreads()) threads" diff --git a/src/errors.jl b/src/errors.jl new file mode 100644 index 00000000..361dcd76 --- /dev/null +++ b/src/errors.jl @@ -0,0 +1,15 @@ +module Errors +## In this module, we export commonly used exceptions across the package + +export ValidationError + +# This is used by the Extractors.jl module to signal that a validation error has occurred +struct ValidationError <: Exception + msg::String +end + +function Base.showerror(io::IO, e::ValidationError) + print(io, "Validation Error: $(e.msg)") +end + +end \ No newline at end of file diff --git a/src/extractors.jl b/src/extractors.jl index 7a70f577..c2bd9929 100644 --- a/src/extractors.jl +++ b/src/extractors.jl @@ -8,6 +8,7 @@ using StructTypes using ..Util: text, json, formdata, parseparam using ..Reflection: struct_builder, extract_struct_info +using ..Errors: ValidationError using ..Types export Extractor, extract, validate, extracttype, isextractor, isreqparam, isbodyparam, @@ -85,14 +86,14 @@ function try_validate(param::Param{U}, instance::T) :: T where {T, U <: Extracto # Case 1: Use global validate function - returns true if one isn't defined for this type if !validate(instance) impl = Base.which(validate, (T,)) - throw(ArgumentError("Validation failed for $(param.name): $T \n|> $instance \n|> $impl")) + throw(ValidationError("Validation failed for $(param.name): $T \n|> $instance \n|> $impl")) end # Case 2: Use custom validate function from an Extractor (if defined) if param.hasdefault && param.default isa U && !isnothing(param.default.validate) if !param.default.validate(instance) impl = Base.which(param.default.validate, (T,)) - throw(ArgumentError("Validation failed for $(param.name): $T \n|> $instance \n|> $impl")) + throw(ValidationError("Validation failed for $(param.name): $T \n|> $instance \n|> $impl")) end end diff --git a/src/utilities/misc.jl b/src/utilities/misc.jl index cf56730d..3ccd8ee1 100644 --- a/src/utilities/misc.jl +++ b/src/utilities/misc.jl @@ -2,11 +2,12 @@ using HTTP using JSON3 using Dates +using ..Errors: ValidationError + export countargs, recursive_merge, parseparam, queryparams, redirect, handlerequest, format_response!, set_content_size!, format_sse_message - ### Request helper functions ### """ @@ -19,7 +20,6 @@ function queryparams(req::HTTP.Request) :: Dict return HTTP.queryparams(uri.query) end - """ redirect(path::String; code = 308) @@ -29,6 +29,14 @@ function redirect(path::String; code = 307) :: HTTP.Response return HTTP.Response(code, ["Location" => path]) end +function handle_error(::ValidationError) + return json(("message" => "400: Bad Request"), status = 400) +end + +function handle_error(::Any) + return json(("message" => "500: Internal Server Error"), status = 500) +end + function handlerequest(getresponse::Function, catch_errors::Bool; show_errors::Bool = true) if !catch_errors return getresponse() @@ -37,9 +45,9 @@ function handlerequest(getresponse::Function, catch_errors::Bool; show_errors::B return getresponse() catch error if show_errors && !isa(error, InterruptException) - @error "ERROR: " exception=(error, catch_backtrace()) + @error "ERROR: " exception=(error, catch_backtrace()) end - return json(("message" => "The Server encountered a problem"), status = 500) + return handle_error(error) end end end diff --git a/test/extractortests.jl b/test/extractortests.jl index 8e94c7d2..73bcb286 100644 --- a/test/extractortests.jl +++ b/test/extractortests.jl @@ -90,14 +90,14 @@ end # Test that negative age trips the global validator req = HTTP.Request("GET", "/", [], """name=joe&age=-4""") param = Param(:form, Form{Person}, missing, false) - @test_throws ArgumentError extract(param, LazyRequest(request=req)) + @test_throws Oxygen.Core.Errors.ValidationError extract(param, LazyRequest(request=req)) # Test that age < 25 trips the local validator req = HTTP.Request("GET", "/", [], """name=joe&age=10""") default_value = Form{Person}(x -> x.age > 25) param = Param(:form, Form{Person}, default_value, true) - @test_throws ArgumentError extract(param, LazyRequest(request=req)) + @test_throws Oxygen.Core.Errors.ValidationError extract(param, LazyRequest(request=req)) end @@ -253,7 +253,7 @@ end @suppress_err begin # should fail since we are missing query params r = internalrequest(HTTP.Request("GET", "/headers", ["limit" => "3"], "")) - @test r.status == 500 + @test r.status == 400 end @suppress_err begin @@ -265,7 +265,7 @@ end "value": 12.0 } """)) - @test r.status == 500 + @test r.status == 400 end r = internalrequest(HTTP.Request("POST", "/json", [], """