Giter VIP home page Giter VIP logo

go-tigertonic's Introduction

Tiger Tonic

Build Status

A Go framework for building JSON web services inspired by Dropwizard. If HTML is your game, this will hurt a little.

Like the Go language itself, Tiger Tonic strives to keep features orthogonal. It defers what it can to the Go standard library and a few other packages.

Documentation

Articles and talks

Reference

http://godoc.org/github.com/rcrowley/go-tigertonic

Community

Synopsis

tigertonic.TrieServeMux

HTTP routing in the Go standard library is pretty anemic. Enter tigertonic.TrieServeMux. It accepts an HTTP method, a URL pattern, and an http.Handler or an http.HandlerFunc. Components in the URL pattern wrapped in curly braces - { and } - are wildcards: their values (which don't cross slashes) are added to the URL as u.Query().Get("name").

HandleNamespace is like Handle but additionally strips the namespace from the URL, making API versioning, multitenant services, and relative links easier to manage. This is roughly equivalent to http.ServeMux's behavior.

tigertonic.HostServeMux

Use tigertonic.HostServeMux to serve multiple domain names from the same net.Listener.

tigertonic.Marshaled

Wrap a function in tigertonic.Marshaled to turn it into an http.Handler. The function signature must be something like this or tigertonic.Marshaled will panic:

func myHandler(*url.URL, http.Header, *MyRequest) (int, http.Header, *MyResponse, error)

Request bodies will be unmarshaled into a MyRequest struct and response bodies will be marshaled from MyResponse structs.

Should you need to respond with an error, the tigertonic.HTTPEquivError interface is implemented by tigertonic.BadRequest (and so on for every other HTTP response status) that can be wrapped around any error:

func myHandler(*url.URL, http.Header, *MyRequest) (int, http.Header, *MyResponse, error) {
    return 0, nil, nil, tigertonic.BadRequest{errors.New("Bad Request")}
}

Alternatively, you can return a valid status as the first output parameter and an error as the last; that status will be used in the error response.

If the return type of a tigertonic.Marshaled handler interface implements the io.Reader interface the stream will be written directly to the requestor. A Content-Type header is required to be specified in the response headers and the Accept header for these particular requests can be anything.

Additionally, if the return type of the tigertonic.Marshaled handler implements the io.Closer interface the stream will be automatically closed after it is flushed to the requestor.

tigertonic.Logged, tigertonic.JSONLogged, and tigertonic.ApacheLogged

Wrap an http.Handler in tigertonic.Logged to have the request and response headers and bodies logged to standard output. The second argument is an optional func(string) string called as requests and responses are logged to give the caller the opportunity to redact sensitive information from log entries.

Wrap an http.Handler in tigertonic.JSONLogged to have the request and response headers and bodies logged to standard output as JSON suitable for sending to ElasticSearch, Flume, Logstash, and so on. The JSON will be prefixed with @json: . The second argument is an optional func(string) string called as requests and responses are logged to give the caller the opportunity to redact sensitive information from log entries.

Wrap an http.Handler in tigertonic.ApacheLogged to have the request and response logged in the more traditional Apache combined log format.

tigertonic.Counted and tigertonic.Timed

Wrap an http.Handler in tigertonic.Counted or tigertonic.Timed to have the request counted or timed with go-metrics.

tigertonic.CountedByStatus and tigertonic.CountedByStatusXX

Wrap an http.Handler in tigertonic.CountedByStatus or tigertonic.CountedByStatusXX to have the response counted with go-metrics with a metrics.Counter for each HTTP status code or family of status codes (1xx, 2xx, and so on).

tigertonic.First

Call tigertonic.First with a variadic slice of http.Handlers. It will call ServeHTTP on each in succession until the first one that calls w.WriteHeader.

tigertonic.If

tigertonic.If expresses the most common use of tigertonic.First more naturally. Call tigertonic.If with a func(*http.Request) (http.Header, error) and an http.Handler. It will conditionally call the handler unless the function returns an error. In that case, the error is used to create a response.

tigertonic.PostProcessed and tigertonic.TeeResponseWriter

tigertonic.PostProcessed uses a tigertonic.TeeResponseWriter to record the response and call a func(*http.Request, *http.Response) after the response is written to the client to allow post-processing requests and responses.

tigertonic.HTTPBasicAuth

Wrap an http.Handler in tigertonic.HTTPBasicAuth, providing a map[string]string of authorized usernames to passwords, to require the request include a valid Authorization header.

tigertonic.CORSHandler and tigertonic.CORSBuilder

Wrap an http.Handler in tigertonic.CORSHandler (using CORSBuilder.Build()) to inject CORS-related headers. Currently only Origin-related headers (used for cross-origin browser requests) are supported.

tigertonic.Configure

Call tigertonic.Configure to read and unmarshal a JSON configuration file into a configuration structure of your own design. This is mere convenience and what you do with it after is up to you.

tigertonic.WithContext and tigertonic.Context

Wrap an http.Handler and a zero value of any non-interface type in tigertonic.WithContext to enable per-request context. Each request may call tigertonic.Context with the *http.Request in progress to get a pointer to the context which is of the type passed to tigertonic.WithContext.

tigertonic.Version

Respond with a version string that may be set at compile-time.

Usage

Install dependencies:

sh bootstrap.sh

Then define your service. The working example may be a more convenient place to start.

Requests that have bodies have types. JSON is deserialized by adding tigertonic.Marshaled to your routes.

type MyRequest struct {
	ID     string      `json:"id"`
	Stuff  interface{} `json:"stuff"`
}

Responses, too, have types. JSON is serialized by adding tigertonic.Marshaled to your routes.

type MyResponse struct {
	ID     string      `json:"id"`
	Stuff  interface{} `json:"stuff"`
}

Routes are just functions with a particular signature. You control the request and response types.

func myHandler(u *url.URL, h http.Header, *MyRequest) (int, http.Header, *MyResponse, error) {
    return http.StatusOK, nil, &MyResponse{"ID", "STUFF"}, nil
}

Wire it all up in main.main!

mux := tigertonic.NewTrieServeMux()
mux.Handle("POST", "/stuff", tigertonic.Timed(tigertonic.Marshaled(myHandler), "myHandler", nil))
tigertonic.NewServer(":8000", tigertonic.Logged(mux, nil)).ListenAndServe()

Ready for more? See the full example which includes all of these handlers plus an example of how to use tigertonic.Server to stop gracefully. Build it with go build, run it with ./example, and test it out:

curl -H"Host: example.com" -sv "http://127.0.0.1:8000/1.0/stuff/ID"
curl -H"Host: example.com" -X"POST" -d'{"id":"ID","stuff":"STUFF"}' -sv "http://127.0.0.1:8000/1.0/stuff"
curl -H"Host: example.com" -X"POST" -d'{"id":"ID","stuff":"STUFF"}' -sv "http://127.0.0.1:8000/1.0/stuff/ID"
curl -H"Host: example.com" -sv "http://127.0.0.1:8000/1.0/forbidden"

WTF?

Dropwizard was named after http://gunshowcomic.com/316 so Tiger Tonic was named after http://gunshowcomic.com/338.

If Tiger Tonic isn't your cup of tea, perhaps one of these fine tools suits you:

go-tigertonic's People

Contributors

abh avatar babo avatar bigkevmcd avatar bketelsen avatar dsmith avatar emil2k avatar ice799 avatar jbarratt avatar matterkkila avatar micrypt avatar mihasya avatar mncaudill avatar nbrownus avatar randallsquared avatar rcrowley avatar rossjhagan avatar snikch avatar stevenwilkin avatar wadey avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

go-tigertonic's Issues

Authorization via client certificates

Whether a part of the core or not, there should be a handler that can verify client certificates and possibly make authorization decisions on a per-route basis.

go1.2 breaks http error wrappers

After testing with go1.2, I found that my tigertonic applications don't compile with the following error:

myfile.go:20: implicit assignment of unexported field 'error' in tigertonic.NotFound literal

The code I am doing on this line is basically:

err := &tigertonic.NotFound{fmt.Errorf("My error message")}

Looks like go1.2 is preventing assignment to embedded "error" fields if you aren't in the same package. This seems like a pretty unfortunate change, we might need to make a helper method to construct these wrapper errors.

accept tcp 127.0.0.1:12345: use of closed network connection

I'm using the HEAD from master, and getting this error:

accept tcp 127.0.0.1:12345: use of closed network connection

(This is the error returned from Server.ListenAndServe(), and in turn net.Listen())

I'm trying to figure what exactly is trying to use it. Any ideas?

Support http.Flush()

It'd be nice if the response writers implemented the http.Flusher interface so chunked responses can be forced out.

Convenience for TLS certificates in/around Server

This is tedious:

c, err := tls.LoadX509KeyPair(*cert, *key)
if nil != err {
    log.Fatalln(err)
}
certPool := x509.NewCertPool()
buf, err := ioutil.ReadFile(*ca)
if nil != err {
    log.Fatalln(err)
}
certPool.AppendCertsFromPEM(buf)
tlsConfig = &tls.Config{
    Certificates: []tls.Certificate{c},
    CipherSuites: []uint16{tls.TLS_RSA_WITH_RC4_128_SHA},
    RootCAs:      certPool,
}

Erroneously returns 405 when the last segment of the request URL matches a parameter in some other handled URL

I'm handling a couple of URLs:

mux.Handle("GET", "/api/{organization}/groups", ...)
mux.Handle("GET", "/api/{organization}/groups/{group}/hosts", ...)

Given this set of handlers:
GET @ /api/12345/foo - Returns 404, as I'd expect
...but...
GET @ /api/12345/groups/ - Returns 405.
GET @ /api/12345/groups/foo - Also returns 405.

Both 405 responses come back claiming:

{
    "description": "only OPTIONS are allowed",
    "error": "tigertonic.MethodNotAllowed"
}

There seems to be some weird behavior if it's trying to match URLs and the last segment of the requested URL matches a parameter portion of any other URL, even if there's no way the URL could match, as in /api/12345/groups/foo. It also doesn't seem to be checking for empty parameter values in cases like this, causing /api/12345/groups to work as expected but /api/12345/groups/ to fail with 405.

Also, if I add another handler:

mux.Handle("GET", "/api/{organization}/groups/{group}", ...)

...then requests to /api/12345/groups/ do get routed to this new handler, but it gets an empty "group" value.

Middleware middleware that makes it easy to optionally handle a request

This could take a number of forms. The motivation is to have an analogy to Java's servlet filters or (forgive me) or the middleware chains in Django and Express.

As a strawman, consider a func First(handlers ...http.Handler). It could call handler.ServeHTTP(w, r) on successive handlers until w.WriteHeader is called.

What to do in a panic?

Just wondering what the canonical response to a panic ought to be.

We rolled a web service in Revel and we were able to create a panic interceptor, however my very simple tiger-tonic web site tanks and exits on panic and I'm not sure if there's a built in way to capture that without tanking the server.

In retrospection Revel was not an ideal use case for our web service and I think Tiger Tonic or similar is a much stronger match.

Thanks much.

should graceful shutdown branch be the default now?

I'm using the go1.3 branch because I need the graceful shutdown part. go1.3 is out now and the currently recommended go version. Would you consider merging the 1.3 shutdown work into the master branch now?

Healthchecks

It'd be nice to have a HealthCheck registry, and some sort of "healthy" or "unhealthy" output with a Handler that can be linked up...

Error Handler

If I were to extract the current errors handling to an interface and allow it to be replaced, would that be something that might be pulled? Or would I be wasting my time?

too narrow listing of TLS cipher suites

The cipher suites in go-tigertonic all rely on RC4 which has many suspicions around it (its biases are a little funny and there are strong rumors of breaks from certain Government Bodies). While RC4 is a fine fallback for TLS 1.0 (due to BEAST), later versions of TLS are better off using CBC modes. Also, Go's crypto/tls already mitigates the BEAST attack.

The default list of cipher suites in crypto/tls is very solid and was chosen carefully.

If you wish to lock the cipher suite list down further, a more scalable way to do this would be to require TLS 1.2 as the minimum TLS version and continue to use the default cipher suite list. (TLS 1.2 and 1.1 fix the BEAST attack by changing how CBC is performed as one useful, if mooted, example). Using the defaults and setting the minimum version will net you the Langley-Approved cipher suites with few backwards compatibility problems.

Of course, perhaps I've mis-guessed why those were selected.

Support URL namespaces

It's really just another TrieServeMux or even http.ServeMux but that strips the namespace from the URL before handling it further.

We need a way to factor out per-request initialization

@wadey says:

main thing I miss is a way to build up state as the request passes along the filters, not sure what the best way to model that is though
i dont like how some frameworks just make a grabbag map on the request object
but I feel like there is something we could do, maybe a domain specific request object you can create
I want to noodle on that, thats the main thing I feel like tigertonic is missing

It's pretty dissatisfying to have to call a GetUser(request) function in every single http.Handler that needs a *User, for example.

Method values may give us something but we've used those so far to provide global, not per-request, context.

HTTP response codes for errors

type HTTPEquivError interface {
    error
    Status() int
}

Use errors like this to respond with something sensible at the HTTP layer. We're doing this internally at Betable and it's nice.

no obvious way to log json unmarshal errors

Let's say I have this code:

// Set main handler & context.
collectHandler :=
    tigertonic.Timed(
        corsBuilder.Build(
            tigertonic.WithContext(
                // Create context for handler.
                tigertonic.If(createContext, tigertonic.Marshaled(collect)),
                RequestContext{})),
        "Collect", nil)

If there's some JSON unmarshal problem in collect (which is a function with the correct signature), there's no obvious way to log it, besides wrapping everything with tigertonic.Logged(), which logs to much.

Is there a way to do that which I haven't figured out?

Components of URL pattern are rewritten(overridden) by subpattern if different names are used

Adding handlers for subpatterns overrides the parent pattern if the components are named differently. Ex:

apiMux = tigertonic.NewTrieServeMux()
apiMux.Handle("GET", "/templates/{name}", tigertonic.Marshaled(handlers.ReadTemplate))
apiMux.Handle("GET", "/templates/{n1}/{n2}", tigertonic.Marshaled(handlers.ReadSubTemplate))

The first handler can never be executed in this scenario. This is a useful feature in my humble opinion, however, it needs to be documented.

The proper way to use it would be:

apiMux = tigertonic.NewTrieServeMux()
apiMux.Handle("GET", "/templates/{n1}", tigertonic.Marshaled(handlers.ReadTemplate))
apiMux.Handle("GET", "/templates/{n1}/{n2}", tigertonic.Marshaled(handlers.ReadSubTemplate))

Just my 2 cents.

Can't use go-tigertonic on Google AppEngine

Hello,

It seems that tigertonic's dependency — go-metrics ain't compile on GAE. I get the following error message when I try deploy tigertonic-based application on appengine:

…
06:11 PM Error 422: --- begin server output ---
Compile failed:
2014/07/23 07:11:56 go-app-builder: build timing: 4×6g (414.00874ms total), 6×gopack (228.451024ms total), 0×6l (0 total)
2014/07/23 07:11:56 go-app-builder: failed running 6g: exit status 1

github.com/rcrowley/go-metrics/runtime.go:122: undefined: runtime.NumCgoCall
--- end server output ---
06:11 PM Rolling back the update.
Error 422: --- begin server output ---

--- end server output ---
error while running appcfg.py: exit status 1

CSV output

http://golang.org/pkg/encoding/csv/ is a reasonable interface that would be easy to wrap around an http.ResponseWriter. This would be extremely easy for GET requests but it'd be nice to reuse the Marshaler code for JSON POST requests.

@matterkkila let's talk about how you want to use it and whether you will even need POST responses as CSV.

Return Error Messages

This is a non issue really, I'm just curious! What's the design reason for spitting out the 'kind' of error? I don't get the benefit of return it to the end user (and exposing internals).

{
  "description": "Invalid id given",
  "error": "validator.Errors"
}

vs

{
  "error": "Invalid id given"
}

Duplicate name when using Timed causes silent metrics loss

Because of the way tigertonic.Timed interacts with go-metrics, it's easy to end up in a confusing situation where there are metrics missing from an endpoint merely because another endpoint was given a timer with the same name.

I'm not yet sure what the correct behavior here is. My observations and thoughts so far:

  • There are (debatably) legitimate reasons that the same endpoint might be registered at two different URLs, so it might actually be fine to have both instances present metrics at the same name. However, you can also work around this issue (by pre-defining the handler outside the calls to .Handle). So erroring out when tigertonic.Timed is called with a dupe would still accommodate this usecase.
  • The route definitions are a thing that gets copy pasted all the time. It's a likely scenario that someone neglects to rename the metric, then has no real feedback that it's not working until they basically prove to themselves some other way that their graphs are wrong.

(Both of these scenarios have occurred here)

mocking.URL does not catch methodNotAllowed error

Change example.go on line 51 from

mux.Handle(
                "GET",
                "/stuff/{id}",

to

mux.Handle(
                "GET",
                "/stufff/{id}",

and run tests. No error is caught. The reason is because methodNotAllowed (defined in trie_serve_mux.go) is a handler and mocking.URL does not see it as a problem.

Bugs in README

The examples in README look wrong.

func myHandler(u *url.URL, h http.Header, *MyRequest) (int, http.Header, *MyResponse, error) {
..

mux.Handle("GET", "/stuff", tigertonic.Marshaled(tigertonic.Timed(myHandler, "myHandler", nil)))

surely that should be Timed(Marshaled(myHandler, "myHandler", nil))
as myHandler is not a http.Handler.

Cookie-based auth

Hopefully this is based on OAuth or some other standard to bridge browser cookies into that auth scheme.

Lock-free per-request context

WithContext and Context are unnecessarily globally synchronized. I think the solution is essentially an http.Handler that allocates an instance of another http.Handler (this is the type of the per-request context) and calls its ServeHTTP method.

Decouple CORS from TrieServeMux

@mihasya

I'm not sure how plausible this is but it would be nice for CORS to work without assuming a TrieServeMux. From IRC:

[11:41:46] <submersive>  rcrowley: looking at tigertonic cors stuff, confused as to how this works with options well?
[11:43:36] <submersive>  it doesn't look like it knows the allowed methods or accepts/provides content types?
[11:43:43] <rcrowley>    submersive: mihasya actually wrote the CORS support so hopefully he's around.  As far as I remember, OPTIONS is handled correctly by tigertonic.TrieServeMux with some added headers for CORS.
[11:44:00] <submersive>  ah with Trie Mux
[11:44:22] <submersive>  been using gorilla mux since we match on uuid's
[11:44:35] <submersive>  I'll take a closer look, thanks
[11:45:00] <rcrowley>    Ah, that explains it.
[11:45:33] <rcrowley>    submersive: I'd like to decouple those components; they really don't have anything to do with each other except for the collision on header names.
[11:46:21] <submersive>  rcrowley: yeah, really need something to build out that metadata somehow
[11:46:42] <submersive>  like I guess java has that jax-rs, erlang has the webmachine/cowboy_rest thing
[11:47:27] <submersive>  nothing really like that for go except I guess go-restful, but its all tied in with go-restful so harder to mix'n'match like with tigertonic and gorilla (I like tigertonic and gorilla)
[11:48:33] <submersive>  I guess a fancier mux could do that
[11:51:59] <rcrowley>    submersive: the key's really http.ResponseWriter.Header() and some agreement about how to cooperatively add/modify headers.  I think for CORS it's solvable.  I'll ping mihasya, who doesn't appear to be online right now.

Get Url pattern or Url with filled parameters from within handler

I'm not sure whether I've just missed it in the docs/source code, but is there a way to extract the Original Url pattern or direct Url call to which a Handler responds for example when embedding resources or dealing with pagination so that it can be returned in the JSON?

For ex.
The url for an /api/experimental/{experiment} called with /api/experimental/yoyo returned from a tigertonic.Marshalled Handler is:
http://localhost:9980/experimental/yoyo?experiment=yoyo&%7Bexperiment%7D=yoyo&

Is there any way to unmarshal the url so we get a clean form
/api/experimental/yoyo
or better yet, get the pattern and do operations on it:
/api/experimental/{experiment}

Tracking cookie equivalent

Maybe even literally cookies.

This always gets bolted onto every web app-driving API; let's do it right.

Add access to http.Request object in Marshaled()

I need to access the remote address in a marshaled function, and for this I need access to the http.Request object. Currently there's no obvious way to do that. Am I missing something? Will be happy for any guidance if there is an easy way to do so. Otherwise, please add this option. Will be happy to do that myself and pull request if you'll guide me first.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.