Skip to content

Commit

Permalink
Fix empty request body validation (#30)
Browse files Browse the repository at this point in the history
* Fix empty request body validation

* update docs
  • Loading branch information
glossd authored Dec 1, 2024
1 parent fd7f20c commit a84b279
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 33 deletions.
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,18 +286,18 @@ type Config struct {
}
```

## HTTP handlers
`fetch.ToHandlerFunc` accepts `func(in) out, error` signature function and converts it into `http.HandlerFunc`.
It unmarshals the HTTP request body into the function argument, then marshals the returned value into the HTTP response body.
## HTTP Handlers
`fetch.ToHandlerFunc` converts `func(in) (out, error)` signature function into `http.HandlerFunc`.
It unmarshals the HTTP request body into the function argument then marshals the returned value into the HTTP response body.
```go
// accepts Pet object and returns Pet object
type Pet struct {
Name string
}
http.HandleFunc("POST /pets", fetch.ToHandlerFunc(func(in Pet) (*Pet, error) {
pet, err := savePet(in)
if err != nil {
log.Println("Couldn't create a pet", err)
return nil, err
if in.Name == "" {
return nil, fmt.Errorf("name can't be empty")
}
return pet, nil
return &Pet{Name: in.Name}, nil
}))
http.ListenAndServe(":8080", nil)
```
Expand Down Expand Up @@ -335,6 +335,6 @@ fetch.SetDefaultHandlerConfig(fetch.HandlerConfig{Middleware: func(w http.Respon
return true
}
return false
},})
}})
```

2 changes: 1 addition & 1 deletion respond.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func Respond(w http.ResponseWriter, body any, config ...RespondConfig) error {
bodyStr, err = Marshal(body)
if err != nil {
_ = respond(w, cfg.ErrorStatus, fmt.Sprintf(respondErrorFormat, err), isRespondErrorFormatJSON, cfg)
return err
return fmt.Errorf("failed to marshal response body: %s", err)
}
}

Expand Down
78 changes: 57 additions & 21 deletions to_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import (
"strconv"
)

type handleTag = string

const (
pathvalTag handleTag = "pathval"
headerTag handleTag = "header"
)

var defaultHandlerConfig = HandlerConfig{
ErrorHook: func(err error) {
fmt.Printf("fetch.Handle failed to respond: %s\n", err)
Expand All @@ -32,7 +39,7 @@ type HandlerConfig struct {
// ErrorHook is called if an error happens while sending an HTTP response
ErrorHook func(err error)
// Middleware is applied before ToHandlerFunc processes the request.
// Return true if to end the request processing.
// Return true to end the request processing.
Middleware func(w http.ResponseWriter, r *http.Request) bool
}

Expand All @@ -45,18 +52,18 @@ ToHandlerFunc converts ApplyFunc into http.HandlerFunc,
which can be used later in http.ServeMux#HandleFunc.
It unmarshals the HTTP request body into the ApplyFunc argument and
then marshals the returned value into the HTTP response body.
To insert PathValue into a field of the unmarshaled entity, specify `pathval` tag
To insert PathValue into a field of the input entity, specify `pathval` tag
to match the pattern's wildcard:
type Pet struct {
Id int `pathval:"id"`
}
type Pet struct {
Id int `pathval:"id"`
}
`header` tag can be used to insert HTTP headers into struct field.
type Pet struct {
Content string `header:"Content-Type"`
}
type Pet struct {
Content string `header:"Content-Type"`
}
*/
func ToHandlerFunc[In any, Out any](apply ApplyFunc[In, Out]) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -75,14 +82,16 @@ func ToHandlerFunc[In any, Out any](apply ApplyFunc[In, Out]) http.HandlerFunc {
}
return
}
in, err = Unmarshal[In](string(reqBody))
if err != nil {
cfg.ErrorHook(err)
err = RespondError(w, 400, err)
if len(reqBody) > 0 || shouldValidateInput(in) {
in, err = Unmarshal[In](string(reqBody))
if err != nil {
cfg.ErrorHook(err)
cfg.ErrorHook(fmt.Errorf("failed to unmarshal request body: %s", err))
err = RespondError(w, 400, err)
if err != nil {
cfg.ErrorHook(err)
}
return
}
return
}
}
in = enrichEntity(in, r)
Expand All @@ -101,12 +110,39 @@ func ToHandlerFunc[In any, Out any](apply ApplyFunc[In, Out]) http.HandlerFunc {
}
}

func enrichEntity[T any](entity T, r *http.Request) T {
typeOf := reflect.TypeOf(entity)
if typeOf.Kind() == reflect.Pointer {
typeOf = reflect.ValueOf(entity).Elem().Type()
// Input entity just might have a field with pathval tag
// and nothing else, we don't need to unmarshal it.
// In case it has some untagged fields, then it must be validated.
func shouldValidateInput(v any) bool {
if t, ok := isStructType(v); ok {
for i := 0; i < t.NumField(); i++ {
tag := t.Field(i).Tag
if tag.Get(headerTag) == "" && tag.Get(pathvalTag) == "" {
return true
}
}
return false
} else {
return false
}
if typeOf.Kind() != reflect.Struct {
}

func isStructType(v any) (reflect.Type, bool) {
typeOf := reflect.TypeOf(v)
switch typeOf.Kind() {
case reflect.Pointer:
t := reflect.ValueOf(v).Elem().Type()
return t, t.Kind() == reflect.Struct
case reflect.Struct:
return typeOf, true
default:
return typeOf, false
}
}

func enrichEntity[T any](entity T, r *http.Request) T {
typeOf, ok := isStructType(entity)
if !ok {
return entity
}
var elem reflect.Value
Expand All @@ -117,10 +153,10 @@ func enrichEntity[T any](entity T, r *http.Request) T {
}
for i := 0; i < typeOf.NumField(); i++ {
field := typeOf.Field(i)
if header := field.Tag.Get("header"); header != "" {
if header := field.Tag.Get(headerTag); header != "" {
elem.Field(i).SetString(r.Header.Get(header))
}
if pathval := field.Tag.Get("pathval"); pathval != "" {
if pathval := field.Tag.Get(pathvalTag); pathval != "" {
pathvar := r.PathValue(pathval)
if pathvar != "" {
switch field.Type.Kind() {
Expand Down
34 changes: 33 additions & 1 deletion to_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func TestToHandlerFunc_MultiplePathValue(t *testing.T) {
assert(t, mw.status, 200)
}

func TestToHandlerFunc_ParseInt(t *testing.T) {
func TestToHandlerFunc_PathvalParseInt(t *testing.T) {
type Pet struct {
Id int `pathval:"id"`
Name string
Expand All @@ -71,6 +71,38 @@ func TestToHandlerFunc_ParseInt(t *testing.T) {
assert(t, mw.status, 200)
}

func TestToHandlerFunc_GetWithPathvalAndNothingToUnmarshal(t *testing.T) {
type Pet struct {
Id int `pathval:"id"`
}
f := ToHandlerFunc(func(in Pet) (Empty, error) {
assert(t, in.Id, 1)
return Empty{}, nil
})
mw := newMockWriter()
mux := http.NewServeMux()
mux.HandleFunc("GET /ids/{id}", f)
r, err := http.NewRequest("GET", "/ids/1", bytes.NewBuffer([]byte(``)))
assert(t, err, nil)
mux.ServeHTTP(mw, r)
assert(t, mw.status, 200)
}

func TestToHandlerFunc_J(t *testing.T) {
f := ToHandlerFunc(func(in J) (J, error) {
assert(t, in.Q("name").String(), "Lola")
return M{"status": "ok"}, nil
})
mw := newMockWriter()
mux := http.NewServeMux()
mux.HandleFunc("POST /j", f)
r, err := http.NewRequest("POST", "/j", bytes.NewBuffer([]byte(`{"name":"Lola"}`)))
assert(t, err, nil)
mux.ServeHTTP(mw, r)
assert(t, mw.status, 200)
assert(t, string(mw.body), `{"status":"ok"}`)
}

func TestToHandlerFunc_Header(t *testing.T) {
type Pet struct {
Content string `header:"Content"`
Expand Down

0 comments on commit a84b279

Please sign in to comment.