@@ -13,9 +13,30 @@ import (
1313 "net/url"
1414 "strconv"
1515 "strings"
16+ "sync"
1617 "time"
1718)
1819
20+ type handlerCtxKeyType struct {}
21+
22+ var handlerCtxKey = handlerCtxKeyType {}
23+
24+ type handlerCtx struct {
25+ mu sync.Mutex
26+ links []Link
27+ }
28+
29+ // AddHandlerLinks associates links with the current operation to be propagated back to the caller. This method
30+ // Can be called from an [Operation] handler Start method or from a [Handler] StartOperation method. The context
31+ // provided must be the context passed to the handler. This method may be called multiple times for a given handler,
32+ // each call appending additional links. Links will only be attached on successful responses.
33+ func AddHandlerLinks (ctx context.Context , links ... Link ) {
34+ hctx := ctx .Value (handlerCtxKey ).(* handlerCtx )
35+ hctx .mu .Lock ()
36+ hctx .links = append (hctx .links , links ... )
37+ hctx .mu .Unlock ()
38+ }
39+
1940// An HandlerStartOperationResult is the return type from the [Handler] StartOperation and [Operation] Start methods. It
2041// has two implementations: [HandlerStartOperationResultSync] and [HandlerStartOperationResultAsync].
2142type HandlerStartOperationResult [T any ] interface {
@@ -27,6 +48,8 @@ type HandlerStartOperationResultSync[T any] struct {
2748 // Value is the output of the operation.
2849 Value T
2950 // Links to be associated with the operation.
51+ //
52+ // Deprecated: Use AddHandlerLinks instead.
3053 Links []Link
3154}
3255
@@ -51,6 +74,8 @@ type HandlerStartOperationResultAsync struct {
5174 // OperationToken is a unique token to identify the operation.
5275 OperationToken string
5376 // Links to be associated with the operation.
77+ //
78+ // Deprecated: Use AddHandlerLinks instead.
5479 Links []Link
5580}
5681
@@ -115,8 +140,12 @@ type Handler interface {
115140 // It is the implementor's responsiblity to respect the client's wait duration and return in a timely fashion.
116141 // Consider using a derived context that enforces the wait timeout when implementing this method and return
117142 // [ErrOperationStillRunning] when that context expires as shown in the example.
143+ //
144+ // NOTE: Experimental
118145 GetOperationResult (ctx context.Context , service , operation , token string , options GetOperationResultOptions ) (any , error )
119146 // GetOperationInfo handles requests to get information about an asynchronous operation.
147+ //
148+ // NOTE: Experimental
120149 GetOperationInfo (ctx context.Context , service , operation , token string , options GetOperationInfoOptions ) (* OperationInfo , error )
121150 // CancelOperation handles requests to cancel an asynchronous operation.
122151 // Cancelation in Nexus is:
@@ -271,6 +300,8 @@ func (h *httpHandler) startOperation(service, operation string, writer http.Resp
271300 }
272301
273302 ctx , cancel , ok := h .contextWithTimeoutFromHTTPRequest (writer , request )
303+ hctx := & handlerCtx {}
304+ ctx = context .WithValue (ctx , handlerCtxKey , hctx )
274305 if ! ok {
275306 return
276307 }
@@ -280,6 +311,13 @@ func (h *httpHandler) startOperation(service, operation string, writer http.Resp
280311 if err != nil {
281312 h .writeFailure (writer , err )
282313 } else {
314+ if err := addLinksToHTTPHeader (hctx .links , writer .Header ()); err != nil {
315+ h .logger .Error ("failed to serialize links into header" , "error" , err )
316+ // clear any previous links already written to the header
317+ writer .Header ().Del (headerLink )
318+ writer .WriteHeader (http .StatusInternalServerError )
319+ return
320+ }
283321 response .applyToHTTPResponse (writer , h )
284322 }
285323}
@@ -445,9 +483,19 @@ func (h *httpHandler) handleRequest(writer http.ResponseWriter, request *http.Re
445483 h .writeFailure (writer , HandlerErrorf (HandlerErrorTypeBadRequest , "failed to parse URL path" ))
446484 return
447485 }
486+
487+ // First handle StartOperation at /{service}/{operation}
488+ if len (parts ) == 3 && request .Method == "POST" {
489+ h .startOperation (service , operation , writer , request )
490+ return
491+ }
492+
448493 token := request .Header .Get (HeaderOperationToken )
449494 if token == "" {
450495 token = request .URL .Query ().Get ("token" )
496+ } else {
497+ // Sanitize this header as it is explicitly passed in as an argument.
498+ request .Header .Del (HeaderOperationToken )
451499 }
452500
453501 if token != "" {
@@ -479,21 +527,13 @@ func (h *httpHandler) handleRequest(writer http.ResponseWriter, request *http.Re
479527 h .writeFailure (writer , HandlerErrorf (HandlerErrorTypeNotFound , "not found" ))
480528 }
481529 } else {
482- if len (parts ) > 3 {
483- token , err = url .PathUnescape (parts [3 ])
484- if err != nil {
485- h .writeFailure (writer , HandlerErrorf (HandlerErrorTypeBadRequest , "failed to parse URL path" ))
486- return
487- }
530+ token , err = url .PathUnescape (parts [3 ])
531+ if err != nil {
532+ h .writeFailure (writer , HandlerErrorf (HandlerErrorTypeBadRequest , "failed to parse URL path" ))
533+ return
488534 }
489535
490536 switch len (parts ) {
491- case 3 : // /{service}/{operation}
492- if request .Method != "POST" {
493- h .writeFailure (writer , HandlerErrorf (HandlerErrorTypeBadRequest , "invalid request method: expected POST, got %q" , request .Method ))
494- return
495- }
496- h .startOperation (service , operation , writer , request )
497537 case 4 : // /{service}/{operation}/{operation_id}
498538 if request .Method != "GET" {
499539 h .writeFailure (writer , HandlerErrorf (HandlerErrorTypeBadRequest , "invalid request method: expected GET, got %q" , request .Method ))
0 commit comments