I agree, I wouldn't have been against a required return, but the mitigation is relatively straightforward by using custom handlers like mentioned at the end, if needed.
I really like Go's default HTTP handlers because it offers that straightforward flexibility. Nothing rocket-sciencey.
Well yeah, I think it’s important to let you write bytes to the socket and close it when you want to, and if people are paranoid about making obvious, easy to spot bugs, they can use some abstraction to protect themselves.
There are definitely edge cases here (like what if the handler already wrote a body and it was sent on the wire, etc), but this is absolutely the pattern to use, and one of my favorite parts of a type system that allows methods on function types.
Actually a bunch of router/web helper libraries do this in different ways which makes it a huge pain to compose middleware from different libraries.
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
This. Middlewares typically expect to wrap an http.Handler so you lose all the middleware composition ergonomics by adding a return value to your custom handler signature
The beauty is in the simplicity. `http.Handler` is all about what is needed to handle HTTP and nothing else. It doesn't intend to make things pretty, or safe, or anything really, except to express the minimum surface area required to interact with HTTP while abstracting the commonalities. Beyond that, as the article mentions, it's quite easy to abstract and implement your own sugar layer.
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 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.
"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 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.
"net/http" doesn't seem to come with a lot of great defaults that almost every web app would use such as named path parameters which you'd need to write a bunch of code and regex to extract, middleware, routing based on HTTP method etc. This is all stuff you'd have to implement anyway
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
I use Gin and enjoy the conveniences. Part of the reason is that it is opinionated. However, for the standard library, I think a strong, but minimal base is more important. One could argue about how extensible net/http is, but it seems reasonable as a foundation and it has been extended in a variety of opinionated (to varying degrees) ways!
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.
I agree totally, and it's worth highlighting part of the problem for those who don't write Go. http.ResponseWriter superficially looks relatively simple, but it has a number of optional interfaces attached to it that allow for things like "hijacking" the connection (for things like websockets, that initiate an HTTP connection but use it to transform into something else). Writing a wrapper around the easy and declared use case is not that hard, but blows up in weird ways if you don't also handle all the optional interfaces, and that gets even more fun if you start having frameworks trying to layer multiple of these together.
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.
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.
There are major performance advantages, especially once you're past HTTP/1.1, to passing a mutable response (i.e. in Go a ResponseWriter) to the handler rather than expecting the handler to return something created whole-cloth.
What advantages are you thinking of? The obvious one is that you might want to stream the response rather than construct it in-memory at once. But as I read the parent (and as I've used similar interfaces in Rust), you don't have to have constructed the whole response with this interface. You can return something whose body is a stream.
If you return something whose body is a stream you still have to construct that thing, including the stream. And if you return something whose body is a stream you didn't fill in yet, you need to create entire async thunks or threads to fill that data in. You have also gained ~nothing.
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:
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
Now do a body that requires literally any transformation or generation at all. Say, a series of DB query results as a CSV. (Oh, and make sure to cancel work when the request disconnects!)
Or heck, just try adding some error handling from the `io.Copy` result to what you have.
Just return an io.Reader that wraps the DB cursor and outputs CSV data... I fail to see what makes this so difficult. If the "mutable" approach involves passing down a mutable *http.ResponseWriter, then the dual approach of passing an io.Reader upstream is equivalent.
Try to write this, really. It’s such an unergonomic mess compared to for loop in the handler. It will also allocate a very complex reader that the actual version doesn’t need to.
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.
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.
I’ve not yet gotten to use it, but I understand Axum also implements IntoResponse for Result<A: IntoResponse, B: IntoResponse>, which is very convenient for the sort of error code of the above.
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.
Correct, I spent about half an hour implementing IntoResponse for my own project's error type, but now I can just use normal error handling and I get presented with an error page on any issues. It's quite magic (not really, very streamlined).
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 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).
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.
Thank you! I ran into this branching problem the other day, ended up trying to return a complicated boxed future and gave up.
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.
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.
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.
I usually create an abstraction like this in an API to require all returned errors to come with some sort of machine readable code. You either have to classify this response with an existing code in the list or add a new one. That list then gets generated as part of the documentation so consumers are getting possible error states straight from the code (without having to look at said code).
I do similar: I create a `handleError` function that deals with the error (logs it, reports it, redirects to the appropriate page), and call that in a return statement.
It'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.
To me this reads as a positive to the default handlers, as it shows how easy it was to make it work into a format that works for the programmer's preference. I appreciated the solution to the complaint
Not necessarily? The type used for Response can include a "body" type that supports streaming. E.g., body: AsyncRead in Rust. In Go I think it would resemble a channel that emitted the contents of the body.
(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.)
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 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.
- 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.
> 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.
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.
> 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, the issue is that there’s an implicit contract that says something like “a handler must write to the ResponseWriter or call http.Error before returning”, but the compiler doesn’t enforce that contract.
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)
Well its a bit of an issue with escape analysis to know for sure that something is or isn't called before the handler returns. Imagine a scenario where multiple threads get involved. That would be a lot to track.
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.
... Warning ${function} took too long to check and was skipped; consider refactoring if possible or add UNSAFE.NoEscapeTracking to the function signature.
Maybe something like that to let the humans know and make higher level choices?
With Rust you could write a function that consumes the HTTP client, not allowing you use it after, resulting in a compiler error for your example.
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.
I agree that they are awkward. I've solved it similarly by defining a custom handleFunc [1] and a custom errHTTP struct [2] which contains the HTTP response code and even more detailed error codes. It is very nice to work with.
Like so:
if err == util.ErrLimitReached {
return errHTTPEntityTooLargeAttachmentTooLarge
}
Or so:
if err != nil {
return wrapErrHTTP(errHTTPBadRequestActionsInvalid, err.Error())
}
Seems a rather theoretical problem to me because usually an external library is used. Also in the underlying http response first the return code is written before the body. This matters a lot when for instance copying a buffer into the response
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!"))
}
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())
}
}()
Maybe this is a subtlety of Go I'm not aware of, but at least in most languages, the issue with the explicit return value is that you must first return before you can send the value.
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.
So the problem with these "good" examples is that they expose the end user to Errors, 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 to expose the message of an Error to the end user.
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.
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.
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:
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.
This is a good example of how standard libraries should be. They are fine for simple use cases, and they provide the basic primitives for more complex third-party libraries to build on top of. Stuff like basic HTTP functionality.
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.
96 comments
[ 3.0 ms ] story [ 159 ms ] threadI 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:
Then you can just register it withEven 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
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:
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...
async fn create_user( Json(payload): Json<CreateUser>, ) -> impl IntoResponse {
}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.
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.
It'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.
(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.)
type HandlerFuncErr func(w http.ResponseWriter, r http.Request) error
func HandleErr(h HandlerFuncErr) http.HandlerFunc { return func(w http.ResponseWriter, r http.Request) { ... } }
- 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
https://github.com/barthr/web-util
And the blog post explaining it: https://bartfokker.com/posts/clean-handlers
The default leaves you room to do harder things like custom chunk streaming.
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?
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.
Like so:
Or so: [1] https://github.com/binwiederhier/ntfy/blob/main/server/serve...[2] https://github.com/binwiederhier/ntfy/blob/main/server/error...
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.
We've got a general rule of thumb never to expose the message of an Error to the end user.
Either way, it wouldn't address the bug here.
if id == "" { fmt.Errorf("get album: missing id") }
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.
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.