Skip to content

Commit

Permalink
feat: added size limiter middleware
Browse files Browse the repository at this point in the history
* Added size limiter middleware
* Added context type validation on JSON decoders
* Improved BodyDecodeErrorHandler function (previously named as bodyDecodeErrorHandler)
  • Loading branch information
ralvarezdev committed Feb 4, 2025
1 parent 0699929 commit fb6f235
Show file tree
Hide file tree
Showing 15 changed files with 272 additions and 43 deletions.
143 changes: 129 additions & 14 deletions http/json/body.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,83 @@ import (
"errors"
"fmt"
gonethttpresponse "github.com/ralvarezdev/go-net/http/response"
"io"
"net/http"
"strings"
)

// Inspired by:
// https://www.alexedwards.net/blog/how-to-properly-parse-a-json-request-body

var (
ErrCodeUnmarshalRequestBodyFailed *string
ErrCodeRequestBodyFieldError *string
ErrCodeSyntaxError *string
ErrCodeUnmarshalTypeError *string
ErrCodeUnknownField *string
ErrCodeEmptyBody *string
ErrCodeMaxBodySizeExceeded *string
)

// bodyDecodeErrorHandler handles the error on JSON body decoding
func bodyDecodeErrorHandler(
// NewUnmarshalTypeErrorResponse creates a new response for an UnmarshalTypeError
func NewUnmarshalTypeErrorResponse(
fieldName string,
fieldTypeName string,
) gonethttpresponse.Response {
return gonethttpresponse.NewResponseFromFailRequestError(
gonethttpresponse.NewFieldError(
fieldName,
fmt.Sprintf(
gonethttpresponse.ErrInvalidFieldValueType,
fieldTypeName,
),
ErrCodeUnmarshalTypeError,
http.StatusBadRequest,
),
)
}

// NewSyntaxErrorResponse creates a new response for a SyntaxError
func NewSyntaxErrorResponse(offset int64) gonethttpresponse.Response {
// Create the error
err := fmt.Errorf(ErrSyntaxError, offset)

return gonethttpresponse.NewJSendErrorResponse(
err,
err,
nil,
ErrCodeSyntaxError,
http.StatusBadRequest,
)
}

// NewUnknownFieldErrorResponse creates a new response for an unknown field error
func NewUnknownFieldErrorResponse(fieldName string) gonethttpresponse.Response {
return gonethttpresponse.NewResponseFromFailRequestError(
gonethttpresponse.NewFieldError(
fieldName,
fmt.Sprintf(ErrUnknownField, fieldName),
ErrCodeUnknownField,
http.StatusBadRequest,
),
)
}

// NewMaxBodySizeExceededErrorResponse creates a new response for a body size exceeded error
func NewMaxBodySizeExceededErrorResponse(limit int64) gonethttpresponse.Response {
// Create the error
err := fmt.Errorf(ErrMaxBodySizeExceeded, limit)

return gonethttpresponse.NewJSendErrorResponse(
err,
err,
nil,
ErrCodeMaxBodySizeExceeded,
http.StatusRequestEntityTooLarge,
)
}

// BodyDecodeErrorHandler handles the error on JSON body decoding
func BodyDecodeErrorHandler(
w http.ResponseWriter,
err error,
encoder Encoder,
Expand All @@ -25,7 +92,11 @@ func bodyDecodeErrorHandler(
}

// Check is there is an UnmarshalTypeError
var syntaxError *json.SyntaxError
var maxBytesError *http.MaxBytesError
var unmarshalTypeError *json.UnmarshalTypeError

// Check if the error is an UnmarshalTypeError
if errors.As(err, &unmarshalTypeError) {
// Check which field failed
fieldName := unmarshalTypeError.Field
Expand All @@ -35,21 +106,65 @@ func bodyDecodeErrorHandler(
if fieldName != "" {
return encoder.Encode(
w,
gonethttpresponse.NewResponseFromFailRequestError(
gonethttpresponse.NewFieldError(
fieldName,
fmt.Sprintf(
gonethttpresponse.ErrInvalidFieldValueType,
fieldTypeName,
),
http.StatusBadRequest,
ErrCodeRequestBodyFieldError,
),
),
NewUnmarshalTypeErrorResponse(fieldName, fieldTypeName),
)
}
}

// Check if the error is a SyntaxError
if errors.As(err, &syntaxError) {
return encoder.Encode(
w,
NewSyntaxErrorResponse(syntaxError.Offset),
)
}

// Check if the error is an ErrUnexpectedEOF
if errors.Is(err, io.ErrUnexpectedEOF) {
return encoder.Encode(
w,
gonethttpresponse.NewJSendErrorResponse(
ErrUnexpectedEOF,
ErrUnexpectedEOF,
nil,
ErrCodeSyntaxError,
http.StatusBadRequest,
),
)
}

// Check if the error is an unknown field error
if strings.HasPrefix(err.Error(), "json: unknown field ") {
// Get the field name
fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")

return encoder.Encode(
w,
NewUnknownFieldErrorResponse(fieldName),
)
}

// Check if the error is caused by an empty request body
if errors.Is(err, io.EOF) {
return encoder.Encode(
w,
gonethttpresponse.NewJSendErrorResponse(
ErrEmptyBody,
ErrEmptyBody,
nil,
ErrCodeEmptyBody,
http.StatusBadRequest,
),
)
}

// Catch the error caused by the request body being too large
if errors.As(err, &maxBytesError) {
return encoder.Encode(
w,
NewMaxBodySizeExceededErrorResponse(maxBytesError.Limit),
}

return encoder.Encode(
w,
gonethttpresponse.NewJSendErrorResponse(
Expand Down
26 changes: 26 additions & 0 deletions http/json/content_type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package json

import (
"net/http"
"strings"
)

// Inspired by:
// https://www.alexedwards.net/blog/how-to-properly-parse-a-json-request-body

// CheckContentType checks if the content type is JSON
func CheckContentType(r *http.Request) bool {
contentType := r.Header.Get("Content-Type")
if contentType != "" {
mediaType := strings.ToLower(
strings.TrimSpace(
strings.Split(
contentType,
";",
)[0],
),
)
return mediaType == "application/json"
}
return false
}
21 changes: 13 additions & 8 deletions http/json/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (
)

var (
ErrCodeReadBodyFailed *string
ErrCodeNilDestination *string
ErrCodeFailedToReadBody *string
ErrCodeNilDestination *string
)

type (
Expand Down Expand Up @@ -53,6 +53,14 @@ func (d *DefaultDecoder) Decode(
r *http.Request,
dest interface{},
) (err error) {
// Check the content type
if !CheckContentType(r) {
_ = d.encoder.Encode(
w,
gonethttpresponse.NewResponseFromFailRequestError(ErrInvalidContentType),
)
}

// Check the decoder destination
if dest == nil {
_ = d.encoder.Encode(
Expand All @@ -66,20 +74,17 @@ func (d *DefaultDecoder) Decode(
if err != nil {
_ = d.encoder.Encode(
w,
gonethttpresponse.NewJSendErrorResponse(
gonethttpstatusresponse.NewJSendDebugInternalServerError(
err,
nil,
nil,
ErrCodeReadBodyFailed,
http.StatusBadRequest,
ErrCodeFailedToReadBody,
),
)
return err
}

// Decode JSON body into destination
if err = json.Unmarshal(body, dest); err != nil {
_ = bodyDecodeErrorHandler(w, err, d.encoder)
_ = BodyDecodeErrorHandler(w, err, d.encoder)
}
return err
}
18 changes: 18 additions & 0 deletions http/json/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,28 @@ package json

import (
"errors"
gonethttpresponse "github.com/ralvarezdev/go-net/http/response"
"net/http"
)

var (
ErrCodeInvalidContentType *string
Er
)

var (
ErrNilEncoder = errors.New("json encoder is nil")
ErrNilDecoder = errors.New("json decoder is nil")
ErrUnmarshalBodyFailed = errors.New("failed to unmarshal json body")
ErrInvalidContentType = gonethttpresponse.NewHeaderError(
"Content-Type",
"invalid content type, expected application/json",
ErrCodeInvalidContentType,
http.StatusUnsupportedMediaType,
)
ErrMaxBodySizeExceeded = "json body size exceeds the maximum allowed size, limit is %d bytes"
ErrSyntaxError = "json body contains badly-formed JSON at position %d"
ErrUnexpectedEOF = errors.New("json body contains badly-formed JSON")
ErrEmptyBody = errors.New("json body is empty")
ErrUnknownField = "json body contains an unknown field %s"
)
17 changes: 15 additions & 2 deletions http/json/stream_decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package json
import (
"encoding/json"
goflagsmode "github.com/ralvarezdev/go-flags/mode"
gonethttpresponse "github.com/ralvarezdev/go-net/http/response"
gonethttpstatusresponse "github.com/ralvarezdev/go-net/http/status/response"
"net/http"
)
Expand Down Expand Up @@ -37,6 +38,14 @@ func (d *DefaultStreamDecoder) Decode(
r *http.Request,
dest interface{},
) (err error) {
// Check the content type
if !CheckContentType(r) {
_ = d.encoder.Encode(
w,
gonethttpresponse.NewResponseFromFailRequestError(ErrInvalidContentType),
)
}

// Check the decoder destination
if dest == nil {
_ = d.encoder.Encode(
Expand All @@ -48,9 +57,13 @@ func (d *DefaultStreamDecoder) Decode(
)
}

// Create a new reader from the body
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()

// Decode JSON body into destination
if err = json.NewDecoder(r.Body).Decode(dest); err != nil {
_ = bodyDecodeErrorHandler(w, err, d.encoder)
if err = decoder.Decode(dest); err != nil {
_ = BodyDecodeErrorHandler(w, err, d.encoder)
}
return err
}
2 changes: 1 addition & 1 deletion http/middleware/auth/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ var (
ErrInvalidAuthorizationHeader = gonethttpresponse.NewHeaderError(
gojwtnethttp.AuthorizationHeaderKey,
"invalid authorization header",
http.StatusUnauthorized,
ErrCodeInvalidAuthorizationHeader,
http.StatusUnauthorized,
)
)
Loading

0 comments on commit fb6f235

Please sign in to comment.