binder

package
v0.3.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 17, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package binder provides comprehensive HTTP request data binding utilities for Go web applications. It supports binding JSON, form data, query parameters, and path parameters to Go structs with built-in validation, sanitization, and security features.

Features

  • JSON binding with strict parsing and size limits
  • Form data binding supporting both URL-encoded and multipart forms
  • Query parameter binding with multi-value support
  • Path parameter binding compatible with popular routers
  • Automatic input sanitization to prevent XSS and injection attacks
  • Comprehensive error handling with descriptive messages
  • Security hardening against DoS and malformed data attacks

Usage

The package provides four main binding functions that can be used individually or combined:

import "github.com/dmitrymomot/forge/pkg/binder"

// JSON binding
jsonBinder := binder.JSON()

// Form binding (URL-encoded and multipart)
formBinder := binder.Form()

// Query parameter binding
queryBinder := binder.Query()

// Path parameter binding (requires router-specific extractor)
pathBinder := binder.Path(chi.URLParam) // for chi router

JSON Binding

JSON binding parses request bodies with Content-Type validation, size limits, and strict parsing to prevent malformed data:

type CreateUserRequest struct {
	Name     string `json:"name"`
	Email    string `json:"email"`
	Age      int    `json:"age"`
	Optional *bool  `json:"optional,omitempty"`
}

func handler(w http.ResponseWriter, r *http.Request) {
	var req CreateUserRequest
	if err := binder.JSON()(r, &req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	// req is now populated from JSON body
}

Form Binding

Form binding handles both URL-encoded forms and multipart forms with file uploads. It supports comprehensive struct tags and type conversion:

type UploadRequest struct {
	Title       string                  `form:"title"`
	Category    string                  `form:"category"`
	Tags        []string                `form:"tags"`     // Multi-value support
	IsPublic    bool                    `form:"public"`   // String to bool conversion
	Priority    int                     `form:"priority"` // String to int conversion
	Avatar      *multipart.FileHeader   `file:"avatar"`   // Single file upload
	Attachments []*multipart.FileHeader `file:"files"`    // Multiple file uploads
	Internal    string                  `form:"-"`        // Ignored field
}

func uploadHandler(w http.ResponseWriter, r *http.Request) {
	var req UploadRequest
	if err := binder.Form()(r, &req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// Process uploaded files
	if req.Avatar != nil {
		file, err := req.Avatar.Open()
		if err != nil {
			http.Error(w, "Failed to open file", http.StatusInternalServerError)
			return
		}
		defer file.Close()
		// Process file content
	}
}

Query Parameter Binding

Query parameter binding extracts data from URL query strings with support for multi-value parameters and type conversion:

type SearchRequest struct {
	Query     string   `query:"q"`
	Page      int      `query:"page"`
	PageSize  int      `query:"page_size"`
	Tags      []string `query:"tags"`     // ?tags=go&tags=web
	Active    *bool    `query:"active"`   // Optional parameter
	SortBy    string   `query:"sort"`
	Ascending bool     `query:"asc"`
}

func searchHandler(w http.ResponseWriter, r *http.Request) {
	var req SearchRequest
	if err := binder.Query()(r, &req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	// req is populated from query parameters
}

Path Parameter Binding

Path parameter binding extracts values from URL path segments using router-specific extractor functions:

import "github.com/go-chi/chi/v5"

type ProfileRequest struct {
	UserID   string `path:"id"`
	Username string `path:"username"`
}

func main() {
	r := chi.NewRouter()
	pathBinder := binder.Path(chi.URLParam)

	r.Get("/users/{id}/profile/{username}", func(w http.ResponseWriter, r *http.Request) {
		var req ProfileRequest
		if err := pathBinder(r, &req); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		// req.UserID and req.Username are populated from path
	})
}

// With gorilla/mux
import "github.com/gorilla/mux"

func setupMuxRoutes() {
	muxExtractor := func(r *http.Request, fieldName string) string {
		vars := mux.Vars(r)
		return vars[fieldName]
	}
	pathBinder := binder.Path(muxExtractor)

	router := mux.NewRouter()
	router.HandleFunc("/users/{id}/profile/{username}", func(w http.ResponseWriter, r *http.Request) {
		var req ProfileRequest
		if err := pathBinder(r, &req); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		// Use req...
	})
}

Combining Multiple Binders

Multiple binders can be combined to handle complex request structures that include data from different sources:

type ComplexRequest struct {
	// From path parameters
	UserID   string `path:"id"`

	// From query parameters
	Page     int    `query:"page"`
	PageSize int    `query:"page_size"`

	// From form data
	Name     string                `form:"name"`
	Avatar   *multipart.FileHeader `file:"avatar"`
}

func complexHandler(w http.ResponseWriter, r *http.Request) {
	var req ComplexRequest

	// Apply binders in sequence
	binders := []func(*http.Request, any) error{
		binder.Path(chi.URLParam),
		binder.Query(),
		binder.Form(),
	}

	for _, bind := range binders {
		if err := bind(r, &req); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
	}

	// req is now populated from all sources
}

Supported Types

The binder package supports automatic type conversion for:

  • string
  • int, int8, int16, int32, int64
  • uint, uint8, uint16, uint32, uint64
  • float32, float64
  • bool (recognizes: true, false, 1, 0, on, off, yes, no)
  • Slices of any of the above types
  • Pointers to any of the above types (for optional fields)
  • *multipart.FileHeader and []*multipart.FileHeader (for file uploads)

Security Features

The package includes several security hardening measures:

  • Request size limits to prevent DoS attacks (DefaultMaxJSONSize=1MB, DefaultMaxMemory=10MB)
  • Input sanitization to prevent XSS and injection attacks
  • Filename sanitization for uploaded files to prevent path traversal
  • Boundary validation for multipart forms
  • Strict JSON parsing with unknown field rejection
  • Context timeout handling to avoid processing cancelled requests

Error Handling

The package provides comprehensive error types for different failure scenarios:

import (
	"errors"
	"github.com/dmitrymomot/forge/pkg/binder"
)

func handleBindingError(err error) {
	switch {
	case errors.Is(err, binder.ErrUnsupportedMediaType):
		// Handle unsupported media type
	case errors.Is(err, binder.ErrFailedToParseJSON):
		// Handle JSON parsing error
	case errors.Is(err, binder.ErrFailedToParseForm):
		// Handle form parsing error
	case errors.Is(err, binder.ErrFailedToParseQuery):
		// Handle query parsing error
	case errors.Is(err, binder.ErrFailedToParsePath):
		// Handle path parsing error
	case errors.Is(err, binder.ErrMissingContentType):
		// Handle missing content type
	case errors.Is(err, binder.ErrBinderNotApplicable):
		// Handle inapplicable binder
	default:
		// Handle other binding errors
	}
}

Constants

The package defines the following constants:

  • DefaultMaxJSONSize: Maximum JSON request body size (1MB)
  • DefaultMaxMemory: Maximum memory for multipart form parsing (10MB)

Index

Constants

View Source
const DefaultMaxJSONSize = 1 << 20 // 1 MB

DefaultMaxJSONSize is the default maximum size for JSON request bodies (1MB).

View Source
const DefaultMaxMemory = 10 << 20 // 10 MB

DefaultMaxMemory is the default maximum memory used for parsing multipart forms (10MB).

Variables

View Source
var (
	// ErrUnsupportedMediaType indicates the Content-Type header specifies a media type
	// that the binder doesn't support (e.g., text/plain for JSON binder).
	ErrUnsupportedMediaType = errors.New("unsupported media type")

	// ErrFailedToParseJSON indicates the request body contains invalid JSON
	// or doesn't match the target struct schema.
	ErrFailedToParseJSON = errors.New("failed to parse JSON request body")

	// ErrFailedToParseForm indicates form data parsing failed due to malformed
	// multipart boundaries or invalid URL-encoded data.
	ErrFailedToParseForm = errors.New("failed to parse form data")

	// ErrFailedToParseQuery indicates query parameter parsing failed,
	// typically due to type conversion errors.
	ErrFailedToParseQuery = errors.New("failed to parse query parameters")

	// ErrFailedToParsePath indicates path parameter extraction or conversion failed.
	ErrFailedToParsePath = errors.New("failed to parse path parameters")

	// ErrMissingContentType indicates the request lacks a Content-Type header
	// when one is required for parsing.
	ErrMissingContentType = errors.New("missing content type")

	// ErrBinderNotApplicable indicates the binder cannot process the request
	// (e.g., wrong HTTP method or missing required data).
	ErrBinderNotApplicable = errors.New("binder not applicable for this request")
)

Error variables define common binding failures that can occur during request processing.

Functions

This section is empty.

Types

type Binder

type Binder func(r *http.Request, v any) error

Binder represents a function that binds HTTP request data to a Go value. It provides a unified interface for extracting and mapping data from various parts of an HTTP request (form data, JSON body, path parameters, query parameters) into strongly-typed Go structures.

func Form

func Form() Binder

Form creates a unified binder for both form data and file uploads. It handles application/x-www-form-urlencoded and multipart/form-data content types.

Supported struct tags:

  • `form:"name"` - binds to form field "name"
  • `form:"-"` - skips the field
  • `file:"name"` - binds to uploaded file "name"
  • `file:"-"` - skips the field

Supported types for form fields:

  • Basic types: string, int, int64, uint, uint64, float32, float64, bool
  • Slices of basic types for multi-value fields
  • Pointers for optional fields

Supported types for file fields:

  • *multipart.FileHeader - single file
  • []*multipart.FileHeader - multiple files

Example:

type UploadRequest struct {
	Title    string                  `form:"title"`
	Category string                  `form:"category"`
	Tags     []string                `form:"tags"`     // Multi-value field
	Avatar   *multipart.FileHeader   `file:"avatar"`   // Optional file
	Gallery  []*multipart.FileHeader `file:"gallery"`  // Multiple files
	Internal string                  `form:"-"`        // Skipped
}

func uploadHandler(w http.ResponseWriter, r *http.Request) {
	var req UploadRequest
	if err := binder.Form()(r, &req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	if req.Avatar != nil {
		file, err := req.Avatar.Open()
		if err != nil {
			http.Error(w, "Failed to open file", http.StatusInternalServerError)
			return
		}
		defer file.Close()
		// Process file...
	}
}

http.HandleFunc("/upload", uploadHandler)

func JSON

func JSON() Binder

JSON creates a JSON binder function.

Example:

func createUserHandler(w http.ResponseWriter, r *http.Request) {
	var req CreateUserRequest
	if err := binder.JSON()(r, &req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	// req is populated from JSON body
	// Process req and return response...
}

http.HandleFunc("/users", createUserHandler)

func Path

func Path(extractor func(r *http.Request, fieldName string) string) Binder

Path creates a path parameter binder function using the provided extractor. The extractor function is called for each struct field to get its path parameter value.

It supports struct tags for custom parameter names:

  • `path:"name"` - binds to path parameter "name"
  • `path:"-"` - skips the field

Supported types:

  • Basic types: string, int, int64, uint, uint64, float32, float64, bool
  • Pointers for optional fields

Example with chi router:

type ProfileRequest struct {
	UserID   string `path:"id"`
	Username string `path:"username"`
	Name     string `form:"name"`     // From form data
	Expand   bool   `query:"expand"`  // From query string
}

func profileHandler(w http.ResponseWriter, r *http.Request) {
	var req ProfileRequest

	// Apply multiple binders in sequence
	binders := []func(*http.Request, any) error{
		binder.Path(chi.URLParam),
		binder.Query(),
		binder.Form(),
	}

	for _, bind := range binders {
		if err := bind(r, &req); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
	}

	// req.UserID and req.Username are populated from path
	// req.Name is populated from form data
	// req.Expand is populated from query string
	// Process req and return response...
}

r := chi.NewRouter()
r.Get("/users/{id}/profile/{username}", profileHandler)

Example with gorilla/mux:

muxExtractor := func(r *http.Request, fieldName string) string {
	vars := mux.Vars(r)
	return vars[fieldName]
}

router := mux.NewRouter()
router.HandleFunc("/users/{id}/profile/{username}", func(w http.ResponseWriter, r *http.Request) {
	var req ProfileRequest
	if err := binder.Path(muxExtractor)(r, &req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	// Process req...
})

func Query

func Query() Binder

Query creates a query parameter binder function.

It supports struct tags for custom parameter names:

  • `query:"name"` - binds to query parameter "name"
  • `query:"-"` - skips the field
  • `query:"name,omitempty"` - same as query:"name" for parsing

Supported types:

  • Basic types: string, int, int64, uint, uint64, float32, float64, bool
  • Slices of basic types for multi-value parameters
  • Pointers for optional fields

Example:

type SearchRequest struct {
	Query    string   `query:"q"`
	Page     int      `query:"page"`
	PageSize int      `query:"page_size"`
	Tags     []string `query:"tags"`     // ?tags=go&tags=web or ?tags=go,web
	Active   *bool    `query:"active"`   // Optional
	Internal string   `query:"-"`        // Skipped
}

func searchHandler(w http.ResponseWriter, r *http.Request) {
	var req SearchRequest
	if err := binder.Query()(r, &req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	// req is populated from query parameters
	// Process req and return response...
}

http.HandleFunc("/search", searchHandler)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL