The problem with Go’s default HTTP handlers
preslav.me
preslav.me
origin := http.StripPrefix("/", http.FileServer(http.Dir("www")))
wrapped := http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) {
writer.Header().Set("Access-Control-Allow-Origin", "whatever.com")
writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
writer.Header().
Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
origin.ServeHTTP(writer, req)
})
http.Handle("/", wrapped)
You end up writing a ton of boilerplate like this if you're sticking with the standard library, but having a ton of awareness and control over every aspect of what you're doing is really just so easy in this language. If I don't understand anything in the standard library I just go to the definition of it in the golang source code and usually end up figuring out what I need. It's really impressive how much you can get done without using any packages if you're writing anything net-facing that just needs to do one thing and do it well.Anything more complex than the example can be streamlined or abstracted away by a third-party library. People get very opinionated about how those kinds of abstractions should work, so it's probably better for the core Go team not to pick a model and spend time building it.
async fn create_user( Json(payload): Json<CreateUser>, ) -> impl IntoResponse {
let user = User {
id: 1337,
username: payload.username,
};
// this will be converted into a JSON response
// with a status code of `201 Created`
(StatusCode::CREATED, Json(user))
}If the handler is complex enough that branches can't all return the same type, you can just add .into_response() to each return site, and now you're just returning a response, which also implements IntoResponse (as a no-op).
This avoids the problem discussed in this article while also avoiding much of the boilerplate of manually building responses.
It’s one of my biggest annoyance with Warp: it only implements Reply for `Result<T, http::error::Error>`, so if you have a faillible handler with more complicated error types (e.g. with a json body, or even a 200 response for RPC handlers) you’re in the awkward position of having to join the paths back (usually by converting everything to a Response by hand), which makes mistakes easier.
Echo is closer to the pattern described above, but:
1. Echo lets you return an error response, but doesn't let you customize how the error is rendered _in a modular way_. You can only have a single global custom error handler that decides how to serialize the errors, and it needs to have knowledge of how to properly serialize every kind of error in your app. I have to admit this is generally good enough for most of the apps, and I don't think you can do any better in Go, considering it doesn't have type classes like Rust. The best that you can do is to let errors implement a CustomHttpRenderer interface on their own and try to cast into it — but this is already doable with a custom error renderer in Echo.
2. Echo doesn't let you return a non-error Response from the handler - instead it's written directly to the ReponseWriter. This approach makes writing unit tests several orders of magnitude harder and also means you can forget to write a successful response in some cases. You can also both write a response directly and return an error, which cannot result in a valid behavior (Echo will take the safe route and just ignore the error code and do nothing).
In retrospect it makes sense to do it the way you mentioned. I think because I’m not as confident with traits and rust’s brand of async I tried to get it to work the same way I would in other languages, which is to mess with the function’s return type myself, rather than let the compiler figure it out.
The author says that because we have a complex function, and the HTTP handler is designed the way it is, we cannot avoid running into problems. I disagree. This are two separate things.
You could take a more functional approach and separate the main logic and the side effect. Which is writing the response.
func aMoreAdvancedHandler(w http.ResponseWriter, r *http.Request) {
res, err := helper()
if err {
http.Error(w, err.Error(), err.Status())
}
w.write(res)
}
func helper() ([]byte, err error) {
// todo
}
Where now the helper function has the problem of dealing with multiple operations that can potentially fail. Which is exactly the problem in the example. Not the HTTP handler itself...For example, we could use the defer function call with a named error to check if something fails and guarantee that we always return an appropriate error. Similar to the pattern used to free resources... I don't know how is this commonly called. But again, the problem is the helper function not the handler.
I don't want to use the FP terminology, probably most people are familiar with this pseudo-code:
func helper() (res, err) {
return foo().bar().baz()
}
Where the chain foo, bar, baz will continue to be executed if all the calls are success, but early terminate if an error occurs.So, where is the problem?
I really like Go's default HTTP handlers because it offers that straightforward flexibility. Nothing rocket-sciencey.
Why not implement ServeHTTP on the custom handler though? That's not exactly difficult:
type MyHandlerFunc func(w http.ResponseWriter, r *http.Request) error
func (f MyHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
err := f(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
Then you can just register it with http.Handle("/hello", MyHandlerFunc(someHandler))Even Mux's middleware library is not consistent in how it does this for different middlewares so they have to be applied different ways or even have wrapper functions written
We've got a general rule of thumb never to expose the message of an Error to the end user.
The HTTP handler should always ensure that "something" is returned, at the very least a return code. Once something has been returned to the client, can you really argue that there's an error?
There's a point to the risk of missing return value, you just run the risk of complicating the request flow by adding it. I'd argue that most people would view the code path as having ended, once the client receives a response. The request shouldn't really continue on the server end after that. There can be background processing, go routines or something like that, but that's a bit different, as it doesn't involve the original requests-response.
I agree that the baseline net/http implementation is correct. There is no appropriate default error (return) handler that the framework could implement, at the level it lives at, that would be correct and wouldn't be limiting.
However I very often in my own Go code immediately create an abstraction that does exactly this, because it is a good default for everything I'm doing. It just isn't suitable for everything (e.g., what do you do with this in the case of a protocol upgrade to websockets? the HTTP handlers need to be able to just terminate gracefully without being forced to "return" something).
What I would say is, net/http is a little too low level to directly use. You don't need a full framework necessarily, though if you want to use one that does this more power to you. But if you understand HTTP, it doesn't take much wrapping to make net/http useful for "normal" APIs or websites. It takes a little, though.
I'd much prefer using something that's an abstraction of it like Gin, Chi, Echo and the like. They're fairly performative and add very little overhead on top of the standard package
Instead everyone tries to do it by putting in their own implementation of the ResponseWriter interface proxying back to the 'real' one, which works perfectly but with a lot of extra code about 80% of the time, sort of works but often takes some performance hit 19% of the time, and blows up utterly when it composes poorly with someone else's 1% of the time.
Go interfaces are fun and all but optional interfaces can get a bit spiky when you start trying to use the decorator pattern on them. And in Go, decorator is a fundamental pattern in the toolkit used quite frequently, not an obscure novelty to pull out once every few years.
I think that approach used by clojure's ring shows an elegant way to represent http responses https://github.com/ring-clojure/ring/wiki/Concepts#responses. They are essentially structs with the following fields:
status := number
headers := map of string->string
body := stream | string | seq | inputstream
Request handlers are handed a request struct that is similar. The handler is a function that maps a request to a response (it doesn't actually write to streams itself).
I like this style for an http library for a couple of reasons:
1. HTTP resources can be viewed as functions whose domain is the request, and range is the response. Having the abstraction match that makes for really nice code. 2. If you model the request/response structs symmetrically and expose your http client as a handler itself, you can write proxies very easily. For an example of this, see http4k (https://www.http4k.org/documentation/) (a kotlin library).
I won't argue that the net/http abstraction should change, but I do agree with the author's take on the desired shape of an http library. I'd probably just write my own abstraction on top to fill the gap.
You didn't outline the performance advantage. You've got "you need to create entire async thunks or threads to fill that data in", but that absolutely isn't true.
Let's look at how we might stream a file in response in go with the current model vs this new model, and you can point out where the performance difference is:
// current
func responseHandler(rw http.ResponseWriter, req *http.Request) {
fi, err := os.OpenFile("/tmp/file")
if err != nil {
rw.WriteHeader(500)
rw.Write([]byte("error opening file"))
}
io.Copy(rw, fi)
fi.Close()
}
// returning a streaming Response object
func responseHandler(req *http.Request) *http.HandlerResponse {
fi, err := os.OpenFile("/tmp/file")
if err != nil {
return &http.HandlerResponse{Status: 500, Body: ioutil.NopCloser(bytes.NewReader([]byte("error opening file"))}
}
return &http.HandlerResponse{Status: 200, Body: fi}
}
In the happy path, there's no difference in number of goroutines or anything, right? In both cases, this all runs in the goroutine or thread or whatever that the caller _already_ created for the request handler. What difference does it make if the request handler calls `io.Copy(response, body)` or if your function does, in both case it's the same goroutine/thread.> You have also gained ~nothing.
You were the one that claimed you lose massive performance, and that's what we're asking about. The ergonomics are a separate thing from performance
Or heck, just try adding some error handling from the `io.Copy` result to what you have.
https://flask.palletsprojects.com/en/2.2.x/patterns/streamin...
By being able to call into the response object, the handler is able to send to the client immediately, and then the handler writer can do other bookkeeping before exiting the handler.
With a return value, work would need to be fire off asynchronously before the return value is sent.
https://github.com/barthr/web-util
And the blog post explaining it: https://bartfokker.com/posts/clean-handlers
(As a library, though, you'd also want to make the simple non-streaming case of "just return this blob of bytes" simple for the consumer, too.)
Like so:
if err == util.ErrLimitReached {
return errHTTPEntityTooLargeAttachmentTooLarge
}
Or so: if err != nil {
return wrapErrHTTP(errHTTPBadRequestActionsInvalid, err.Error())
}
[1] https://github.com/binwiederhier/ntfy/blob/main/server/serve...[2] https://github.com/binwiederhier/ntfy/blob/main/server/error...
But the matter of purity vs side effects is completely orthogonal to the default HTTP handler design. The handler provides the inputs and the output sinks that represent the "real world". But it's entirely up to you how you manage the data therein. You can continue to procedurally consume these input/output entities turtles all the way down, or you can work in a functional style until you are actually ready to flush the result of the computation down the writer.
I'm opinionated here, because I also think all API's should be built starting from a spec, such as OpenAPI, and to that end, I've written a library to do just that, generate models and echo handlers from API specs, and other contributors have added gin, chi, etc [1]
Trying to implement a circuit breaker against multiple upstreams for a single request in Go was a nightmare.
A circuit-breaker is basically by definition not request-scoped, so should not go in the context.
type HTTPError struct {
Err error
Code int
}
And then, with a couple of wrappers like this: func errorNotFound(err error) (wrapped error) {
return &HTTPError{
Err: err,
Code: http.StatusNotFound,
}
}
you could do something like this: return errorNotFound(err)
or this: return errorInternal(err) // 500 Internal Server ErrorIt's like 5 minute's work to code up, and I get to set whatever logging destination and redirect logic works best for my project.
I wrote a rudimentary static checker [2] to make sure that any function does not call both the jsonError() and errorPage() functions. This was quite a problem given how much I reuse code. I disagree with the author's take because it is incredibly obvious and quite easy to track down when you forget a return. You will have a big block of garbage in the middle of your page and JSON, and your unique error message should lead you directly to the line.
My problem is actually in executing the templates at the end of the functions [1], when you finally write out the page. If that has an error, you can't really back track. I've been thinking about changing that to write to a temporary buffer which then writes to the http.ResponseWriter but haven't made the change yet.
1. https://gist.github.com/TACIXAT/24621013248e2d91bebd16c5a02a...
2. https://gist.github.com/TACIXAT/1000e7d873f0a8a738b3e720b4a7...
I've managed to write some pretty complicated REST services with just the standard HTTP library and Mux for routing.
In fact I've never used any of the bigger libraries like Gin or Echo.
While all of my Node JS colleagues are drowning in dependabot PRs we're just cruising by in the Golang team with our small handful of dependencies. Heaven!
However there are a few of them. One particularly important one is that the „return response“ variants lack extremely on the observability front - which then in the end leads to services which are hard to run in production.
Eg let’s say I want to measure in my handler what „last byte latency“ is - or at least when the handler pushed the last byte into a tcp socket. With the Go approach it’s not a problem - we can wrap the handler in another handler which does emit metrics.
With the „return response object“ it won’t work easily anymore. At the time the handler returns nothing actually has happened.
The same thing applies for eg tracking the amount of concurrently executed requests to understand resource utilization (and starvation). No issue with the Go variant - hard to do with „function as a service“.
It’s actually rather unfortunate that all Rust frameworks went down the „response object“ route and made it hard to add proper observability.
The proposed improvement makes some ways to accidentally not adhere to the contract more obvious to humans, but still doesn’t enforce the contract.
I wonder whether there are languages that allow one to write a library that enforces such contracts at compile time.
(One way to do that would be to pass the Request and the ResponseWriter together with a state machine that is hard-coded in the API as a single object to the handler, with the equivalents of w.Write and http.Error declaring their state transitions, and having a language functionality that requires such object to be in an end state of that state machine whenever the function returns)
I don't think its a great pattern but just speaking hypothetically, if you ensure that only valid Response instances exist (because you organized the constructors to only make valid instances and nulls are invalid) you can force the user to return a valid instance.
Firstly, Java requires the compiler can prove variables are initialized before first use, and there’s a precisely described, fairly restricted algorithm that must be able to do that.
Similarly, Rust can’t correctly accept all code without ownership issues, so it supports only a subset of such code (in this case, AFAIK, that set is defined by the compiler, and growing over time)
In both cases the language designers on purpose limited what programs are valid so that the compiler can enforce certain constraints, eradicating a class of bugs.
I wonder whether there’s a language that works similar for this kind of constraints.
C++ destructors can do it for simple things such as “you must close this file before returning”, but that’s because the compiler will insert the call where needed.
Enforcing “you must call f before you ever call g” also is doable in simple cases: have f return a value of a type that must be passed to g or an object foo on which one g is implemented as a member: foo.g().
I’m not aware of any language that allows for more complex state transitions, though.
Edit: I think Rust can do (¿most of?) this by having a function take ownership of an object passed in, thus preventing further calls to it, and returning a new object on which only the then permissible calls can be made. I’m not sure one can require that to end with a ‘stopping state’ object, though.
Maybe something like that to let the humans know and make higher level choices?
func (api \*App) HandleOnError(w http.ResponseWriter,
err error, status int, context string) bool {
if err != nil {
api.logging.Error(err, context)
http.Error(w, err.Error(), http.StatusInternalServerError)
return true
}
return false
}
and then it's used in the handler funcs if api.HandleOnError(w, err, http.StatusInternalServerError, "Getting stuff for things") {
return
}
Makes the logging verbose and handling always properly taken care of, makes the error handling in handlers verbose enough from the caller but abstract enough to not be a majority of the handlers body.- by default, you can't tell if a handler has already written to the body or emitted a status code.
- by default, you can't read the body more than once
- r.PostForm vs r.Form ...that they both exist, and that they're only available after a manual call to r.ParseForm
- the http server accepts an unbounded number of connections. it'll just keep making goroutines as they come in. there's no way that I know of to apply back pressure.
there's probably more that I grump about, but those are the ones at the top of my mind. for the most part it works well, and i can work around my gripes.
This is a reasonable default - probably the only reasonable default. `TeeReader` and `MultiReader` are easily available if you want to spare the memory. (But you're right that the converse on the ResponseWriter isn't true, it's much more difficult to get your own implementation right.)
- the http server accepts an unbounded number of connections. it'll just keep making goroutines as they come in. there's no way that I know of to apply back pressure.
`net.Listener` is an interface you may implement as you choose. But in most cases a CPU limit is more than sufficient backpressure.
Fair enough
> `net.Listener` is an interface you may implement as you choose
Good point -- thanks, I hadn't considered to try and bound connections using a listener implementation.
> But in most cases a CPU limit is more than sufficient backpressure.
In many cases it's probably fine - but not all.
The issue with letting CPU be the back pressure is that it'll happen _after_ the connection is established. Unless I'm mistaken, if a load balancer can't connect, it'll usually try another host, but if it connects and times out...there may have been some side effects, so it can't try another host.
> The issue with letting CPU be the back pressure is that it'll happen _after_ the connection is established.
But this is true of anything the http.Server could have done. If you want an application-agnostic client to be sure it's safe to re-issue a POST, you have to stop it before the connection is open because as soon as it's open that data is coming through. Conversely, for anything (general or application-specific) the http.Server could have done after pickup, you could do in an early-stage handler for little to no additional overhead.
It feels like you're asking for something HTTP can't really do, so of course http.Server can't do it.
This is a fair enough point. I wasn't being discerning enough with my critique of http.Server, and when writing my initial comment, and neglected that a custom net.Listener could be supplied.
> Whether the lb can safely retry should depend on the HTTP method
to be more precise with this statement whether the lb can safely retry _after an initial connection has been made_ should depend on the http method (e.g., GET and HEAD can be considered idempotent...whether they actually are is up to the server)
> But this is true of anything the http.Server could have done.
Yes - which is why I saw value in your mentioning that a customer listener may be supplied.
> If you want an application-agnostic client to be sure it's safe to re-issue a POST, you have to stop it before the connection is open because as soon as it's open that data is coming through.
This was exactly my point.
> Conversely, for anything (general or application-specific) the http.Server could have done after pickup, you could do in an early-stage handler for little to no additional overhead.
Yes, but since the listener accepted the connection, it can't be retried against a different host.
to recap: I'm suggesting that there are times when it's appropriate to have a listener that circuit breaks to stop accepting connections. To your point, while the need exists, that's a concern of the tcp layer rather than the http layer, so not a fair criticism of http.Server
So, what should you do with an error that happens while writing the HTTP response itself? Maybe keep statistics on how often it happens, in case happens often enough that it seems relevant for debugging. But there's no good way to report an error to the client because the network connection is likely broken.
If you're not going to keep statistics, dropping errors from writing an http response is reasonable and arguably correct.
Of course it's not worth migrating to Echo just for that but it's good to know that some routers implement it differently, if it's something that bothers you.
try
{
// Some dangerous code which may throw/raise
w.Write([]byte("Hello, World!"))
}
catch( FileException )
{
w.Write([]byte("Screw your file!"))
}
catch( ... )
{
w.Write([]byte("Screw you in general!"))
}
and no such problem age := getUserAge()
if age < 18 {
w.Write([]byte("You are too young"))
// Forgot to return here...
}
w.Write([]byte("<insert adult content>")) var err error
defer func() {
if err != nil {
w.Write([]byte(err.Error())
}
}()In Go you could write a function taking the HTTP client reference and setting it to null. This would at least produce a runtime error if it ever comes across the path rather than doing something else that is unexpected.
if id == "" { fmt.Errorf("get album: missing id") }
Either way, it wouldn't address the bug here.
Because http errors/responses can be too complex for the limited Golang error objects.
You usually want an appropriate error code, a body with a message or set some header.
There is a case to be made for a linter rule that warns when a conditional evaluates an error value, without presenting a return path.
The default leaves you room to do harder things like custom chunk streaming.
Please edit the HN title to reflect the blog post.
type HandlerFuncErr func(w http.ResponseWriter, r http.Request) error
func HandleErr(h HandlerFuncErr) http.HandlerFunc { return func(w http.ResponseWriter, r http.Request) { ... } }