Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to pass information back to middleware? #914

Open
koote opened this issue May 14, 2024 · 4 comments
Open

How to pass information back to middleware? #914

koote opened this issue May 14, 2024 · 4 comments

Comments

@koote
Copy link

koote commented May 14, 2024

The problem is in middleware some information is unknown, those information can only be determined later.
For example, I have an instrumentation middleware, which measures the request handling time (egress - ingress), and logs the error when request handling hit any errors, obviously those kind of information is only available when request reaches the handler implements business logic. When instrumentation middleware receives the request, it has no way to know how long will processing current request take or whether current request will encounter any errors.

However, in golang the only way to pass information when process http request is using the context object, the information flows from upstream to downstream, I can wrap any values or objects into context using context.WithValue and export the key, downstream modules can use the key to extract those data out from context, but I cannot do it in reverse order, passing the information from downstream back to upstream modules like middleware.

Seems I need a mechanism to setup a callback hook in http process pipeline, when request pops up from the stack/pipeline, but I cannot find such a mechanism is supported by go-chi, does anyone have a good suggestion on this? Thanks.

@VojtechVitek
Copy link
Contributor

VojtechVitek commented May 15, 2024

Each middleware that needs to get information from downstream handlers needs to create their own storage in the context, e.g. a pointer to a value, that will be dereferenced and filled from within downstream (so it can bubble back up).

There's no other way around this, as contexts get unwrapped on a way back up.

I agree it would be useful to have a common middleware pkg for this.

@koote
Copy link
Author

koote commented May 16, 2024

Each middleware that needs to bubble up information from handlers downstream needs to create their own storage in the context, e.g. a pointer to a value that will be filled downstream.

There's no other way around, as contexts get unwrapped on a way back up.

I agree it would be useful to have a common middleware pkg for this.

Thanks @VojtechVitek, are you suggesting that, I can do like this:

type contextKey struct {
    name string
}

var (
    FinalErrorKey = &contextKey{"final-error"}
)

func Middleware() func(http.Handler) http.Handler {
    return func(h http.Handler) http.Handler {
        return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
            var finalError error
            ctx := req.Context()
            ctx = context.WithValue(ctx, ErrorKey, &finalError)
            h.ServeHTTP(rw, req.WithContext(ctx))
       })
    }
}

Then extract the value and fill it in handler:

result, err := processTheRequest(req)
if err != nil {
    errorVal = context.Value(middleware.FinalErrorKey)
    if errorWrapper != nil {
       *errorVal = err
    }
}

@kirides
Copy link

kirides commented May 31, 2024

You'd typically do something like the following, but more reasonably would be a "top level" handler that wraps any function into an handler that returns an error instead of passing state through context.

type contextKey int
const errorHolderKey contextKey = 1

type errorHolder struct {
    Error error
}

func Middleware() func(http.Handler) http.Handler {
    return func(h http.Handler) http.Handler {
        return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
            ctx := req.Context()
            holder := &errorHolder{}
            ctx = context.WithValue(ctx, errorHolderKey, holder)

            h.ServeHTTP(rw, req.WithContext(ctx))

            // Do something with "holder"
       })
    }
}

func handler(w ..., r ...) {
    something, err := processTheRequest(req)
    if err != nil {
        holder := context.Value(errorHolderKey).(*errorHolder)
        // holder is a pointer to a errorHolder, this allows us to mutate it's state.
        holder.Error = err
    }
}

Better would be

func ErrorHandler(handler func(w http.ResponseWriter, r *http.Request) error) http.HandlerFund {
    return func(w http.ResponseWriter, r *http.Request) {
        if err := handler(w, r); err != nil {
            // handle the error, display to user, assuming the error allows it.
        }
    }
}

USAGE:

mux.HandleFunc("/something", ErrorHandler(func(w http.ResponseWriter, r *http.Request) error {
    if ( ... ) {
        return fmt.Errorf("something broke")
    }
    return nil
}))

@VojtechVitek
Copy link
Contributor

@koote Yes, exactly. An example from a recent HTTP logger prototype:

  1. Set context storage in top-level middleware:
    https://github.com/golang-cz/httplog/blob/da3963e3a9a0f42e74933eb1ddb9bd4d03a47e21/logger.go#L87

  2. And then later you can write to that storage from within downstream middleware / handlers:
    https://github.com/golang-cz/httplog/blob/da3963e3a9a0f42e74933eb1ddb9bd4d03a47e21/_example/main.go#L82
    https://github.com/golang-cz/httplog/blob/da3963e3a9a0f42e74933eb1ddb9bd4d03a47e21/context.go#L20

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants