Embrace Go's HTTP Tools
Oct 14, 2013
Some time ago I released nosurf, a Go middleware for mitigating Cross-Site Request Forgery attacks. Writing a seemingly simple and small package was enough to fall in love with how Go handles HTTP. Yet, it's up to us to either embrace the standard HTTP facilities or fragmentate, sacrificing composability and modularity.
http.Handler is THE interface
Unified HTTP interfaces for web apps written in certain programming languages, like WSGI for Python and Rack for Ruby are a great idea, but they weren't always there. For instance, Rack only emerged in 2007, when Rails had already been going strong for a while.
Meanwhile in Go, the only interface needed has been in development since 2009, and although it's been through some serious changes since that, by the end of 2011, months before Go 1.0 was released, it had already stabilized.
Of course, I'm talking about the mighty http.Handler
.
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
To be able to handle HTTP requests, your type only needs to implement this one method. The method reads the request info from the given *Request and writes a response into the given ResponseWriter. Seems simple enough, right?
Complement, don't replace
Yet, when building abstractions on top of that, some get it wrong. Take for example Mango, described by its author as "a modular web-application framework for Go, inspired by Rack and PEP333".
This is what a Mango application looks like:
func Hello(env mango.Env) (mango.Status, mango.Headers, mango.Body) {
return 200, mango.Headers{}, mango.Body("Hello World!")
}
Looks simple, concise and very similar to WSGI or Rack, right?
Except for one thing. While with dynamic/duck typing,
you could have any iterable for a body,
here mango.Body
is simply a string.
Essentially, that takes away the ability to do
any sort of streaming responses with Mango.
Even if it were to expose a ResponseWriter
,
anything written to it would clash with the returned values,
since they're only returned at the end of the function,
after the calls to ResponseWriter have already been made.
That's bad. Whether you need another interface
on top of existing net/http
is a matter of taste,
but even if you do, it should not take functionality away.
An interface that is nicer to code with,
but takes away important functions is clearly inferior.
The right way
A popular "micro" web framework web.go
deals with this in a simple, yet much better way.
Its handlers take a pointer to web.Context
as an optional first argument.
type Context struct {
Request *http.Request
Params map[string]string
Server *Server
http.ResponseWriter
}
// ...
func hello(ctx *web.Context, val string) string {
return "hello " + val
}
web.Context
does not take the standard HTTP handler structures away.
Instead, the *Request
argument is available as a struct member
and Context
implements the required
embeds the original ResponseWriter.
The string you return from the function (if any) is simply appended
to the response.ResponseWriter
methods itself
That is a good design choice and I think it goes well with Go's philosophy. Even though you get a nice higher-level API, you don't have to sacrifice the low-level control over the request handling.
Start now
Go's HTTP library infrastructure, despite growing rapidly, still has some gaps left to fill. But the last thing we need is fragmentation and annoying incompatibilities due to poor design and abstractions that actually take important functionality away. Embracing and supporting the standard Go HTTP facilities is, in my humble opinion, the straightest way to having functional and modular 3rd-party HTTP tools.