mid

package module
Version: v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Mar 2, 2020 License: MIT Imports: 19 Imported by: 1

README

Mid Go Report Card GoDoc

A net/http compatible middleware for protecting, validating, and automatically hydrating handlers with user input from JSON or multipart form bodies, route parameters, and query string values.

Mid is a tiny library that saves time and makes code easier to read by removing the need to type input decoding and validation checks for every handler.

Imagine a simpler, automatic gRPC for REST API's.

Compatible with:

Usage

For all user input you must define a struct that contains the expected fields and rules. For example, imagine we are saving a blog comment. We might get the blog post id from the URL path and the comment fields from a JSON body. We can use struct tags to specify the rules and location of the data we are expecting.

(We use asaskevich/govalidator for validation.)

type NewComment struct {
	PostID  int    `valid:"required" param:"post_id"`
	Comment string `valid:"required,stringlength(100|1000)"`
	Email   string `valid:"required,email"`
}

Note: the "param" struct tag specifies a route parameter named "post_id" holds the value

Next we write a http.HandlerFunc with one extra field: a reference to the populated NewComment:

handler := func(w http.ResponseWriter, r *http.Request, comment NewComment) error {
	// we now have access to populated fields like "comment.Email"
	return nil
}

We then wire this up to our router and are ready to start accepting input:

// julienschmidt/httprouter
router.Handler("POST", "/post/:post_id/comment", mid.Hydrate(handler))

// gorilla/mux
router.HandleFunc("/post/{post_id}/comment", mid.Hydrate(handler)).Methods("POST")

// net/http
http.Handle("/post/comment", mid.Hydrate(handler))

Note: the last net/http mux example does not provide "route parameters" so it won't actually pass the validation due to the PostID struct tag "param" not existing.

At this point we can rest assured that our handler will never be called unless input matching our exact validation rules is provided by the client. If the client passes invalid data, then a JSON response object is returned specifying the issues.

    {
        error: string
        fields: map[string]string
	}

We follow a simpler version of the Google style for JSON API responses:

"A JSON response should contain either a data object or an error object, but not both. If both data and error are present, the error object takes precedence." - https://google.github.io/styleguide/jsoncstyleguide.xml

Responding to clients

What if you want to return data to the client? You can specify any type and it will be returned to the client packaged in a standard JSON wrapper.

handler := func(w http.ResponseWriter, r *http.Request, newComment NewComment) (Comment, error) {
	comment, err := commentService.Save(newComment)
	return comment, err
}

This will result in the following HTTP 200 response:

    {
        data: { ...Comment fields here... }
	}
See the examples for more.

Security Notes

HTTP request bodies can be any size, it is recommended you limit them using the mid.MaxBodySize() middleware to prevent attacks.

A large number of TCP requests can cause multiple issues including degraded performance and your OS terminating your Go service because of high memory usage. A Denial-of-service attack is one example. The mid.RequestThrottler exists to help keep a cap on how many requests your application instance will serve concurrently.

It is recommended you create a helper function that wraps both these and the Hydration.

// Close connection with a 503 error if not handled within 3 seconds
throttler := mid.RequestThrottler(20, 3*time.Second)

wrapper := func(function interface{}) http.Handler {
	return throttler(mid.MaxBodySize(mid.Hydrate(function), 1024*1024))
}

...

router.Handler("POST", "/:user/profile", wrapper(controller.SaveProfile))

Supported Validations

https://github.com/asaskevich/govalidator#list-of-functions

Benchmarks

$ go test --bench=. --benchmem
goos: darwin
goarch: amd64
pkg: github.com/Xeoncross/mid/benchmarks
BenchmarkEcho-8       	  200000	      8727 ns/op	    3731 B/op	      39 allocs/op
BenchmarkGongular-8   	  100000	     18266 ns/op	    6511 B/op	      76 allocs/op
BenchmarkMid-8        	  100000	     12607 ns/op	    7174 B/op	     120 allocs/op
BenchmarkVanilla-8    	 2000000	       849 ns/op	     288 B/op	      17 allocs/op

Gongular is slower in these benchmarks because 1) it's a full framework with extra mux wrapping code and 2) calculations and allocs that go into handling dependency injection in a way mid is able to avoid completely by keeping the handler separate from the binding object.

Echo is the fastest, but also requires writing the most code because it is less feature-complete. Unlike the other two, the echo benchmark does not include things like URL parameter binding or standard response handling.

HTML Templates

Please see https://github.com/Xeoncross/got - a minimal wrapper to improve Go html/template usage by providing pre-computed inheritance with no loss of speed.

Documentation

Index

Constants

View Source
const (
	// TagQuery is the field tag for looking up Query Parameters
	TagQuery = "query"
	// TagParam is the field tag for looking up URL Parameters
	TagParam = "param"
)

Variables

View Source
var ValidationErrorMessage = "Invalid Request"

ValidationErrorMessage sent to client on validation fail

Functions

func DotFileHidingFileSystem added in v0.5.0

func DotFileHidingFileSystem(fs http.FileSystem) http.FileSystem

DotFileHidingFileSystem is an http.FileSystem that hides "dot files" from being served.

http.Handle("/", http.FileServer(mid.DotFileHidingFileSystem(http.Dir("/dist"))))

func FileSystem added in v0.5.0

func FileSystem(fs http.FileSystem) http.FileSystem

FileSystem wrapper to send index.html to all non-existant paths and hide dot files

func Hydrate added in v0.4.0

func Hydrate(function interface{}) http.HandlerFunc

Hydrate and validate a http.Handler with input from HTTP GET/POST requests

func InterruptContext added in v0.5.0

func InterruptContext() context.Context

InterruptContext listing for os.Signal (i.e. CTRL+C) to cancel a context and end a server/daemon gracefully

func JSON

func JSON(w http.ResponseWriter, status int, data interface{}) (int, error)

JSON safely written to ResponseWriter by using a buffer to prevent partial sends

func ListenWithContext added in v0.6.0

func ListenWithContext(ctx context.Context, server *http.Server, healthy *int32) error

ListenWithContext starts the server while watching context to trigger graceful shutdown; Use with mid.InterruptContext() to control application lifecycle

func MaxBodySize added in v0.2.0

func MaxBodySize(next http.Handler, size int64) http.Handler

MaxBodySize limits the size of the request body to avoid a DOS with a large JSON structure Go does this internally for multipart bodies: https://golang.org/src/net/http/request.go#L1136

func MustQueryParams added in v0.5.0

func MustQueryParams(h http.Handler, params ...string) http.Handler

MustQueryParams circit breaker middleware only forwards requests which have the specified query params set

func RequestCounter added in v0.2.0

func RequestCounter(duration time.Duration, callback func(uint64, chan struct{})) func(http.Handler) http.Handler

RequestCounter is useful for counting requests for logging

func RequestThrottler added in v0.4.0

func RequestThrottler(concurrentRequests int, timeout time.Duration) func(http.Handler) http.Handler

RequestThrottler creates a re-usable limiter for multiple http.Handlers If the server is too busy to handle the request within the timeout, then a "503 Service Unavailable" status will be sent and the connection closed.

func SpaFileSystem added in v0.5.0

func SpaFileSystem(fs http.FileSystem) http.FileSystem

SpaFileSystem wraps a http.FileSystem and returns index.html for all missing paths while blocking directory browsing. Look into expanding or replacing this with: https://github.com/aaronellington/gospa https://golang.org/src/net/http/fs.go?s=20651:20691#L705

http.Handle("/", http.FileServer(mid.SpaFileSystem(http.Dir("/dist"))))

Types

type JSONResponse added in v0.4.0

type JSONResponse struct {
	Data   interface{}       `json:"data,omitempty"`
	Error  string            `json:"error,omitempty"`
	Fields map[string]string `json:"fields,omitempty"`
}

JSONResponse for validation errors or service responses

type ParseError added in v0.4.0

type ParseError struct {
	Place     string
	FieldName string `json:",omitempty"`
	Reason    string
}

ParseError occurs whenever the field cannot be parsed, i.e. type mismatch

func (ParseError) Error added in v0.4.0

func (p ParseError) Error() string

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
t or T : Toggle theme light dark auto
y or Y : Canonical URL