Live data from Hacker News

The problem with Go’s default HTTP handlers

preslav.me

51–60 of 97 posts

Re: The problem with Go’s default HTTP handlers

#51
post #5
post #3

It's not something I even thought about, but I guess I see the point, I just don't agree. In the HTTP handlers it makes sense that you don't have return values, because: What would you do with that value exactly? 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 poin…

"In the HTTP handlers it makes sense that you don't have return values, because: What would you do with that value exactly?" 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 abstrac…

I agree with you that an error return would be superfluous and probably misused, but middleware with the default handler could be a lot more usable if you could just query a ResponseWriter's written status code.

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.

Re: The problem with Go’s default HTTP handlers

#52

This isn't so much a critique of the HTTP handlers themselves but relying on (and stuffing a ton of shit into) context as a way of dealing with complex request/response patterns was a major headache. Trying to implement a circuit breaker against multiple upstreams for a single request in Go was a nightmare.

People put way too much stuff in the context (whether that's the req.Context() or the gin.Context or whatever). It should only be for things that are request-scoped and need to cross middlewares that aren't aware of them. Most such middlewares (logging, metrics, last-ditch error handling) get put at the front of the chain anyway.

A circuit-breaker is basically by definition not request-scoped, so should not go in the context.

Re: The problem with Go’s default HTTP handlers

#53
post #37

So the problem with these "good" examples is that they expose the end user to Error s, messages likely not written by you or your team and moreso not intended for general consumption - you need to be really careful about doing so because an unexpected error could expose lots of troubling things like credentials and general information about your system you'd rather not expose. We've got a general rule of thumb never…

Yep, that's why you'd want the error handler to try and type assert the error to something like an HTTPError where that's an interface you control. If it is an HTTPError, then you can "trust" the code/message and write that back to the user. Otherwise, 500 with a generic response body.

Re: The problem with Go’s default HTTP handlers

#54
Well yeah, the default stdlib HTTP handlers are very basic. The cool thing is that the standard library is replete enough to build tons on top of that, though. CORS headers using nothing but stdlib for example:

  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.

Re: The problem with Go’s default HTTP handlers

#55

Why is it http.Error(w, ...) and not w.Write(http.Error(...))? The latter would be way more clear in showing that nothing magical happens and a return (if wanted) is still necessary (I don't write Go, so I don't know the conventions)

`http.Error` is a convenience for writing an error message and setting the code. You can just use `w.Write` if you want. The reason `Error` isn't part of the interface signature for `ResponseWriter` is presumably that everything that implements `ResponseWriter` would have to duplicate the logic.

Either way, it wouldn't address the bug here.

Re: The problem with Go’s default HTTP handlers

#56
post #18

I dont necessarily agree they should return a value, but I do have some issues with the api: - 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 connection…

- by default, you can't read the body more than once

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.

Re: The problem with Go’s default HTTP handlers

#57
I dislike the example.

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?

Re: The problem with Go’s default HTTP handlers

#58
post #4

I like the way Axum for Rust handles this. All response handlers return something that implements IntoResponse. Lots of types implement IntoResponse, thus making the simple cases really streamlined. async fn create_user( Json(payload): Json , ) -> 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` (Status…

There are Go HTTP libraries that behave that way too. This article only refers to the one in standard library.

Re: The problem with Go’s default HTTP handlers

#59
post #33

To me it seems like a problem induced by the way Go handles errors. With exceptions it would be something like 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

Putting aside discussions about Gos error handing and opinions about try and catch blocks, this specific problem (wrt the HTTP handler) can emulate the same code you’ve got above using a defer block.

  var err error
  defer func() {
    if err != nil {
      w.Write([]byte(err.Error())
    }
  }()
Post reply on HN