-
-
Save xeoncross/b10ad4998250ce4ee4350176f528c727 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2016 Noel Cower. All rights reserved. | |
// Use of this source code is governed by a BSD-style license that can be found | |
// at the end of the file. | |
// This file is intended to demonstrate writing a general-purpose HTTP handler | |
// where the input and output are known to be JSON, follow a particular error | |
// convention, and so on. It falls apart in particular with GET requests where | |
// request bodies are uncommon, at best. | |
// | |
// The responder handlers mimic gRPC's error code/response format, but does not | |
// do much else in that respect (e.g., no trailing headers or custom headers, | |
// HTTP/2 just a property of the client and server, etc.). | |
// | |
// Basically, this is meant to show, more than anything else, how you can | |
// simplify some boilerplate for HTTP handlers by making careful use of | |
// interfaces. | |
package main | |
import ( | |
"context" | |
"encoding/json" | |
"fmt" | |
"log" | |
"net/http" | |
"net/http/httptest" | |
"net/http/httputil" | |
"path" | |
"reflect" | |
"strconv" | |
"strings" | |
) | |
// ErrorResponse is the type of error message sent back when a Responder returns an error. If the | |
// Responder's error isn't an *ErrorResponse, one is allocated with a Code of Unknown and the | |
// error's description. | |
type ErrorResponse struct { | |
Code uint `json:"code"` | |
Desc string `json:"desc"` | |
} | |
func (e *ErrorResponse) Error() string { | |
if e == nil { | |
return "<nil>" | |
} | |
return fmt.Sprint(e.Code, ": ", e.Desc) | |
} | |
func mkErrResponse(err error) *ErrorResponse { | |
if err == nil { | |
return nil | |
} | |
er, _ := err.(*ErrorResponse) | |
if er != nil { | |
return er | |
} | |
return &ErrorResponse{Unknown, err.Error()} | |
} | |
// ErrorCode returns the error code of an *ErrorResponse, otherwise Unknown. | |
func ErrorCode(err error) uint { | |
if e, _ := err.(*ErrorResponse); e != nil { | |
return e.Code | |
} | |
return Unknown | |
} | |
func httpErrorCode(err error) int { | |
switch ErrorCode(err) { | |
case InvalidArgument, BadRequest: | |
return http.StatusBadRequest | |
case NotFound: | |
return http.StatusNotFound | |
case Unimplemented: | |
return http.StatusNotImplemented | |
default: | |
return http.StatusInternalServerError | |
} | |
} | |
// Error codes for ServeJSON to return an ErrorResponse with. | |
const ( | |
InvalidArgument = 0 | |
Unknown = 1 | |
NotFound = 2 | |
Unimplemented = 3 | |
BadRequest = 4 | |
Invalid = 31 | |
// Etc. -- mimicking gRPC here | |
) | |
// Protocols | |
// A NewResponder returns a new responder for a given *http.Request. | |
type NewResponder interface { | |
NewResponder(*http.Request) (Responder, error) | |
} | |
// NewResponderFunc is a function type NewResponder. This is a convenience type. | |
type NewResponderFunc func(*http.Request) (Responder, error) | |
func (fn NewResponderFunc) NewResponder(req *http.Request) (Responder, error) { | |
return fn(req) | |
} | |
// A Responder receives an HTTP request and returns an HTTP status code and a message to be sent, | |
// encoded as JSON, in response to the request. If an error is returned, it is converted to an | |
// *ErrorResponse if not already one and the HTTP status code is determined by the *ErrorResponse's | |
// Code field (which defaults to Unknown, producing an internal server error code). | |
// | |
// A Responder is typically an instance of an input request to be fulfilled. | |
type Responder interface { | |
ServeJSON(*http.Request) (httpStatus int, msg interface{}, err error) | |
} | |
// ResponderMessage is an optional interface that a Responder may implement to provide an | |
// alternative body to unmarshal incoming JSON into. | |
type ResponderMessage interface { | |
Message() interface{} | |
} | |
// Handlers | |
func allocResponder(msg Responder) Responder { | |
t := reflect.TypeOf(msg) | |
if t.Kind() == reflect.Ptr { | |
t = t.Elem() | |
} | |
return reflect.New(t).Interface().(Responder) | |
} | |
// JSONResponderByPrototype creates a JSONResponder handler that simply creates a NewResponder that | |
// allocates a new instance of msg each time. The new msg instance is zeroed. | |
func JSONResponderByPrototype(msg Responder) http.Handler { | |
if ra, ok := msg.(NewResponder); ok { | |
return JSONResponder(ra) | |
} | |
return JSONResponderFunc(func(*http.Request) (Responder, error) { | |
return allocResponder(msg), nil | |
}) | |
} | |
// JSONResponderFunc is a convenience function for calling JSONResponder with a NewResponderFunc. | |
func JSONResponderFunc(fn NewResponderFunc) http.Handler { | |
return JSONResponder(fn) | |
} | |
type responderKey string | |
func (r *responderKey) GoString() string { return string(*r) } | |
var rawMessageKey = new(responderKey) | |
func init() { *rawMessageKey = "responder:msg" } | |
// InputMessage returns the message used to store an incoming request body. This can be useful if | |
// a Responder implements ResponderMessage but does not store it. | |
func InputMessage(ctx context.Context) interface{} { | |
return ctx.Value(rawMessageKey) | |
} | |
func withInputMessage(ctx context.Context, msg interface{}) context.Context { | |
return context.WithValue(ctx, rawMessageKey, msg) | |
} | |
type jsonResponseHandler struct { | |
gen NewResponder | |
} | |
func (h *jsonResponseHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | |
var ( | |
body, resp interface{} | |
code int | |
err error | |
) | |
// Insert correct content type handling here and such or somewhere downstream | |
responder, err := h.gen.NewResponder(req) | |
body = responder | |
if err != nil { | |
goto replyWithError | |
} | |
if rm, ok := body.(ResponderMessage); ok { | |
body = rm.Message() | |
} | |
if body == nil { | |
err = &ErrorResponse{Unimplemented, "not implemented"} | |
goto replyWithError | |
} | |
if err = json.NewDecoder(req.Body).Decode(body); err != nil { | |
err = &ErrorResponse{BadRequest, err.Error()} | |
goto replyWithError | |
} | |
req = req.WithContext(withInputMessage(req.Context(), body)) | |
code, resp, err = responder.ServeJSON(req) | |
replyWithError: | |
if err != nil { | |
code = httpErrorCode(err) | |
err = mkErrResponse(err) | |
resp = err | |
} else if code == 0 { | |
code = http.StatusOK | |
} | |
respBody, err := json.Marshal(resp) | |
if err != nil { | |
log.Print("error encoding %T response (from %T): %v", resp, responder, err) | |
w.Header().Set("Content-Length", "0") | |
w.WriteHeader(http.StatusInternalServerError) | |
return | |
} | |
w.Header().Set("Content-Type", "application/json") | |
w.Header().Set("Content-Length", strconv.Itoa(len(respBody))) | |
// TODO: Handle Accept-Encoding header here or downstream (i.e., buffer and encode | |
// later) | |
w.WriteHeader(code) | |
if _, err := w.Write(respBody); err != nil { | |
// Log error? Probably connection died. | |
log.Print("error writing %T response (from %T): %v", resp, responder, err) | |
} | |
} | |
// JSONResponder creates an http.Handler that allocates a new message, decodes a request body into | |
// it, and serves a JSON response. It modifies the Content-Type and Content-Length headers of the | |
// response and, in error cases, may write an HTTP status code other than the one returned for each | |
// responder message. | |
func JSONResponder(gen NewResponder) http.Handler { | |
return &jsonResponseHandler{gen} | |
} | |
// Implementations of protocols | |
type NewPersonResponse struct { | |
RequestId string `json:"requestId"` | |
Request *NewPersonRequest `json:"request"` | |
} | |
type NewPersonRequest struct { | |
Name string `json:"name"` | |
Age int `json:"age"` | |
} | |
func (req *NewPersonRequest) ServeJSON(*http.Request) (code int, msg interface{}, err error) { | |
return http.StatusCreated, &NewPersonResponse{RequestId: "request-id", Request: req}, nil | |
} | |
type ReturnError uint | |
func (r ReturnError) NewResponder(*http.Request) (Responder, error) { return r, nil } | |
func (ReturnError) Message() interface{} { return new(json.RawMessage) } | |
func (r ReturnError) ServeJSON(req *http.Request) (int, interface{}, error) { | |
enc, _ := json.Marshal(InputMessage(req.Context())) | |
log.Print("Request: ", string(enc)) | |
return 0, nil, &ErrorResponse{uint(r), "error"} | |
} | |
type YieldError uint | |
func (r *YieldError) ServeJSON(*http.Request) (int, interface{}, error) { | |
return http.StatusOK, &ErrorResponse{Code: uint(*r), Desc: "error code"}, nil | |
} | |
func main() { | |
http.Handle("/person", JSONResponderByPrototype(new(NewPersonRequest))) | |
http.Handle("/error", JSONResponder(ReturnError(Unimplemented))) | |
http.Handle("/error2", JSONResponderByPrototype(new(YieldError))) | |
type testCase struct { | |
Path string | |
Body string | |
} | |
cases := []testCase{ | |
{ | |
Path: "/person", | |
Body: `{"name": "Johnny", "age": 637}`, | |
}, | |
{ | |
Path: "/error", | |
Body: `{"arbitrary": [1, 2, 3, "bepis"], "really now": true}`, | |
}, | |
{ | |
Path: "/error2", | |
Body: `0`, | |
}, | |
{ | |
Path: "/error2", | |
Body: `1`, | |
}, | |
{ | |
Path: "/error2", | |
Body: `2`, | |
}, | |
{ | |
Path: "/error2", | |
Body: `3`, | |
}, | |
} | |
for i, c := range cases { | |
// Committing a sin here for the sake of brevity: errors are not checked in this loop. | |
if i > 0 { | |
fmt.Print("\n\n\n") | |
} | |
fmt.Print(i+1, ":\n") | |
req, _ := http.NewRequest("GET", "http://127.0.0.1:8080/", strings.NewReader(c.Body)) | |
req.URL.Path = path.Join(req.URL.Path, c.Path) | |
reqBuf, _ := httputil.DumpRequest(req, true) | |
fmt.Print("< ", strings.Replace(string(reqBuf), "\n", "\n< ", -1), "\n\n") | |
rec := httptest.NewRecorder() | |
http.DefaultServeMux.ServeHTTP(rec, req) | |
resp := rec.Result() | |
respBuf, _ := httputil.DumpResponse(resp, true) | |
fmt.Print("> ", strings.Replace(string(respBuf), "\n", "\n> ", -1), "\n") | |
} | |
} | |
// Copyright (c) 2016, Noel Cower | |
// All rights reserved. | |
// | |
// Redistribution and use in source and binary forms, with or without | |
// modification, are permitted provided that the following conditions are met: | |
// | |
// 1. Redistributions of source code must retain the above copyright notice, | |
// this list of conditions and the following disclaimer. | |
// | |
// 2. Redistributions in binary form must reproduce the above copyright notice, | |
// this list of conditions and the following disclaimer in the documentation | |
// and/or other materials provided with the distribution. | |
// | |
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE | |
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
// POSSIBILITY OF SUCH DAMAGE. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment