From 8e2d2cce3ebe3ab4c6edb59d5d40cc8c9379adb6 Mon Sep 17 00:00:00 2001 From: Tom Wiesing Date: Thu, 24 Nov 2022 14:19:17 +0100 Subject: [PATCH] httpx: Add new redirect --- pkg/httpx/errors.go | 75 +++++++++++++++++++++++++++++++++++++++++++ pkg/httpx/html.go | 18 ++--------- pkg/httpx/httpx.go | 25 +++++++++++++-- pkg/httpx/json.go | 18 ++--------- pkg/httpx/redirect.go | 22 +++++++++++++ 5 files changed, 123 insertions(+), 35 deletions(-) create mode 100644 pkg/httpx/errors.go create mode 100644 pkg/httpx/redirect.go diff --git a/pkg/httpx/errors.go b/pkg/httpx/errors.go new file mode 100644 index 0000000..99d5c3a --- /dev/null +++ b/pkg/httpx/errors.go @@ -0,0 +1,75 @@ +package httpx + +import ( + "encoding/json" + "errors" + "net/http" +) + +// ErrInterceptor intercepts errors and directly returns specific responses for them +type ErrInterceptor struct { + Errors map[error]Response + Fallback Response +} + +// Intercept attempts to intercept the given error. +// When err is nil, does nothing. +// +// When err is not nil, first attempts to find a static response in errors and respond with that. +// Otherwise it returns the Fallback response. +// intercepted indicates if some response was sent. +func (ei ErrInterceptor) Intercept(w http.ResponseWriter, r *http.Request, err error) (intercepted bool) { + if err == nil { + return false + } + + res, ok := ei.Errors[err] + if !ok { + res = ei.Fallback + } + + res.ServerHTTP(w, r) + return true +} + +// StatusInterceptor creates a new ErrInterceptor handling default responses. +// If body returns err != nil, StatusInterceptor calls panic(). +func StatusInterceptor(contentType string, body func(code int, text string) ([]byte, error)) ErrInterceptor { + makeResponse := func(code int) (res Response) { + var err error + res.Body, err = body(code, http.StatusText(code)) + if err != nil { + panic("StatusInterceptor: err != nil") + } + + res.ContentType = contentType + res.StatusCode = code + return + } + + return ErrInterceptor{ + Errors: map[error]Response{ + ErrNotFound: makeResponse(http.StatusNotFound), + ErrForbidden: makeResponse(http.StatusForbidden), + }, + Fallback: makeResponse(http.StatusInternalServerError), + } +} + +// Common errors accepted by all httpx handlers +var ( + ErrNotFound = errors.New("httpx: Not Found") + ErrForbidden = errors.New("httpx: Forbidden") +) + +var ( + textInterceptor = StatusInterceptor("text/plain", func(code int, text string) ([]byte, error) { + return []byte(text), nil + }) + jsonInterceptor = StatusInterceptor("application/json", func(code int, text string) ([]byte, error) { + return json.Marshal(map[string]any{"status": text, "code": code}) + }) + htmlInterceptor = StatusInterceptor("text/html", func(code int, text string) ([]byte, error) { + return []byte(`` + text + `` + text), nil + }) +) diff --git a/pkg/httpx/html.go b/pkg/httpx/html.go index 4a017d8..bd0a70f 100644 --- a/pkg/httpx/html.go +++ b/pkg/httpx/html.go @@ -12,27 +12,13 @@ type HTMLHandler[T any] struct { TemplateName string // name of template to render, defaults to root } -var htmlInternalServerErr = []byte(`Internal Server ErrorInternal Server Error`) -var htmlNotFound = []byte(`Not FoundNot Found`) - // ServeHTTP calls j(r) and returns json func (h HTMLHandler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) { // call the function result, err := h.Handler(r) - // entity not found - if err == ErrNotFound { - w.Header().Set("Content-Type", "text/html") - w.WriteHeader(http.StatusNotFound) - w.Write(htmlNotFound) - return - } - - // handle other errors - if err != nil { - w.Header().Set("Content-Type", "text/html") - w.WriteHeader(http.StatusInternalServerError) - w.Write(htmlInternalServerErr) + // intercept any errors + if htmlInterceptor.Intercept(w, r, err) { return } diff --git a/pkg/httpx/httpx.go b/pkg/httpx/httpx.go index d6b64fb..ebde9ae 100644 --- a/pkg/httpx/httpx.go +++ b/pkg/httpx/httpx.go @@ -1,6 +1,25 @@ package httpx -import "errors" +import ( + "net/http" +) -// ErrNotFound should be returned from any httpx error to indicate that the item was not found -var ErrNotFound = errors.New("httpx: Error 404") +// Response represents a response to an http request. +type Response struct { + ContentType string // defaults to text/plain + Body []byte + StatusCode int // defaults to [http.StatusOK] +} + +func (response Response) ServerHTTP(w http.ResponseWriter, r *http.Request) { + if response.ContentType == "" { + response.ContentType = "text/plain" + } + w.Header().Set("Content-Type", response.ContentType) + + if response.StatusCode <= 0 { + response.StatusCode = http.StatusOK + } + w.WriteHeader(response.StatusCode) + w.Write(response.Body) +} diff --git a/pkg/httpx/json.go b/pkg/httpx/json.go index bbacc15..e95bf86 100644 --- a/pkg/httpx/json.go +++ b/pkg/httpx/json.go @@ -5,9 +5,6 @@ import ( "net/http" ) -var jsonInternalServerErr = []byte(`{"status":"internal server error"}`) -var jsonNotFound = []byte(`{"status":"not found"}`) - // JSON creates a new JSONHandler func JSON[T any](f func(r *http.Request) (T, error)) JSONHandler[T] { return JSONHandler[T](f) @@ -22,19 +19,8 @@ func (j JSONHandler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) { // call the function result, err := j(r) - // entity not found - if err == ErrNotFound { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusNotFound) - w.Write(jsonNotFound) - return - } - - // handle other errors - if err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - w.Write(jsonInternalServerErr) + // handle any errors + if jsonInterceptor.Intercept(w, r, err) { return } diff --git a/pkg/httpx/redirect.go b/pkg/httpx/redirect.go new file mode 100644 index 0000000..b5090b1 --- /dev/null +++ b/pkg/httpx/redirect.go @@ -0,0 +1,22 @@ +package httpx + +import ( + "net/http" +) + +// RedirectHandler represents a handler that redirects the user to the address returned +type RedirectHandler func(r *http.Request) (string, int, error) + +// ServeHTTP calls r(r) and returns json +func (rh RedirectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // call the function + url, code, err := rh(r) + + // intercept the errors + if textInterceptor.Intercept(w, r, err) { + return + } + + // do the redirect + http.Redirect(w, r, url, code) +}