Live data from Hacker News

The problem with Go’s default HTTP handlers

preslav.me

31–40 of 97 posts

Re: The problem with Go’s default HTTP handlers

#31
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())
  }
[1] https://github.com/binwiederhier/ntfy/blob/main/server/serve...

[2] https://github.com/binwiederhier/ntfy/blob/main/server/error...

Re: The problem with Go’s default HTTP handlers

#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

Re: The problem with Go’s default HTTP handlers

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

Re: The problem with Go’s default HTTP handlers

#35
post #26

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

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.

Re: The problem with Go’s default HTTP handlers

#36

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.

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.

Re: The problem with Go’s default HTTP handlers

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

Re: The problem with Go’s default HTTP handlers

#38
post #35
post #26

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

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

Yes, but there’s related prior art.

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.

Re: The problem with Go’s default HTTP handlers

#39

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.

> the mitigation is relatively straightforward by using custom handlers like mentioned at the end, if needed. 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.StatusInternal…

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.

Re: The problem with Go’s default HTTP handlers

#40

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.

> the mitigation is relatively straightforward by using custom handlers like mentioned at the end, if needed. 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.StatusInternal…

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

Post reply on HN