Skip to content

Allow customize how HTTP errors are rendered when websocket.Accept fails to upgrade #545

Open

Description

@jraygauthier

IDEA: Might be as simple as a new HttpErrorRenderer (or OnError, etc.) option added to websocket.AcceptOptions. When unspecified, default to a fn that implements current rendering behavior. It would take at minimum a ctx, the err and the http status code.

Nice addition: the full set of errors.New("..") would be exposed to user so that one can use errors.Is to get more granularity than provided http statuses.

We need this feature in order to produce https://www.rfc-editor.org/rfc/rfc9457.html (a standardized JSON error response format) when an error occurs in order to match the rest of our API which produces this error response format for all of its endpoints. Current hard-coded behavior prevents us to comply with our policy when WS upgrade fails in this library. Our user thus can't systematically assume this nice JSON format and must workaround this limitation...

Activity

  1. mafredri commented on Dec 16, 2025

    @mafredri
    Member

    Hey @jraygauthier. This is a good idea, and a shortcoming I've identified before. Could you expand upon on why the error handler needs to take ctx? Is it for values or for cancellation, and if the latter, what type of long-running operations do you expect it will be used to run?

  2. jraygauthier commented on Dec 16, 2025

    @jraygauthier
    Author

    Could you expand upon on why the error handler needs to take ctx? Is it for values or for cancellation, and if the latter, what type of long-running operations do you expect it will be used to run?

    Hi @mafredri. My concern was more about proper cancellation in the context of graceful shutdown. I guess that as Accept takes the request (websocket.Accept(w, r, &websocket.AcceptOptions{})) that'd be the request's context so that when one cancels the request (most likely the BaseContext provided to one's http.Server), any long running operation one might want to run in that handler is bound to that request so that it remains possible to gracefully shutdown the server's websockets (which are just long lived requests).

    If our error handler is provided with w, r just like Accept than we're already covered as the implementer can just r.Context() and get both extra values and cancellation from it.

    Here's how I currently handle that graceful shutdown at the top level:

    /* ... */
    
    r := routes.Routes(/*..*/)
    
    reqTracker := request.NewTracker()
    handler := middleware.RequestTracker(reqTracker)(r)
    
    reqCtx, reqCancel := context.WithCancel(ctx)
    
    server := &http.Server{
    	/*... */
    	Handler:           handler,
    	BaseContext: func(net.Listener) context.Context {
    		return reqCtx
    	},
    }
    
    server.RegisterOnShutdown(func() {
    	reqCancel()
    })
    
    shutdownServer := func(ctx context.Context) error {
    	defer reqTracker.Wait(ctx)
    	return server.Shutdown(ctx)
    }
    
    /* ... */
  3. mafredri commented on Dec 17, 2025

    @mafredri
    Member

    @jraygauthier thanks for the context. I’m still wondering why the context, or even r, need to be arguments? Could you provide real code examples where their inclusion add value compared to just w, err, statusCode? I would expect that typically the handler is just a plain write on w and there’s really no way to utilize a context then.

    In very bespoke implementation, the caller could even capture a context and r via closure at the accept call-site, if needed.

  4. jraygauthier commented on Dec 18, 2025

    @jraygauthier
    Author

    @mafredri :

    In very bespoke implementation, the caller could even capture a context and r via closure at the accept call-site, if needed.

    I agree there, in almost all cases w, err, statusCode would be plenty enough and as you say, taking those extra inputs via closure would be perfectly fine.

    Could you provide real code examples

    No actual code examples to provide here. Here are a couple of edge cases I can think of.

    The context might be useful:

    • when using 3rd party lib that requires it
    • to extract values injected via middle-wares (my specific case where I have a developer mode which when active let me respond with extra detailed error information that would normally be only logged)

    The request might be useful:

    • for responses whose content depends on headers

    But as you said, those can anyway be captured via closure thus covering those less common edge cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions