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)
+}