Skip to content

Commit 25b1053

Browse files
committed
Responding to PR comments
1 parent 6916b28 commit 25b1053

5 files changed

Lines changed: 67 additions & 84 deletions

File tree

src/Temporalio/Nexus/ProtoLinkExtensions.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,21 @@ public static Api.Common.V1.Link.Types.WorkflowEvent ToWorkflowEvent(this NexusL
299299
return evt;
300300
}
301301

302-
// Simple query param parser because .NET stdlib doesn't have one in all versions.
302+
/// <summary>
303+
/// Parse a URI query string into its parameters. Simple hand-rolled parser because the .NET
304+
/// stdlib does not have one in all versions we target.
305+
/// </summary>
306+
/// <param name="uri">URI whose query string to parse.</param>
307+
/// <returns>
308+
/// The query parameters, with both keys and values percent-decoded. Values are additionally
309+
/// form-decoded, i.e. <c>+</c> becomes a space, since that is how a query value is encoded on
310+
/// the way out; the replacement is safe to do before percent-decoding because a literal
311+
/// <c>+</c> is encoded as <c>%2B</c>. A parameter present with no <c>=</c> maps to an empty
312+
/// string, so callers cannot distinguish <c>?reason</c> from <c>?reason=</c>. Keys are looked
313+
/// up with the dictionary's default ordinal comparer, so lookups are case-sensitive and
314+
/// <c>reason</c> and <c>Reason</c> are different parameters. A query repeating a key throws,
315+
/// which callers treat as an invalid link like any other malformed URI.
316+
/// </returns>
303317
private static Dictionary<string, string> ParseQueryParams(Uri uri) =>
304318
uri.Query.
305319
TrimStart('?').

src/Temporalio/Worker/NexusWorker.cs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -389,16 +389,23 @@ private HandlerException ConvertToHandlerException(Exception exc)
389389
}
390390
else if (exc is WorkflowQueryFailedException)
391391
{
392-
// A query handler that threw will throw again on every attempt, so this must be a
393-
// non-retryable type. Falling through to the Internal default below would make the
394-
// server retry the operation until it times out instead of failing it.
395-
return new(HandlerErrorType.BadRequest, "Workflow query failed", exc);
392+
// The query handler faulted rather than the request being bad, and it will fault the
393+
// same way on every attempt, so Internal must be marked non-retryable explicitly.
394+
return new(
395+
HandlerErrorType.Internal,
396+
"Workflow query failed",
397+
exc,
398+
HandlerErrorRetryBehavior.NonRetryable);
396399
}
397400
else if (exc is WorkflowQueryRejectedException)
398401
{
399-
// Rejection is a property of the workflow's state against the reject condition, so
400-
// retrying cannot change the outcome either.
401-
return new(HandlerErrorType.BadRequest, "Workflow query rejected", exc);
402+
// Rejection follows from the queried workflow's status, so as above this is not a
403+
// caller error and retrying cannot change the outcome.
404+
return new(
405+
HandlerErrorType.Internal,
406+
"Workflow query rejected",
407+
exc,
408+
HandlerErrorRetryBehavior.NonRetryable);
402409
}
403410
else if (exc is ApplicationFailureException appExc && appExc.NonRetryable)
404411
{

tests/Temporalio.Tests/Nexus/NexusWorkflowUpdateHandleTests.cs

Lines changed: 0 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ namespace Temporalio.Tests.Nexus;
33
using System.Linq;
44
using System.Text;
55
using System.Text.Json;
6-
using Temporalio.Api.Common.V1;
76
using Temporalio.Nexus;
87
using Xunit;
98

@@ -207,71 +206,4 @@ public void FromToken_ToleratesAbsentRid()
207206
Assert.Equal(string.Empty, handle.RunId);
208207
Assert.Equal("u", handle.UpdateId);
209208
}
210-
211-
[Fact]
212-
public void CommonLink_ToNexusLink_UnsetOneof_ReturnsNull()
213-
{
214-
// A link whose oneof is unset (neither workflow-event nor workflow) must not dereference a
215-
// null variant; it returns null so callers can skip it.
216-
var link = new Link().ToNexusLink();
217-
218-
Assert.Null(link);
219-
}
220-
221-
[Fact]
222-
public void WorkflowLink_ToNexusLink_BuildsWorkflowUri()
223-
{
224-
// A workflow link addresses the execution itself, so there is no "/history" suffix. The
225-
// suffix belongs to the workflow-event form, and its absence is what distinguishes the two.
226-
var workflow = new Link.Types.Workflow
227-
{
228-
Namespace = "ns",
229-
WorkflowId = "wid",
230-
RunId = "rid",
231-
};
232-
var link = workflow.ToNexusLink();
233-
234-
Assert.Equal("temporal", link.Uri.Scheme);
235-
Assert.Equal("/namespaces/ns/workflows/wid/rid", link.Uri.AbsolutePath);
236-
Assert.Equal(Link.Types.Workflow.Descriptor.FullName, link.Type);
237-
}
238-
239-
[Fact]
240-
public void CommonLink_ToNexusLink_PrefersWorkflowEvent()
241-
{
242-
var common = new Link
243-
{
244-
WorkflowEvent = new()
245-
{
246-
Namespace = "ns",
247-
WorkflowId = "wid",
248-
RunId = "rid",
249-
EventRef = new() { EventId = 1 },
250-
},
251-
};
252-
var link = common.ToNexusLink();
253-
254-
Assert.NotNull(link);
255-
Assert.Equal(Link.Types.WorkflowEvent.Descriptor.FullName, link.Type);
256-
}
257-
258-
[Fact]
259-
public void CommonLink_ToNexusLink_FallsBackToWorkflow()
260-
{
261-
// No history event (e.g. a rejected update) — falls back to the workflow link.
262-
var common = new Link
263-
{
264-
Workflow = new()
265-
{
266-
Namespace = "ns",
267-
WorkflowId = "wid",
268-
RunId = "rid",
269-
},
270-
};
271-
var link = common.ToNexusLink();
272-
273-
Assert.NotNull(link);
274-
Assert.Equal(Link.Types.Workflow.Descriptor.FullName, link.Type);
275-
Assert.Equal("/namespaces/ns/workflows/wid/rid", link.Uri.AbsolutePath);
276-
}
277209
}

tests/Temporalio.Tests/Nexus/ProtoLinkExtensionsTests.cs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,31 @@ public void ToWorkflow_ReasonPrefixKeyIgnored()
384384
Assert.Equal(string.Empty, link.ToWorkflow().Reason);
385385
}
386386

387+
[Fact]
388+
public void ToWorkflow_ReasonKeyLookupIsCaseSensitive()
389+
{
390+
// Query params are held in a dictionary with the default ordinal comparer, so "Reason" is a
391+
// different param than "reason" and does not populate the field.
392+
var link = new NexusLink(
393+
new Uri("temporal:///namespaces/ns/workflows/wf-id/run-id?Reason=nope"),
394+
Api.Common.V1.Link.Types.Workflow.Descriptor.FullName);
395+
396+
Assert.Equal(string.Empty, link.ToWorkflow().Reason);
397+
}
398+
399+
[Fact]
400+
public void ToWorkflow_RepeatedReasonKey_Throws()
401+
{
402+
// A repeated key cannot go into the dictionary, so it surfaces as the same ArgumentException
403+
// as any other malformed link. Callers converting links catch that and drop the link with a
404+
// warning rather than failing the operation.
405+
var link = new NexusLink(
406+
new Uri("temporal:///namespaces/ns/workflows/wf-id/run-id?reason=a&reason=b"),
407+
Api.Common.V1.Link.Types.Workflow.Descriptor.FullName);
408+
409+
Assert.Throws<ArgumentException>(() => link.ToWorkflow());
410+
}
411+
387412
[Fact]
388413
public void ToWorkflow_LiteralPlusInPathIsPreserved()
389414
{
@@ -430,6 +455,8 @@ public void ToProtoLink_WorkflowShape_RoundTrips()
430455
[Fact]
431456
public void ProtoToNexusLink_WorkflowVariant_Dispatches()
432457
{
458+
// The workflow variant is what a link carries when there is no history event to point at,
459+
// e.g. a Query or a rejected update.
433460
var protoLink = new Api.Common.V1.Link
434461
{
435462
Workflow = new() { Namespace = "ns", WorkflowId = "wf", RunId = "run" },
@@ -500,11 +527,12 @@ public void ToWorkflowEvent_FormDecodesQueryValues()
500527
[Fact]
501528
public void ToWorkflow_AcceptsEmptyRunId()
502529
{
503-
// Characterization, not a statement of intent. A trailing slash still yields the expected
504-
// segment count, so the run ID comes back empty rather than being rejected. The same
505-
// leniency exists in the workflow-event, activity, and nexus-operation converters, since
506-
// they share this path parsing, so tightening it is a decision about all four rather than
507-
// about this converter.
530+
// A trailing slash still yields the expected segment count, so the run ID comes back empty
531+
// rather than being rejected. The workflow-event, activity, and nexus-operation converters
532+
// are equally lenient because all four share this path parsing, so tightening it is a
533+
// decision about all four and belongs with that change rather than this one. This test
534+
// exists so that whichever way it is decided, the behavior changes visibly instead of
535+
// silently.
508536
var link = new NexusLink(
509537
new Uri("temporal:///namespaces/ns/workflows/wf-id/"),
510538
Api.Common.V1.Link.Types.Workflow.Descriptor.FullName);
@@ -515,7 +543,7 @@ public void ToWorkflow_AcceptsEmptyRunId()
515543
[Fact]
516544
public void ToWorkflow_AcceptsEmptyNamespace()
517545
{
518-
// Characterization; see ToWorkflow_AcceptsEmptyRunId.
546+
// The same shared-path-parsing leniency as ToWorkflow_AcceptsEmptyRunId; see that test.
519547
var link = new NexusLink(
520548
new Uri("temporal:///namespaces//workflows/wf-id/run-id"),
521549
Api.Common.V1.Link.Types.Workflow.Descriptor.FullName);

tests/Temporalio.Tests/Worker/NexusQueryOperationTests.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,9 @@ await RunWithCounterAsync(async (endpoint, taskQueue, counter) =>
151151
{
152152
var caller = await RunCallerAsync(
153153
taskQueue, endpoint, new(counter.Id, Fail: true));
154-
await AssertOperationFailedWithAsync(HandlerErrorType.BadRequest, caller);
154+
// The query handler faulted, not the Nexus request, so this is Internal and explicitly
155+
// non-retryable rather than BadRequest.
156+
await AssertOperationFailedWithAsync(HandlerErrorType.Internal, caller);
155157
});
156158
}
157159

@@ -180,7 +182,7 @@ await worker.ExecuteAsync(async () =>
180182

181183
var caller = await RunCallerAsync(
182184
taskQueue, endpointName, new(counter.Id, RejectNotOpen: true));
183-
await AssertOperationFailedWithAsync(HandlerErrorType.BadRequest, caller);
185+
await AssertOperationFailedWithAsync(HandlerErrorType.Internal, caller);
184186
});
185187
}
186188

0 commit comments

Comments
 (0)