httpx: Add new redirect

This commit is contained in:
Tom Wiesing 2022-11-24 14:19:17 +01:00
parent 41d41035e3
commit 8e2d2cce3e
No known key found for this signature in database
5 changed files with 123 additions and 35 deletions

75
pkg/httpx/errors.go Normal file
View file

@ -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(`<!DOCTYPE HTML><title>` + text + `</title>` + text), nil
})
)

View file

@ -12,27 +12,13 @@ type HTMLHandler[T any] struct {
TemplateName string // name of template to render, defaults to root TemplateName string // name of template to render, defaults to root
} }
var htmlInternalServerErr = []byte(`<!DOCTYPE HTML><title>Internal Server Error</title>Internal Server Error`)
var htmlNotFound = []byte(`<!DOCTYPE HTML><title>Not Found</title>Not Found`)
// ServeHTTP calls j(r) and returns json // ServeHTTP calls j(r) and returns json
func (h HTMLHandler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h HTMLHandler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// call the function // call the function
result, err := h.Handler(r) result, err := h.Handler(r)
// entity not found // intercept any errors
if err == ErrNotFound { if htmlInterceptor.Intercept(w, r, err) {
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)
return return
} }

View file

@ -1,6 +1,25 @@
package httpx package httpx
import "errors" import (
"net/http"
)
// ErrNotFound should be returned from any httpx error to indicate that the item was not found // Response represents a response to an http request.
var ErrNotFound = errors.New("httpx: Error 404") 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)
}

View file

@ -5,9 +5,6 @@ import (
"net/http" "net/http"
) )
var jsonInternalServerErr = []byte(`{"status":"internal server error"}`)
var jsonNotFound = []byte(`{"status":"not found"}`)
// JSON creates a new JSONHandler // JSON creates a new JSONHandler
func JSON[T any](f func(r *http.Request) (T, error)) JSONHandler[T] { func JSON[T any](f func(r *http.Request) (T, error)) JSONHandler[T] {
return JSONHandler[T](f) return JSONHandler[T](f)
@ -22,19 +19,8 @@ func (j JSONHandler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// call the function // call the function
result, err := j(r) result, err := j(r)
// entity not found // handle any errors
if err == ErrNotFound { if jsonInterceptor.Intercept(w, r, err) {
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)
return return
} }

22
pkg/httpx/redirect.go Normal file
View file

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