├── .gitignore ├── go.mod ├── go.sum ├── .github └── workflows │ └── test.yml ├── LICENSE ├── README.md ├── decoder.go ├── content_type.go ├── render.go └── responder.go /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-chi/render 2 | 3 | go 1.16 4 | 5 | require github.com/ajg/form v1.5.1 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= 2 | github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= 3 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | - main 9 | pull_request: 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-go@v2 16 | - uses: actions/cache@v2 17 | with: 18 | path: ~/go/pkg/mod 19 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 20 | restore-keys: | 21 | ${{ runner.os }}-go- 22 | - run: go mod download 23 | - name: Run tests 24 | run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./... 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-Present https://github.com/go-chi authors 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # render 2 | 3 | ![tests](https://github.com/go-chi/render/actions/workflows/test.yml/badge.svg) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/go-chi/render)](https://goreportcard.com/report/github.com/go-chi/render) 5 | [![Go Reference](https://pkg.go.dev/badge/github.com/go-chi/render.svg)](https://pkg.go.dev/github.com/go-chi/render) 6 | 7 | The `render` package helps manage HTTP request / response payloads. 8 | 9 | Every well-designed, robust and maintainable Web Service / REST API also needs 10 | well-*defined* request and response payloads. Together with the endpoint handlers, 11 | the request and response payloads make up the contract between your server and the 12 | clients calling on it. 13 | 14 | Typically in a REST API application, you will have your data models (objects/structs) 15 | that hold lower-level runtime application state, and at times you need to assemble, 16 | decorate, hide or transform the representation before responding to a client. That 17 | server output (response payload) structure, is also likely the input structure to 18 | another handler on the server. 19 | 20 | This is where `render` comes in - offering a few simple helpers and interfaces to 21 | provide a simple pattern for managing payload encoding and decoding. 22 | 23 | We've also combined it with some helpers for responding to content types and parsing 24 | request bodies. Please have a look at the [rest](https://github.com/go-chi/chi/blob/master/_examples/rest/main.go) 25 | example which uses the latest chi/render sub-pkg. 26 | 27 | All feedback is welcome, thank you! 28 | -------------------------------------------------------------------------------- /decoder.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "encoding/json" 5 | "encoding/xml" 6 | "errors" 7 | "io" 8 | "net/http" 9 | 10 | "github.com/ajg/form" 11 | ) 12 | 13 | // Decode is a package-level variable set to our default Decoder. We do this 14 | // because it allows you to set render.Decode to another function with the 15 | // same function signature, while also utilizing the render.Decoder() function 16 | // itself. Effectively, allowing you to easily add your own logic to the package 17 | // defaults. For example, maybe you want to impose a limit on the number of 18 | // bytes allowed to be read from the request body. 19 | var Decode = DefaultDecoder 20 | 21 | // DefaultDecoder detects the correct decoder for use on an HTTP request and 22 | // marshals into a given interface. 23 | func DefaultDecoder(r *http.Request, v interface{}) error { 24 | var err error 25 | 26 | switch GetRequestContentType(r) { 27 | case ContentTypeJSON: 28 | err = DecodeJSON(r.Body, v) 29 | case ContentTypeXML: 30 | err = DecodeXML(r.Body, v) 31 | case ContentTypeForm: 32 | err = DecodeForm(r.Body, v) 33 | default: 34 | err = errors.New("render: unable to automatically decode the request content type") 35 | } 36 | 37 | return err 38 | } 39 | 40 | // DecodeJSON decodes a given reader into an interface using the json decoder. 41 | func DecodeJSON(r io.Reader, v interface{}) error { 42 | defer io.Copy(io.Discard, r) //nolint:errcheck 43 | return json.NewDecoder(r).Decode(v) 44 | } 45 | 46 | // DecodeXML decodes a given reader into an interface using the xml decoder. 47 | func DecodeXML(r io.Reader, v interface{}) error { 48 | defer io.Copy(io.Discard, r) //nolint:errcheck 49 | return xml.NewDecoder(r).Decode(v) 50 | } 51 | 52 | // DecodeForm decodes a given reader into an interface using the form decoder. 53 | func DecodeForm(r io.Reader, v interface{}) error { 54 | decoder := form.NewDecoder(r) //nolint:errcheck 55 | return decoder.Decode(v) 56 | } 57 | -------------------------------------------------------------------------------- /content_type.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | ContentTypeCtxKey = &contextKey{"ContentType"} 11 | ) 12 | 13 | // ContentType is an enumeration of common HTTP content types. 14 | type ContentType int 15 | 16 | // ContentTypes handled by this package. 17 | const ( 18 | ContentTypeUnknown ContentType = iota 19 | ContentTypePlainText 20 | ContentTypeHTML 21 | ContentTypeJSON 22 | ContentTypeXML 23 | ContentTypeForm 24 | ContentTypeEventStream 25 | ) 26 | 27 | func GetContentType(s string) ContentType { 28 | s = strings.TrimSpace(strings.Split(s, ";")[0]) 29 | switch s { 30 | case "text/plain": 31 | return ContentTypePlainText 32 | case "text/html", "application/xhtml+xml": 33 | return ContentTypeHTML 34 | case "application/json", "text/javascript": 35 | return ContentTypeJSON 36 | case "text/xml", "application/xml": 37 | return ContentTypeXML 38 | case "application/x-www-form-urlencoded": 39 | return ContentTypeForm 40 | case "text/event-stream": 41 | return ContentTypeEventStream 42 | default: 43 | return ContentTypeUnknown 44 | } 45 | } 46 | 47 | // SetContentType is a middleware that forces response Content-Type. 48 | func SetContentType(contentType ContentType) func(next http.Handler) http.Handler { 49 | return func(next http.Handler) http.Handler { 50 | fn := func(w http.ResponseWriter, r *http.Request) { 51 | r = r.WithContext(context.WithValue(r.Context(), ContentTypeCtxKey, contentType)) 52 | next.ServeHTTP(w, r) 53 | } 54 | return http.HandlerFunc(fn) 55 | } 56 | } 57 | 58 | // GetRequestContentType is a helper function that returns ContentType based on 59 | // context or request headers. 60 | func GetRequestContentType(r *http.Request) ContentType { 61 | if contentType, ok := r.Context().Value(ContentTypeCtxKey).(ContentType); ok { 62 | return contentType 63 | } 64 | return GetContentType(r.Header.Get("Content-Type")) 65 | } 66 | 67 | func GetAcceptedContentType(r *http.Request) ContentType { 68 | if contentType, ok := r.Context().Value(ContentTypeCtxKey).(ContentType); ok { 69 | return contentType 70 | } 71 | 72 | var contentType ContentType 73 | 74 | // Parse request Accept header. 75 | fields := strings.Split(r.Header.Get("Accept"), ",") 76 | if len(fields) > 0 { 77 | contentType = GetContentType(strings.TrimSpace(fields[0])) 78 | } 79 | 80 | if contentType == ContentTypeUnknown { 81 | contentType = ContentTypePlainText 82 | } 83 | return contentType 84 | } 85 | -------------------------------------------------------------------------------- /render.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | ) 7 | 8 | // Renderer interface for managing response payloads. 9 | type Renderer interface { 10 | Render(w http.ResponseWriter, r *http.Request) error 11 | } 12 | 13 | // Binder interface for managing request payloads. 14 | type Binder interface { 15 | Bind(r *http.Request) error 16 | } 17 | 18 | // Bind decodes a request body and executes the Binder method of the 19 | // payload structure. 20 | func Bind(r *http.Request, v Binder) error { 21 | if err := Decode(r, v); err != nil { 22 | return err 23 | } 24 | return binder(r, v) 25 | } 26 | 27 | // Render renders a single payload and respond to the client request. 28 | func Render(w http.ResponseWriter, r *http.Request, v Renderer) error { 29 | if err := renderer(w, r, v); err != nil { 30 | return err 31 | } 32 | Respond(w, r, v) 33 | return nil 34 | } 35 | 36 | // RenderList renders a slice of payloads and responds to the client request. 37 | func RenderList(w http.ResponseWriter, r *http.Request, l []Renderer) error { 38 | for _, v := range l { 39 | if err := renderer(w, r, v); err != nil { 40 | return err 41 | } 42 | } 43 | Respond(w, r, l) 44 | return nil 45 | } 46 | 47 | func isNil(f reflect.Value) bool { 48 | switch f.Kind() { 49 | case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: 50 | return f.IsNil() 51 | default: 52 | return false 53 | } 54 | } 55 | 56 | // Executed top-down 57 | func renderer(w http.ResponseWriter, r *http.Request, v Renderer) error { 58 | rv := reflect.ValueOf(v) 59 | if rv.Kind() == reflect.Ptr { 60 | rv = rv.Elem() 61 | } 62 | 63 | // We call it top-down. 64 | if err := v.Render(w, r); err != nil { 65 | return err 66 | } 67 | 68 | // We're done if the Renderer isn't a struct object 69 | if rv.Kind() != reflect.Struct { 70 | return nil 71 | } 72 | 73 | // For structs, we call Render on each field that implements Renderer 74 | for i := 0; i < rv.NumField(); i++ { 75 | f := rv.Field(i) 76 | if f.Type().Implements(rendererType) { 77 | 78 | if isNil(f) { 79 | continue 80 | } 81 | 82 | fv := f.Interface().(Renderer) 83 | if err := renderer(w, r, fv); err != nil { 84 | return err 85 | } 86 | 87 | } 88 | } 89 | 90 | return nil 91 | } 92 | 93 | // Executed bottom-up 94 | func binder(r *http.Request, v Binder) error { 95 | rv := reflect.ValueOf(v) 96 | if rv.Kind() == reflect.Ptr { 97 | rv = rv.Elem() 98 | } 99 | 100 | // Call Binder on non-struct types right away 101 | if rv.Kind() != reflect.Struct { 102 | return v.Bind(r) 103 | } 104 | 105 | // For structs, we call Bind on each field that implements Binder 106 | for i := 0; i < rv.NumField(); i++ { 107 | f := rv.Field(i) 108 | if f.Type().Implements(binderType) { 109 | 110 | if isNil(f) { 111 | continue 112 | } 113 | 114 | fv := f.Interface().(Binder) 115 | if err := binder(r, fv); err != nil { 116 | return err 117 | } 118 | } 119 | } 120 | 121 | // We call it bottom-up 122 | if err := v.Bind(r); err != nil { 123 | return err 124 | } 125 | 126 | return nil 127 | } 128 | 129 | var ( 130 | rendererType = reflect.TypeOf(new(Renderer)).Elem() 131 | binderType = reflect.TypeOf(new(Binder)).Elem() 132 | ) 133 | 134 | // contextKey is a value for use with context.WithValue. It's used as 135 | // a pointer so it fits in an interface{} without allocation. This technique 136 | // for defining context keys was copied from Go 1.7's new use of context in net/http. 137 | type contextKey struct { 138 | name string 139 | } 140 | 141 | func (k *contextKey) String() string { 142 | return "chi render context value " + k.name 143 | } 144 | -------------------------------------------------------------------------------- /responder.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "encoding/xml" 8 | "fmt" 9 | "net/http" 10 | "reflect" 11 | ) 12 | 13 | // M is a convenience alias for quickly building a map structure that is going 14 | // out to a responder. Just a short-hand. 15 | type M map[string]interface{} 16 | 17 | // Respond is a package-level variable set to our default Responder. We do this 18 | // because it allows you to set render.Respond to another function with the 19 | // same function signature, while also utilizing the render.Responder() function 20 | // itself. Effectively, allowing you to easily add your own logic to the package 21 | // defaults. For example, maybe you want to test if v is an error and respond 22 | // differently, or log something before you respond. 23 | var Respond = DefaultResponder 24 | 25 | // StatusCtxKey is a context key to record a future HTTP response status code. 26 | var StatusCtxKey = &contextKey{"Status"} 27 | 28 | // Status sets a HTTP response status code hint into request context at any point 29 | // during the request life-cycle. Before the Responder sends its response header 30 | // it will check the StatusCtxKey 31 | func Status(r *http.Request, status int) { 32 | *r = *r.WithContext(context.WithValue(r.Context(), StatusCtxKey, status)) 33 | } 34 | 35 | // Respond handles streaming JSON and XML responses, automatically setting the 36 | // Content-Type based on request headers. It will default to a JSON response. 37 | func DefaultResponder(w http.ResponseWriter, r *http.Request, v interface{}) { 38 | if v != nil { 39 | switch reflect.TypeOf(v).Kind() { 40 | case reflect.Chan: 41 | switch GetAcceptedContentType(r) { 42 | case ContentTypeEventStream: 43 | channelEventStream(w, r, v) 44 | return 45 | default: 46 | v = channelIntoSlice(w, r, v) 47 | } 48 | } 49 | } 50 | 51 | // Format response based on request Accept header. 52 | switch GetAcceptedContentType(r) { 53 | case ContentTypeJSON: 54 | JSON(w, r, v) 55 | case ContentTypeXML: 56 | XML(w, r, v) 57 | default: 58 | JSON(w, r, v) 59 | } 60 | } 61 | 62 | // PlainText writes a string to the response, setting the Content-Type as 63 | // text/plain. 64 | func PlainText(w http.ResponseWriter, r *http.Request, v string) { 65 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") 66 | if status, ok := r.Context().Value(StatusCtxKey).(int); ok { 67 | w.WriteHeader(status) 68 | } 69 | w.Write([]byte(v)) //nolint:errcheck 70 | } 71 | 72 | // Data writes raw bytes to the response, setting the Content-Type as 73 | // application/octet-stream. 74 | func Data(w http.ResponseWriter, r *http.Request, v []byte) { 75 | w.Header().Set("Content-Type", "application/octet-stream") 76 | if status, ok := r.Context().Value(StatusCtxKey).(int); ok { 77 | w.WriteHeader(status) 78 | } 79 | w.Write(v) //nolint:errcheck 80 | } 81 | 82 | // HTML writes a string to the response, setting the Content-Type as text/html. 83 | func HTML(w http.ResponseWriter, r *http.Request, v string) { 84 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 85 | if status, ok := r.Context().Value(StatusCtxKey).(int); ok { 86 | w.WriteHeader(status) 87 | } 88 | w.Write([]byte(v)) //nolint:errcheck 89 | } 90 | 91 | // JSON marshals 'v' to JSON, automatically escaping HTML and setting the 92 | // Content-Type as application/json. 93 | func JSON(w http.ResponseWriter, r *http.Request, v interface{}) { 94 | buf := &bytes.Buffer{} 95 | enc := json.NewEncoder(buf) 96 | enc.SetEscapeHTML(true) 97 | if err := enc.Encode(v); err != nil { 98 | http.Error(w, err.Error(), http.StatusInternalServerError) 99 | return 100 | } 101 | 102 | w.Header().Set("Content-Type", "application/json") 103 | if status, ok := r.Context().Value(StatusCtxKey).(int); ok { 104 | w.WriteHeader(status) 105 | } 106 | w.Write(buf.Bytes()) //nolint:errcheck 107 | } 108 | 109 | // XML marshals 'v' to XML, setting the Content-Type as application/xml. It 110 | // will automatically prepend a generic XML header (see encoding/xml.Header) if 111 | // one is not found in the first 100 bytes of 'v'. 112 | func XML(w http.ResponseWriter, r *http.Request, v interface{}) { 113 | b, err := xml.Marshal(v) 114 | if err != nil { 115 | http.Error(w, err.Error(), http.StatusInternalServerError) 116 | return 117 | } 118 | 119 | w.Header().Set("Content-Type", "application/xml; charset=utf-8") 120 | if status, ok := r.Context().Value(StatusCtxKey).(int); ok { 121 | w.WriteHeader(status) 122 | } 123 | 124 | // Try to find 100 { 127 | findHeaderUntil = 100 128 | } 129 | if !bytes.Contains(b[:findHeaderUntil], []byte("