Skip to content

Commit

Permalink
feat(docs+core): documented and changed how server errors work
Browse files Browse the repository at this point in the history
  • Loading branch information
ConsoleTVs committed Nov 24, 2024
1 parent da5e048 commit a3d8140
Show file tree
Hide file tree
Showing 16 changed files with 661 additions and 73 deletions.
272 changes: 234 additions & 38 deletions builder.go

Large diffs are not rendered by default.

67 changes: 54 additions & 13 deletions errors.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,69 @@
package akumu

import "fmt"
import (
"errors"
"fmt"
"net/http"
)

type ServerErrorKind string
// ErrServer is the base error that akumu
// will return whenever there's any error
// in the >=500 - <600 range.
type ErrServer struct {

type ServerError struct {
// Code determines the response's status code.
// This is used to understand what response
// was sent due to that particular error.
Code int
URL string
Text string
Kind ServerErrorKind
Body string

// Request stores the actual http request that
// failed to execute. This is very useful to
// extract information such as URL or headers
// to understand more about what caused this error.
Request *http.Request
}

var (
ServerErrorWriter ServerErrorKind = "writer"
ServerErrorBody ServerErrorKind = "body"
ServerErrorStream ServerErrorKind = "stream"
ServerErrorDefault ServerErrorKind = "default"
// ErrServerWriter determines that the
// server error comes from executing logic
// around [Builder.BodyWriter] or derivates
// that use this method.
ErrServerWriter = errors.New("builder is a body writer")

// ErrServerBody determines that the
// server error comes from executing logic
// around [Builder.BodyReader] or derivates
// that use this method.
ErrServerBody = errors.New("builder is a body reader")

// ErrServerStream determines that the
// server error comes from executing logic
// around [Builder.Stream] or derivates
// that use this method.
ErrServerStream = errors.New("builder is a body stream")

// ErrServerDefault determines that the
// server error comes from executing logic
// around having no body, stream nor writer
// involved in delivering a response.
ErrServerDefault = errors.New("builder is a default no body")
)

func (err ServerError) Error() string {
// Error implements the error interface
// for a server error.
func (err ErrServer) Error() string {
return fmt.Sprintf(
"%d: %s",
err.Code,
err.Text,
http.StatusText(err.Code),
)
}

// Is determines if the given target error
// is an [ErrServer]. This is used when dealing
// with errors using [errors.Is] and [errors.As].
func (err ErrServer) Is(target error) bool {
_, ok := target.(ErrServer)

return ok
}
37 changes: 37 additions & 0 deletions errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package akumu_test

import (
"errors"
"net/http"
"testing"

"github.com/studiolambda/akumu"
)

func TestServerErrors(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/foo/bar", nil)

if err != nil {
t.Fatalf("unable to build http request: %s", err)
}

serverErr := akumu.ErrServer{
Code: http.StatusInternalServerError,
Request: req,
}

if !errors.Is(serverErr, akumu.ErrServer{}) {
t.Fatalf("server error cannot be resolved to its error type")
}

serr := akumu.ErrServer{}
ok := errors.As(serverErr, &serr)

if !ok {
t.Fatalf("server error cannot be resolved to its error type")
}

if serr.Code != serverErr.Code {
t.Fatalf("resolved server error code is different from original")
}
}
19 changes: 17 additions & 2 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ import (
"net/http"
)

// Handler is the akumu's equivalent of the [http.Handler].
//
// Is is the function that can take care of a request and
// handle a correct response for it.
type Handler func(*http.Request) error

// handleResponder is a helper that will handle specific [Responder]
// responses. It also takes care of any parent [Builder].
func handleResponder(writer http.ResponseWriter, request *http.Request, parent *Builder, responder Responder) {
if parent != nil {
parent.
Expand All @@ -20,6 +26,8 @@ func handleResponder(writer http.ResponseWriter, request *http.Request, parent *
Handle(writer, request)
}

// handleNoError is called whenever there's a response that does not
// contain any error. For example, returning `nil` in a handler.
func handleNoError(writer http.ResponseWriter, request *http.Request, parent *Builder) {
if parent != nil {
parent.Handle(writer, request)
Expand All @@ -30,6 +38,7 @@ func handleNoError(writer http.ResponseWriter, request *http.Request, parent *Bu
Response(http.StatusOK).Handle(writer, request)
}

// handle takes care of responding to a given request.
func handle(writer http.ResponseWriter, request *http.Request, err error, parent *Builder) {
if err == nil {
handleNoError(writer, request, parent)
Expand Down Expand Up @@ -57,16 +66,22 @@ func handle(writer http.ResponseWriter, request *http.Request, err error, parent
Handle(writer, request)
}

// ServeHTTP implements the [http.Handler] interface to have
// compatibility with the http package.
func (handler Handler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
handle(writer, request, handler(request), nil)
}

// HandlerFunc transforms the [Handler] into an [http.HandlerFunc].
func (handler Handler) HandlerFunc() http.HandlerFunc {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
handler.ServeHTTP(writer, request)
})
}

func HandlerFunc(handler func(*http.Request) error) http.HandlerFunc {
return Handler(handler).HandlerFunc()
// HandlerFunc transforms a [Handler] into an [http.HandlerFunc].
//
// This function is a simple alias of [Handler.HandlerFunc].
func HandlerFunc(handler Handler) http.HandlerFunc {
return handler.HandlerFunc()
}
5 changes: 5 additions & 0 deletions helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import (
)

// JSON decodes the given request payload into `T`
//
// This is very usefull for cases where you want
// to quickly take care of decoding JSON payloads into
// specific types. It automatically disallows unknown
// fields and uses [json.Decoder] with the [http.Request.Body].
func JSON[T any](request *http.Request) (T, error) {
result := *new(T)

Expand Down
19 changes: 18 additions & 1 deletion hooks.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
package akumu

// OnErrorKey is used in the [http.Request]'s context
// to store the hook or callback that will run whenever
// a server error is found.
type OnErrorKey struct{}

type OnErrorHook func(ServerError)
// OnErrorHook stores the current handler that will take
// care of handling a server error in case it's found.
//
// The `error` is always a [ErrServer] and it's joined with
// either of those:
// - [ErrServerWriter]
// - [ErrServerBody]
// - [ErrServerStream]
// - [ErrServerDefault]
//
// To "extend" the hook, you can make use of composition
// to also call the previous function in the new one, creating
// a chain of handlers. Keep in mind you will need to check
// for `nil` if that was the case.
type OnErrorHook func(error)
10 changes: 10 additions & 0 deletions middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,14 @@ package akumu

import "net/http"

// Middleware is just an alias for a function that
// takes a handler and returns another handler.
//
// Use this in places where the common pattern
// of middlewares is needed.
//
// To maintain compatibility with the ecosystem,
// the handlers used by middlewares are [http.Handler]
// instead of akumu's [Handler]. Use the [Builder.Handle]
// to help when dealing with akumu in middlewares.
type Middleware = func(http.Handler) http.Handler
14 changes: 10 additions & 4 deletions middleware/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,31 @@ import (
"github.com/studiolambda/akumu"
)

// Logger middleware sets a [slog.Logger] instance
// as the logger for any http requests.
func Logger(logger *slog.Logger) akumu.Middleware {
return func(handler http.Handler) http.Handler {
return LoggerWith(handler, logger)
}
}

// LoggerDefault middleware sets the [slog.Default] instance
// as the logger for any http requests.
func LoggerDefault() akumu.Middleware {
return func(handler http.Handler) http.Handler {
return LoggerWith(handler, slog.Default())
}
}

// LoggerWith middleware sets a [slog.Logger] instance
// as the logger for any http requests but this time accepting
// the handler as a parameter.
func LoggerWith(handler http.Handler, logger *slog.Logger) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
parent, hasParent := request.Context().Value(akumu.OnErrorKey{}).(akumu.OnErrorHook)

handler.ServeHTTP(writer, request.WithContext(
context.WithValue(request.Context(), akumu.OnErrorKey{}, func(err akumu.ServerError) {
context.WithValue(request.Context(), akumu.OnErrorKey{}, func(err akumu.ErrServer) {
if hasParent && parent != nil {
parent(err)
}
Expand All @@ -34,9 +41,8 @@ func LoggerWith(handler http.Handler, logger *slog.Logger) http.Handler {
request.Context(),
"server error",
"code", err.Code,
"text", err.Text,
"url", err.URL,
"kind", err.Kind,
"text", http.StatusText(err.Code),
"url", err.Request.URL,
)
}),
))
Expand Down
3 changes: 3 additions & 0 deletions middleware/problems.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import (
"github.com/studiolambda/akumu"
)

// Problems sets the given [akumu.ProblemControls] to the [http.Request].
//
// The [akumu.Problem] will respect those controls when a problem is handled.
func Problems(controls akumu.ProblemControls) akumu.Middleware {
return func(handler http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
Expand Down
21 changes: 19 additions & 2 deletions middleware/recover.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,24 @@ import (
)

var (
ErrUnexpected = errors.New("an unexpected error occurred")
// ErrRecoverUnexpectedError is the default error that's passed to
// the recover response when the error cannot be determined from the
// given recover()'s value.
ErrRecoverUnexpectedError = errors.New("an unexpected error occurred")
)

// Recover recovers any panics during a [akumu.Handler] execution.
//
// If the recover() value is of type error, this is directly passed to
// the [akumu.Failed] method.
//
// If it's of type string, a new error is created
// with that string.
//
// If it implements [fmt.Stringer] then that string
// will be used.
//
// If none matches, [ErrRecoverUnexpectedError] is returned.
func Recover() akumu.Middleware {
return func(handler http.Handler) http.Handler {
return RecoverWith(handler, func(value any) error {
Expand All @@ -24,11 +39,13 @@ func Recover() akumu.Middleware {
return errors.New(err.String())
}

return ErrUnexpected
return ErrRecoverUnexpectedError
})
}
}

// RecoverWith allows a handler to decide what to do with the recover() value,
// allowing to customize the error that [akumu.Failed] receives.
func RecoverWith(handler http.Handler, handle func(value any) error) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
defer func() {
Expand Down
Loading

0 comments on commit a3d8140

Please sign in to comment.