diff --git a/README.md b/README.md index 8ca3ee5..d9e48ab 100644 --- a/README.md +++ b/README.md @@ -422,6 +422,52 @@ result, _ := nexus.StartOperation(ctx, client, operation, MyInput{Field: "value" fmt.Println("got result with backlinks", result.Links) ``` +### Middleware + +The ServiceRegistry supports middleware registration via the `Use` method. The registry's handler will invoke every +registered middleware in registration order. Typical use cases for middleware include global enforcement of +authorization and logging. + +Middleware is implemented as a function that takes the current context and the next handler in the invocation chain and +returns a new handler to invoke. The function can pass through the given handler or return an error to abort the +execution. The registered middleware function has access to common handler information such as the current service, +operation, and request headers. To get access to more specific handler method information, such as inputs and operation +tokens, wrap the given handler. + +**Example** + +```go +type loggingOperation struct { + nexus.UnimplementedOperation[any, any] // All OperationHandlers must embed this. + next nexus.OperationHandler[any, any] +} + +func (lo *loggingOperation) Start(ctx context.Context, input any, options nexus.StartOperationOptions) (nexus.HandlerStartOperationResult[any], error) { + log.Println("starting operation", ExtractHandlerInfo(ctx).Operation) + return lo.next.Start(ctx, input, options) +} + +func (lo *loggingOperation) GetResult(ctx context.Context, token string, options nexus.GetOperationResultOptions) (any, error) { + log.Println("getting result for operation", ExtractHandlerInfo(ctx).Operation) + return lo.next.GetResult(ctx, token, options) +} + +func (lo *loggingOperation) Cancel(ctx context.Context, token string, options nexus.CancelOperationOptions) error { + log.Printf("canceling operation", ExtractHandlerInfo(ctx).Operation) + return lo.next.Cancel(ctx, token, options) +} + +func (lo *loggingOperation) GetInfo(ctx context.Context, token string, options nexus.GetOperationInfoOptions) (*nexus.OperationInfo, error) { + log.Println("getting info for operation", ExtractHandlerInfo(ctx).Operation) + return lo.next.GetInfo(ctx, token, options) +} + +registry.Use(func(ctx context.Context, next nexus.OperationHandler[any, any]) (nexus.OperationHandler[any, any], error) { + // Optionally call ExtractHandlerInfo(ctx) here. + return &loggingOperation{next: next}, nil +}) +``` + ## Contributing ### Prerequisites diff --git a/nexus/handler_context_test.go b/nexus/handler_context_test.go index 234d41b..056e7a7 100644 --- a/nexus/handler_context_test.go +++ b/nexus/handler_context_test.go @@ -9,7 +9,7 @@ import ( ) func TestHandlerContext(t *testing.T) { - ctx := nexus.WithHandlerContext(context.Background()) + ctx := nexus.WithHandlerContext(context.Background(), nexus.HandlerInfo{Operation: "test"}) require.True(t, nexus.IsHandlerContext(ctx)) initial := []nexus.Link{{Type: "foo"}, {Type: "bar"}} nexus.AddHandlerLinks(ctx, initial...) @@ -18,4 +18,5 @@ func TestHandlerContext(t *testing.T) { require.Equal(t, append(initial, additional), nexus.HandlerLinks(ctx)) nexus.SetHandlerLinks(ctx, initial...) require.Equal(t, initial, nexus.HandlerLinks(ctx)) + require.Equal(t, nexus.HandlerInfo{Operation: "test"}, nexus.ExtractHandlerInfo(ctx)) } diff --git a/nexus/operation.go b/nexus/operation.go index 55d9f6e..b7f711a 100644 --- a/nexus/operation.go +++ b/nexus/operation.go @@ -64,13 +64,21 @@ type RegisterableOperation interface { // // Operation implementations must embed the [UnimplementedOperation]. // -// All Operation methods can return a [HandlerError] to fail requests with a custom [HandlerErrorType] and structured [Failure]. -// Arbitrary errors from handler methods are turned into [HandlerErrorTypeInternal],their details are logged and hidden -// from the caller. +// See [OperationHandler] for more information. type Operation[I, O any] interface { RegisterableOperation OperationReference[I, O] + OperationHandler[I, O] +} +// OperationHandler is the interface for the core operation methods. OperationHandler implementations must embed +// [UnimplementedOperation]. +// +// All Operation methods can return a [HandlerError] to fail requests with a custom [HandlerErrorType] and structured [Failure]. +// Arbitrary errors from handler methods are turned into [HandlerErrorTypeInternal], when using the Nexus SDK's +// HTTP handler, their details are logged and hidden from the caller. Other handler implementations may expose internal +// error information to callers. +type OperationHandler[I, O any] interface { // Start handles requests for starting an operation. Return [HandlerStartOperationResultSync] to respond // successfully - inline, or [HandlerStartOperationResultAsync] to indicate that an asynchronous operation was // started. Return an [OperationError] to indicate that an operation completed as failed or @@ -101,6 +109,8 @@ type Operation[I, O any] interface { // ignored by the underlying operation implemention. // 2. idempotent - implementors should ignore duplicate cancelations for the same operation. Cancel(ctx context.Context, token string, options CancelOperationOptions) error + + mustEmbedUnimplementedOperation() } type syncOperation[I, O any] struct { @@ -186,13 +196,26 @@ func (s *Service) Operation(name string) RegisterableOperation { return s.operations[name] } +// MiddlewareFunc is a function which receives an OperationHandler and returns another OperationHandler. +// If the middleware wants to stop the chain before any handler is called, it can return an error. +// +// To get [HandlerInfo] for the current handler, call [ExtractHandlerInfo] with the given context. +// +// NOTE: Experimental +type MiddlewareFunc func(ctx context.Context, next OperationHandler[any, any]) (OperationHandler[any, any], error) + // A ServiceRegistry registers services and constructs a [Handler] that dispatches operations requests to those services. type ServiceRegistry struct { - services map[string]*Service + services map[string]*Service + middleware []MiddlewareFunc } +// NewServiceRegistry constructs an empty [ServiceRegistry]. func NewServiceRegistry() *ServiceRegistry { - return &ServiceRegistry{services: make(map[string]*Service)} + return &ServiceRegistry{ + services: make(map[string]*Service), + middleware: make([]MiddlewareFunc, 0), + } } // Register one or more service. @@ -218,6 +241,15 @@ func (r *ServiceRegistry) Register(services ...*Service) error { return nil } +// Use registers one or more middleware to be applied to all operation method invocations across all registered +// services. Middleware is applied in registration order. If called multiple times, newly registered middleware will be +// applied after any previously registered ones. +// +// NOTE: Experimental +func (s *ServiceRegistry) Use(middleware ...MiddlewareFunc) { + s.middleware = append(s.middleware, middleware...) +} + // NewHandler creates a [Handler] that dispatches requests to registered operations based on their name. func (r *ServiceRegistry) NewHandler() (Handler, error) { if len(r.services) == 0 { @@ -229,76 +261,64 @@ func (r *ServiceRegistry) NewHandler() (Handler, error) { } } - return ®istryHandler{services: r.services}, nil + return ®istryHandler{services: r.services, middlewares: r.middleware}, nil } type registryHandler struct { UnimplementedHandler - services map[string]*Service + services map[string]*Service + middlewares []MiddlewareFunc } -// CancelOperation implements Handler. -func (r *registryHandler) CancelOperation(ctx context.Context, service, operation string, token string, options CancelOperationOptions) error { - s, ok := r.services[service] +func (r *registryHandler) operationHandler(ctx context.Context) (OperationHandler[any, any], error) { + options := ExtractHandlerInfo(ctx) + s, ok := r.services[options.Service] if !ok { - return HandlerErrorf(HandlerErrorTypeNotFound, "service %q not found", service) + return nil, HandlerErrorf(HandlerErrorTypeNotFound, "service %q not found", options.Service) } - h, ok := s.operations[operation] + h, ok := s.operations[options.Operation] if !ok { - return HandlerErrorf(HandlerErrorTypeNotFound, "operation %q not found", operation) + return nil, HandlerErrorf(HandlerErrorTypeNotFound, "operation %q not found", options.Operation) } - // NOTE: We could avoid reflection here if we put the Cancel method on RegisterableOperation but it doesn't seem - // worth it since we need reflection for the generic methods. - m, _ := reflect.TypeOf(h).MethodByName("Cancel") - values := m.Func.Call([]reflect.Value{reflect.ValueOf(h), reflect.ValueOf(ctx), reflect.ValueOf(token), reflect.ValueOf(options)}) - if values[0].IsNil() { - return nil + var handler OperationHandler[any, any] + handler = &rootOperationHandler{h: h} + for i := len(r.middlewares) - 1; i >= 0; i-- { + var err error + handler, err = r.middlewares[i](ctx, handler) + if err != nil { + return nil, err + } } - return values[0].Interface().(error) + return handler, nil } -// GetOperationInfo implements Handler. -func (r *registryHandler) GetOperationInfo(ctx context.Context, service, operation string, token string, options GetOperationInfoOptions) (*OperationInfo, error) { - s, ok := r.services[service] - if !ok { - return nil, HandlerErrorf(HandlerErrorTypeNotFound, "service %q not found", service) - } - h, ok := s.operations[operation] - if !ok { - return nil, HandlerErrorf(HandlerErrorTypeNotFound, "operation %q not found", operation) - } - - // NOTE: We could avoid reflection here if we put the Cancel method on RegisterableOperation but it doesn't seem - // worth it since we need reflection for the generic methods. - m, _ := reflect.TypeOf(h).MethodByName("GetInfo") - values := m.Func.Call([]reflect.Value{reflect.ValueOf(h), reflect.ValueOf(ctx), reflect.ValueOf(token), reflect.ValueOf(options)}) - if !values[1].IsNil() { - return nil, values[1].Interface().(error) +// CancelOperation implements Handler. +func (r *registryHandler) CancelOperation(ctx context.Context, service, operation, token string, options CancelOperationOptions) error { + h, err := r.operationHandler(ctx) + if err != nil { + return err } - ret := values[0].Interface() - return ret.(*OperationInfo), nil + return h.Cancel(ctx, token, options) } -// GetOperationResult implements Handler. -func (r *registryHandler) GetOperationResult(ctx context.Context, service, operation string, token string, options GetOperationResultOptions) (any, error) { - s, ok := r.services[service] - if !ok { - return nil, HandlerErrorf(HandlerErrorTypeNotFound, "service %q not found", service) - } - h, ok := s.operations[operation] - if !ok { - return nil, HandlerErrorf(HandlerErrorTypeNotFound, "operation %q not found", operation) +// operationHandlerInfo implements Handler. +func (r *registryHandler) GetOperationInfo(ctx context.Context, service, operation, token string, options GetOperationInfoOptions) (*OperationInfo, error) { + h, err := r.operationHandler(ctx) + if err != nil { + return nil, err } + return h.GetInfo(ctx, token, options) +} - m, _ := reflect.TypeOf(h).MethodByName("GetResult") - values := m.Func.Call([]reflect.Value{reflect.ValueOf(h), reflect.ValueOf(ctx), reflect.ValueOf(token), reflect.ValueOf(options)}) - if !values[1].IsNil() { - return nil, values[1].Interface().(error) +// operationHandlerResult implements Handler. +func (r *registryHandler) GetOperationResult(ctx context.Context, service, operation, token string, options GetOperationResultOptions) (any, error) { + h, err := r.operationHandler(ctx) + if err != nil { + return nil, err } - ret := values[0].Interface() - return ret, nil + return h.GetResult(ctx, token, options) } // StartOperation implements Handler. @@ -307,29 +327,72 @@ func (r *registryHandler) StartOperation(ctx context.Context, service, operation if !ok { return nil, HandlerErrorf(HandlerErrorTypeNotFound, "service %q not found", service) } - h, ok := s.operations[operation] + ro, ok := s.operations[operation] if !ok { return nil, HandlerErrorf(HandlerErrorTypeNotFound, "operation %q not found", operation) } - m, _ := reflect.TypeOf(h).MethodByName("Start") + h, err := r.operationHandler(ctx) + if err != nil { + return nil, err + } + m, _ := reflect.TypeOf(ro).MethodByName("Start") inputType := m.Type.In(2) iptr := reflect.New(inputType).Interface() if err := input.Consume(iptr); err != nil { // TODO: log the error? Do we need to accept a logger for this single line? return nil, HandlerErrorf(HandlerErrorTypeBadRequest, "invalid input") } - i := reflect.ValueOf(iptr).Elem() + return h.Start(ctx, reflect.ValueOf(iptr).Elem().Interface(), options) +} - values := m.Func.Call([]reflect.Value{reflect.ValueOf(h), reflect.ValueOf(ctx), i, reflect.ValueOf(options)}) +type rootOperationHandler struct { + UnimplementedOperation[any, any] + h RegisterableOperation +} + +func (r *rootOperationHandler) Cancel(ctx context.Context, token string, options CancelOperationOptions) error { + // NOTE: We could avoid reflection here if we put the Cancel method on RegisterableOperation but it doesn't seem + // worth it since we need reflection for the generic methods. + m, _ := reflect.TypeOf(r.h).MethodByName("Cancel") + values := m.Func.Call([]reflect.Value{reflect.ValueOf(r.h), reflect.ValueOf(ctx), reflect.ValueOf(token), reflect.ValueOf(options)}) + if values[0].IsNil() { + return nil + } + return values[0].Interface().(error) +} + +func (r *rootOperationHandler) GetInfo(ctx context.Context, token string, options GetOperationInfoOptions) (*OperationInfo, error) { + // NOTE: We could avoid reflection here if we put the GetInfo method on RegisterableOperation but it doesn't + // seem worth it since we need reflection for the generic methods. + m, _ := reflect.TypeOf(r.h).MethodByName("GetInfo") + values := m.Func.Call([]reflect.Value{reflect.ValueOf(r.h), reflect.ValueOf(ctx), reflect.ValueOf(token), reflect.ValueOf(options)}) if !values[1].IsNil() { return nil, values[1].Interface().(error) } ret := values[0].Interface() - return ret.(HandlerStartOperationResult[any]), nil + return ret.(*OperationInfo), nil +} + +func (r *rootOperationHandler) GetResult(ctx context.Context, token string, options GetOperationResultOptions) (any, error) { + m, _ := reflect.TypeOf(r.h).MethodByName("GetResult") + values := m.Func.Call([]reflect.Value{reflect.ValueOf(r.h), reflect.ValueOf(ctx), reflect.ValueOf(token), reflect.ValueOf(options)}) + if !values[1].IsNil() { + return nil, values[1].Interface().(error) + } + ret := values[0].Interface() + return ret, nil } -var _ Handler = ®istryHandler{} +func (r *rootOperationHandler) Start(ctx context.Context, input any, options StartOperationOptions) (HandlerStartOperationResult[any], error) { + m, _ := reflect.TypeOf(r.h).MethodByName("Start") + values := m.Func.Call([]reflect.Value{reflect.ValueOf(r.h), reflect.ValueOf(ctx), reflect.ValueOf(input), reflect.ValueOf(options)}) + if !values[1].IsNil() { + return nil, values[1].Interface().(error) + } + ret := values[0].Interface() + return ret.(HandlerStartOperationResult[any]), nil +} // ExecuteOperation is the type safe version of [HTTPClient.ExecuteOperation]. // It accepts input of type I and returns output of type O, removing the need to consume the [LazyValue] returned by the diff --git a/nexus/operation_test.go b/nexus/operation_test.go index a4398fe..4a1c05a 100644 --- a/nexus/operation_test.go +++ b/nexus/operation_test.go @@ -267,3 +267,89 @@ func TestInputOutputType(t *testing.T) { require.True(t, reflect.TypeOf(3).AssignableTo(numberValidatorOperation.OutputType())) require.False(t, reflect.TypeOf("s").AssignableTo(numberValidatorOperation.OutputType())) } + +func TestOperationInterceptor(t *testing.T) { + registry := NewServiceRegistry() + svc := NewService(testService) + require.NoError(t, svc.Register( + asyncNumberValidatorOperationInstance, + )) + + var logs []string + // Register the logging middleware after the auth middleware to ensure the auth middleware is called first. + // any middleware that returns an error will prevent the operation from being called. + registry.Use(newAuthMiddleware("auth-key"), newLoggingMiddleware(func(log string) { + logs = append(logs, log) + })) + require.NoError(t, registry.Register(svc)) + + handler, err := registry.NewHandler() + require.NoError(t, err) + + ctx, client, teardown := setup(t, handler) + defer teardown() + + _, err = StartOperation(ctx, client, asyncNumberValidatorOperationInstance, 3, StartOperationOptions{}) + require.ErrorContains(t, err, "unauthorized") + + authHeader := map[string]string{"authorization": "auth-key"} + result, err := StartOperation(ctx, client, asyncNumberValidatorOperationInstance, 3, StartOperationOptions{ + Header: authHeader, + }) + require.NoError(t, err) + require.ErrorContains(t, result.Pending.Cancel(ctx, CancelOperationOptions{}), "unauthorized") + require.NoError(t, result.Pending.Cancel(ctx, CancelOperationOptions{Header: authHeader})) + // Assert the logger only contains calls from successful operations. + require.Len(t, logs, 2) + require.Contains(t, logs[0], "starting operation async-number-validator") + require.Contains(t, logs[1], "cancel operation async-number-validator") +} + +func newAuthMiddleware(authKey string) MiddlewareFunc { + return func(ctx context.Context, next OperationHandler[any, any]) (OperationHandler[any, any], error) { + info := ExtractHandlerInfo(ctx) + if info.Header.Get("authorization") != authKey { + return nil, HandlerErrorf(HandlerErrorTypeUnauthorized, "unauthorized") + } + return next, nil + } +} + +type loggingOperation struct { + UnimplementedOperation[any, any] + Operation OperationHandler[any, any] + name string + output func(string) +} + +func (lo *loggingOperation) Start(ctx context.Context, input any, options StartOperationOptions) (HandlerStartOperationResult[any], error) { + lo.output(fmt.Sprintf("starting operation %s", lo.name)) + return lo.Operation.Start(ctx, input, options) +} + +func (lo *loggingOperation) GetResult(ctx context.Context, id string, options GetOperationResultOptions) (any, error) { + lo.output(fmt.Sprintf("getting result for operation %s", lo.name)) + return lo.Operation.GetResult(ctx, id, options) +} + +func (lo *loggingOperation) Cancel(ctx context.Context, id string, options CancelOperationOptions) error { + lo.output(fmt.Sprintf("cancel operation %s", lo.name)) + return lo.Operation.Cancel(ctx, id, options) +} + +func (lo *loggingOperation) GetInfo(ctx context.Context, id string, options GetOperationInfoOptions) (*OperationInfo, error) { + lo.output(fmt.Sprintf("getting info for operation %s", lo.name)) + return lo.Operation.GetInfo(ctx, id, options) +} + +func newLoggingMiddleware(output func(string)) MiddlewareFunc { + return func(ctx context.Context, next OperationHandler[any, any]) (OperationHandler[any, any], error) { + info := ExtractHandlerInfo(ctx) + + return &loggingOperation{ + Operation: next, + name: info.Operation, + output: output, + }, nil + } +} diff --git a/nexus/server.go b/nexus/server.go index 205dcaf..d38b5e6 100644 --- a/nexus/server.go +++ b/nexus/server.go @@ -21,22 +21,35 @@ type handlerCtxKeyType struct{} var handlerCtxKey = handlerCtxKeyType{} +// HandlerInfo contains the general information for an operation invocation, across different handler methods. +// +// NOTE: Experimental +type HandlerInfo struct { + // Service is the name of the service that contains the operation. + Service string + // Operation is the name of the operation. + Operation string + // Header contains the request header fields received by the server. + Header Header +} + type handlerCtx struct { mu sync.Mutex links []Link + info HandlerInfo } // WithHandlerContext returns a new context from a given context setting it up for being used for handler methods. // Meant to be used by frameworks, not directly by applications. // // NOTE: Experimental -func WithHandlerContext(ctx context.Context) context.Context { - return context.WithValue(ctx, handlerCtxKey, &handlerCtx{}) +func WithHandlerContext(ctx context.Context, info HandlerInfo) context.Context { + return context.WithValue(ctx, handlerCtxKey, &handlerCtx{info: info}) } -// IsHandlerContext returns true if the given context is a handler context where [AddHandlerLinks] and [HandlerLinks] -// can be called. It will only return true when called from an [Operation] handler Start method or from a [Handler] -// StartOperation method. +// IsHandlerContext returns true if the given context is a handler context where [ExtractHandlerInfo], [AddHandlerLinks] +// and [HandlerLinks] can be called. It returns true when called from any [OperationHandler] or [Handler] method or a +// [MiddlewareFunc]. // // NOTE: Experimental func IsHandlerContext(ctx context.Context) bool { @@ -44,8 +57,9 @@ func IsHandlerContext(ctx context.Context) bool { } // HandlerLinks retrieves the attached links on the given handler context. The returned slice should not be mutated. -// The context provided must be the context passed to the handler or this method will panic, [IsHandlerContext] can be -// used to verify the context is valid. +// Links are only attached on successful responses to the StartOperation [Handler] and Start [OperationHandler] methods. +// The context provided must be the context passed to any [OperationHandler] or [Handler] method or a [MiddlewareFunc] +// or this method will panic, [IsHandlerContext] can be used to verify the context is valid. // // NOTE: Experimental func HandlerLinks(ctx context.Context) []Link { @@ -57,11 +71,11 @@ func HandlerLinks(ctx context.Context) []Link { return cpy } -// AddHandlerLinks associates links with the current operation to be propagated back to the caller. This method -// Can be called from an [Operation] handler Start method or from a [Handler] StartOperation method. The context -// provided must be the context passed to the handler or this method will panic, [IsHandlerContext] can be used to -// verify the context is valid. This method may be called multiple times for a given handler, each call appending -// additional links. Links will only be attached on successful responses. +// AddHandlerLinks associates links with the current operation to be propagated back to the caller. This method may be +// called multiple times for a given handler, each call appending additional links. Links are only attached on +// successful responses to the StartOperation [Handler] and Start [OperationHandler] methods. The context provided must +// be the context passed to any [OperationHandler] or [Handler] method or a [MiddlewareFunc] or this method will panic, +// [IsHandlerContext] can be used to verify the context is valid. // // NOTE: Experimental func AddHandlerLinks(ctx context.Context, links ...Link) { @@ -71,11 +85,11 @@ func AddHandlerLinks(ctx context.Context, links ...Link) { hctx.mu.Unlock() } -// SetHandlerLinks associates links with the current operation to be propagated back to the caller. This method -// Can be called from an [Operation] handler Start method or from a [Handler] StartOperation method. The context -// provided must be the context passed to the handler or this method will panic, [IsHandlerContext] can be used to -// verify the context is valid. This method replaces any previously associated links, it is recommended to use -// [AddHandlerLinks] to avoid accidental override. Links will only be attached on successful responses. +// SetHandlerLinks associates links with the current operation to be propagated back to the caller. This method replaces +// any previously associated links, it is recommended to use [AddHandlerLinks] to avoid accidental override. Links are +// only attached on successful responses to the StartOperation [Handler] and Start [OperationHandler] methods. The +// context provided must be the context passed to any [OperationHandler] or [Handler] method or a [MiddlewareFunc] or +// this method will panic, [IsHandlerContext] can be used to verify the context is valid. // // NOTE: Experimental func SetHandlerLinks(ctx context.Context, links ...Link) { @@ -85,6 +99,16 @@ func SetHandlerLinks(ctx context.Context, links ...Link) { hctx.mu.Unlock() } +// ExtractHandlerInfo extracts the [HandlerInfo] from a given context. The context provided must be the context passed +// to any [OperationHandler] or [Handler] method or a [MiddlewareFunc] or this method will panic, [IsHandlerContext] can +// be used to verify the context is valid. +// +// NOTE: Experimental +func ExtractHandlerInfo(ctx context.Context) HandlerInfo { + hctx := ctx.Value(handlerCtxKey).(*handlerCtx) + return hctx.info +} + // An HandlerStartOperationResult is the return type from the [Handler] StartOperation and [Operation] Start methods. It // has two implementations: [HandlerStartOperationResultSync] and [HandlerStartOperationResultAsync]. type HandlerStartOperationResult[T any] interface { @@ -348,18 +372,21 @@ func (h *httpHandler) startOperation(service, operation string, writer http.Resp } ctx, cancel, ok := h.contextWithTimeoutFromHTTPRequest(writer, request) - hctx := &handlerCtx{} - ctx = context.WithValue(ctx, handlerCtxKey, hctx) if !ok { return } defer cancel() + ctx = WithHandlerContext(ctx, HandlerInfo{ + Service: service, + Operation: operation, + Header: options.Header, + }) response, err := h.options.Handler.StartOperation(ctx, service, operation, value, options) if err != nil { h.writeFailure(writer, err) } else { - if err := addLinksToHTTPHeader(hctx.links, writer.Header()); err != nil { + if err := addLinksToHTTPHeader(HandlerLinks(ctx), writer.Header()); err != nil { h.logger.Error("failed to serialize links into header", "error", err) // clear any previous links already written to the header writer.Header().Del(headerLink) @@ -372,10 +399,9 @@ func (h *httpHandler) startOperation(service, operation string, writer http.Resp func (h *httpHandler) getOperationResult(service, operation, token string, writer http.ResponseWriter, request *http.Request) { options := GetOperationResultOptions{Header: httpHeaderToNexusHeader(request.Header)} - + ctx := request.Context() // If both Request-Timeout http header and wait query string are set, the minimum of the Request-Timeout header // and h.options.GetResultTimeout will be used. - ctx := request.Context() requestTimeout, ok := h.parseRequestTimeoutHeader(writer, request) if !ok { return @@ -401,6 +427,11 @@ func (h *httpHandler) getOperationResult(service, operation, token string, write defer cancel() } + ctx = WithHandlerContext(ctx, HandlerInfo{ + Service: service, + Operation: operation, + Header: options.Header, + }) result, err := h.options.Handler.GetOperationResult(ctx, service, operation, token, options) if err != nil { if options.Wait > 0 && ctx.Err() != nil { @@ -424,6 +455,11 @@ func (h *httpHandler) getOperationInfo(service, operation, token string, writer } defer cancel() + ctx = WithHandlerContext(ctx, HandlerInfo{ + Service: service, + Operation: operation, + Header: options.Header, + }) info, err := h.options.Handler.GetOperationInfo(ctx, service, operation, token, options) if err != nil { h.writeFailure(writer, err) @@ -455,6 +491,11 @@ func (h *httpHandler) cancelOperation(service, operation, token string, writer h } defer cancel() + ctx = WithHandlerContext(ctx, HandlerInfo{ + Service: service, + Operation: operation, + Header: options.Header, + }) if err := h.options.Handler.CancelOperation(ctx, service, operation, token, options); err != nil { h.writeFailure(writer, err) return