The Go/HTTP handler impedance mismatch

September 25, 2017

When it comes to writing web apps in Go, I have yet to see a clean solution for a very fundamental problem. I see an impedance mismatch between Go idioms and the necessities of the HTTP protocol. It’s by no means exclusive to Go, but this is where it bothers me, so I’ll limit my discussion to the problem as it affects Go.

From the server standpoint, HTTP is essentially a simple request/response protocol, in which a request is received, and a response is generated. At first glance, this would appear to map cleanly to a function signature, something like:

func HandleRequest(request *http.Request) *http.Response {
	// Do something interesting
}

As simple as this would be, conceptually, it has some pretty serious limitations. One such limitation would be on chaining of handlers and middlewares. As a simple example, there would be no way for a middleware layer to set a response header, then pass off processing to the main handler. To accommodate this, http handlers receive both the request and the response as function arguments. This allows in-place modification at anywhere in the call tree:

func HandleRequest(response http.ResponseWriter, request *http.Request) {
	// Do something interesting
}

This seems quite reasonable, and is immensely flexible, even though it introduces the aforementioned impedance mismatch. Where it becomes cumbersome, however, is with idiomatic Go error handling. Doing anything of any complexity quickly becomes quite tedious:

func HandleRequest(w http.ResponseWriter, r *http.Request) {
	If err :=  ; err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprintln(w, %s, err)
		return
	}
	// Defaults to HTTP status 200
	fmt.Fprintln(w, Success!)
}

And checking multiple error conditions quickly duplicates the error-handling boiler plate. A simple simplification is to move that into a helper function, but that only buys us so much brevity:

func handleError(w http.ResponseWriter, err error) {
	If err == nil {
		return
	}
	w.WriteHeader(http.StatusInternalServerError)
	fmt.Fprintln(w, %s, err)
}

func HandleRequest(w http.ResponseWriter, r *http.Request) {
	If err :=  ; err != nil {
		handleError(w, err)
		return
	}
	// Defaults to HTTP status 200
	fmt.Fprintln(w, Success!)
}

The popular Go framework Echo addresses this by using a custom handler function type which returns an error. This pattern I have also seen in many home-grown HTTP wrappers, and likely exists in some other frameworks as well.

func HandleRequest(w http.ResponseWriter, r *http.Request) error {
	If err :=  ; err != nil {
		return err
	}
	/// Handle request
}

This buys a lot of convenience, but it seems to entirely undermine consistent programming styles. What was previously a single, well-defined scheme for HTTP handlers (modify the response in place, and return when done) is now a convoluted either-or scheme (modify the response in place, unless you encounter an error, in which case you may still modify the response in place, or you may eturn an error, which will later be converted into a response–if a response hasn’t already been sent).

Many other frameworks (ironically, Echo included), allow handling of errors by calling a special function (essentially my handleError helper above, with better syntactic sugar).

And here I am stuck, with three patterns, each with serious drawbacks:

  1. `func(*http.Request) http.Response` seems Go-idiomatic, but severely limited.

  2. `func(http.ResponseWriter, *http.Request)` found in the http standard library, but requires overly verbose error-handling.

  3. `func(http.ResponseWriter, *http.Request) error` solves the error-handling verbosity, at the cost of ambiguity.

  4. `func(*http.Request) (http.Response, error)` might be a nice addition to the list, too, because it's so clearly Go-idiomatic. But it still has the limitations of #1 above.

What is your experience in this area? What are your thoughts? Is the error-returning HTTP handler the best solution, despite its semantic ambiguity?

Is there some other pattern you’ve seen or used, which has promise?

If you could write your own framework from scratch (because we know the world needs more frameworks!), how would you solve this issue?


comments powered by Disqus