Files
pocket-id/backend/internal/httpserver/binding.go
T

108 lines
2.4 KiB
Go

package httpserver
import (
"errors"
"io"
"mime"
"mime/multipart"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
"github.com/pocket-id/pocket-id/backend/internal/apperror"
"github.com/pocket-id/pocket-id/backend/internal/dto"
)
// BindJSON binds and normalizes a JSON request while distinguishing invalid input from internal failures
func BindJSON(c *gin.Context, value any) error {
if err := requireJSONContentType(c); err != nil {
return err
}
err := classifyBindingError(c.ShouldBindJSON(value))
if err != nil {
return err
}
dto.Normalize(value)
return nil
}
// BindOptionalJSON accepts an empty body while normalizing valid input and classifying malformed JSON as invalid input
func BindOptionalJSON(c *gin.Context, value any) error {
if c.Request.ContentLength == 0 {
return nil
}
if err := requireJSONContentType(c); err != nil {
return err
}
err := c.ShouldBindJSON(value)
if errors.Is(err, io.EOF) {
return nil
}
if err = classifyBindingError(err); err != nil {
return err
}
dto.Normalize(value)
return nil
}
// FormFile returns an uploaded file while classifying a missing field as request validation
func FormFile(c *gin.Context, field string) (*multipart.FileHeader, error) {
file, err := c.FormFile(field)
if errors.Is(err, http.ErrMissingFile) {
return nil, apperror.MissingField(field)
}
if err != nil {
return nil, apperror.InvalidRequestBody(err)
}
return file, nil
}
func requireJSONContentType(c *gin.Context) error {
mediaType, _, err := mime.ParseMediaType(c.GetHeader("Content-Type"))
if err != nil {
return apperror.InvalidRequestBody(err)
}
if !isJSONMediaType(mediaType) {
return apperror.InvalidRequestBody(errors.New("request Content-Type is not JSON"))
}
return nil
}
func isJSONMediaType(mediaType string) bool {
topLevelType, subtype, ok := strings.Cut(mediaType, "/")
if !ok || topLevelType != "application" {
return false
}
if subtype == "json" {
return true
}
baseSubtype, ok := strings.CutSuffix(subtype, "+json")
return ok && baseSubtype != ""
}
func classifyBindingError(err error) error {
if err == nil {
return nil
}
if _, ok := errors.AsType[validator.ValidationErrors](err); ok {
return err
}
if _, ok := errors.AsType[binding.SliceValidationError](err); ok {
return err
}
return apperror.InvalidRequestBody(err)
}