Live data from Hacker News

The problem with Go’s default HTTP handlers

preslav.me

91–97 of 97 posts

Re: The problem with Go’s default HTTP handlers

#91
post #48
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…

the problem is that when you use NO_CONTENT that you prolly can violate the http spec. (you can do that with both)

A lot of HTTP frameworks let you violate the HTTP spec to a surprising degree. Though with Rust you can just double check the return data with a middleware for Axum.

Re: The problem with Go’s default HTTP handlers

#92
post #19

Earlier quoted context omitted.

That is what Gin does.

No, that's exactly the opposite of what it does. The article above explicitly criticizes gin for not returning a value. 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 o…

Axum isn't returning anything, IntoResponse outputs to HTTP. The error type in a handler is Infallible.

Re: The problem with Go’s default HTTP handlers

#93
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 ma…

> 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.)

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.

Re: The problem with Go’s default HTTP handlers

#94
post #93

Earlier quoted context omitted.

- 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 ma…

> 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.) Fair enough > `net.Listener` is an interface you may implement as you choose Good point -- thanks, I hadn't considered to try and b…

Whether the lb can safely retry should depend on the HTTP method, in practice you can configure most lbs appropriately based on application knowledge as well. (I.e. a repeat POST might be "safe enough" on some endpoints.)

> 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.

Re: The problem with Go’s default HTTP handlers

#95
post #93

Earlier quoted context omitted.

> 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.) Fair enough > `net.Listener` is an interface you may implement as you choose Good point -- thanks, I hadn't considered to try and b…

Whether the lb can safely retry should depend on the HTTP method, in practice you can configure most lbs appropriately based on application knowledge as well. (I.e. a repeat POST might be "safe enough" on some endpoints.) > 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…

> 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

Re: The problem with Go’s default HTTP handlers

#96
post #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()…

It's very funny that you trying to invalidate the problem OP is explaining and you made the exact same mistake in 10 lines of code :) You forgot to return from the error handling branch.

Re: The problem with Go’s default HTTP handlers

#97
The article points out a „pro“ of returning a response in handlers - but fails to miss any cons.

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.

Post reply on HN