Skip to content

Commit 37755b4

Browse files
authored
Prep for stable API (#42)
- [Remove EXPERIMENTAL warning from README and mark specific exports as experimental](694a754) - [Sanitize token header](1f6799b) - [Add AddHandlerLinks method and deprecate links on result objects](8f448a7)
1 parent 979c946 commit 37755b4

File tree

6 files changed

+64
-16
lines changed

6 files changed

+64
-16
lines changed

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55

66
Client and server package for working with the Nexus [HTTP API](https://github.com/nexus-rpc/api).
77

8-
**⚠️ EXPERIMENTAL ⚠️**
9-
108
## What is Nexus?
119

1210
Nexus is a synchronous RPC protocol. Arbitrary length operations are modelled on top of a set of pre-defined synchronous

nexus/api.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,8 @@ func (e *HandlerError) Unwrap() error {
288288
}
289289

290290
// ErrOperationStillRunning indicates that an operation is still running while trying to get its result.
291+
//
292+
// NOTE: Experimental
291293
var ErrOperationStillRunning = errors.New("operation still running")
292294

293295
// OperationInfo conveys information about an operation.

nexus/handle.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ type OperationHandle[T any] struct {
2525
}
2626

2727
// GetInfo gets operation information, issuing a network request to the service handler.
28+
//
29+
// NOTE: Experimental
2830
func (h *OperationHandle[T]) GetInfo(ctx context.Context, options GetOperationInfoOptions) (*OperationInfo, error) {
2931
var u *url.URL
3032
if h.client.options.UseOperationID {
@@ -77,6 +79,8 @@ func (h *OperationHandle[T]) GetInfo(ctx context.Context, options GetOperationIn
7779
// context deadline to the max allowed wait period to ensure this call returns in a timely fashion.
7880
//
7981
// ⚠️ If a [LazyValue] is returned (as indicated by T), it must be consumed to free up the underlying connection.
82+
//
83+
// NOTE: Experimental
8084
func (h *OperationHandle[T]) GetResult(ctx context.Context, options GetOperationResultOptions) (T, error) {
8185
var result T
8286
var u *url.URL

nexus/operation.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,12 @@ type Operation[I, O any] interface {
8888
// It is the implementor's responsiblity to respect the client's wait duration and return in a timely fashion.
8989
// Consider using a derived context that enforces the wait timeout when implementing this method and return
9090
// [ErrOperationStillRunning] when that context expires as shown in the [Handler] example.
91+
//
92+
// NOTE: Experimental
9193
GetResult(ctx context.Context, token string, options GetOperationResultOptions) (O, error)
9294
// GetInfo handles requests to get information about an asynchronous operation.
95+
//
96+
// NOTE: Experimental
9397
GetInfo(ctx context.Context, token string, options GetOperationInfoOptions) (*OperationInfo, error)
9498
// Cancel handles requests to cancel an asynchronous operation.
9599
// Cancelation in Nexus is:

nexus/server.go

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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].
2142
type 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))

nexus/start_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@ func TestClientRequestID(t *testing.T) {
134134
t.Run(c.name, func(t *testing.T) {
135135
result, err := client.StartOperation(ctx, "foo", nil, c.request)
136136
require.NoError(t, err)
137-
require.Equal(t, c.request.Links, result.Links)
138137
response := result.Successful
139138
require.NotNil(t, response)
140139
var responseBody []byte
@@ -190,7 +189,8 @@ func (h *echoHandler) StartOperation(ctx context.Context, service, operation str
190189
Data: data,
191190
}
192191
}
193-
return &HandlerStartOperationResultSync[any]{Value: output, Links: options.Links}, nil
192+
AddHandlerLinks(ctx, options.Links...)
193+
return &HandlerStartOperationResultSync[any]{Value: output}, nil
194194
}
195195

196196
func TestReaderIO(t *testing.T) {

0 commit comments

Comments
 (0)