From b6154513bba2c7d933fc6c3fcd38706050f461cd Mon Sep 17 00:00:00 2001 From: tekkaya <86028633+tekkaya@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:35:59 -0700 Subject: [PATCH] Report missing activity timeout as BAD_REQUEST inside a Nexus operation handler Previously, building StartActivityOptions with neither scheduleToCloseTimeout nor startToCloseTimeout set always threw a plain IllegalArgumentException. When that build() call happens inside a Nexus operation handler, that exception isn't a recognized Nexus error type, so NexusTaskHandlerImpl's catch-all surfaced it as an opaque HandlerException(INTERNAL), which the caller retries until the operation's schedule-to-close timeout expires instead of failing fast. NexusTaskHandlerImpl.convertKnownFailures now maps any IllegalArgumentException raised by handler code to a real HandlerException(BAD_REQUEST), matching the pattern already used for header/operation-token parsing elsewhere in this class and in TemporalOperationHandler/WorkflowRunOperationImpl. This fixes the missing-timeout case without teaching the general-purpose, Nexus-agnostic StartActivityOptions builder about Nexus, and it also covers every other IllegalArgumentException a Nexus operation handler can throw (missing id, empty task queue, negative start delay, etc.) with one change, regardless of whether the handler is built via TemporalOperationHandler or a raw/sync OperationHandler. --- .../internal/nexus/NexusTaskHandlerImpl.java | 3 ++ .../nexus/NexusTaskHandlerImplTest.java | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/temporal-sdk/src/main/java/io/temporal/internal/nexus/NexusTaskHandlerImpl.java b/temporal-sdk/src/main/java/io/temporal/internal/nexus/NexusTaskHandlerImpl.java index 4d40183c27..c0a538da26 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/nexus/NexusTaskHandlerImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/nexus/NexusTaskHandlerImpl.java @@ -200,6 +200,9 @@ private void convertKnownFailures(Throwable e) { } throw new HandlerException(HandlerException.ErrorType.BAD_REQUEST, failure); } + if (failure instanceof IllegalArgumentException) { + throw new HandlerException(HandlerException.ErrorType.BAD_REQUEST, failure); + } if (failure instanceof ApplicationFailure) { if (((ApplicationFailure) failure).isNonRetryable()) { throw new HandlerException( diff --git a/temporal-sdk/src/test/java/io/temporal/internal/nexus/NexusTaskHandlerImplTest.java b/temporal-sdk/src/test/java/io/temporal/internal/nexus/NexusTaskHandlerImplTest.java index 2ddfa7fda1..5f71fb158e 100644 --- a/temporal-sdk/src/test/java/io/temporal/internal/nexus/NexusTaskHandlerImplTest.java +++ b/temporal-sdk/src/test/java/io/temporal/internal/nexus/NexusTaskHandlerImplTest.java @@ -162,6 +162,34 @@ public void startTaskWithUndeserializableInput() throws TimeoutException { Assert.assertFalse(e.isRetryable()); } + @Test + public void startTaskHandlerMissingBothActivityTimeoutOptions() throws TimeoutException { + WorkflowClient client = mock(WorkflowClient.class); + NexusTaskHandlerImpl nexusTaskHandlerImpl = + new NexusTaskHandlerImpl( + client, NAMESPACE, TASK_QUEUE, dataConverter, new WorkerInterceptor[] {}); + nexusTaskHandlerImpl.registerNexusServiceImplementations( + new Object[] {new ThrowingIllegalArgumentServiceImpl()}); + nexusTaskHandlerImpl.start(); + + PollNexusTaskQueueResponse.Builder task = + PollNexusTaskQueueResponse.newBuilder() + .setRequest( + Request.newBuilder() + .setStartOperation( + StartOperationRequest.newBuilder() + .setOperation("operation") + .setService("TestNexusService1") + .setPayload(dataConverter.toPayload("input").get()) + .build())); + + NexusTaskHandler.Result result = + nexusTaskHandlerImpl.handle(new NexusTask(task, null, null), metricsScope); + HandlerException e = result.getHandlerException(); + Assert.assertNotNull(e); + Assert.assertEquals(HandlerException.ErrorType.BAD_REQUEST, e.getErrorType()); + } + @Test public void startAsyncSyncOperation() throws TimeoutException { WorkflowClient client = mock(WorkflowClient.class); @@ -405,6 +433,18 @@ public OperationHandler operation() { } } + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) + public class ThrowingIllegalArgumentServiceImpl { + @OperationImpl + public OperationHandler operation() { + return OperationHandler.sync( + (ctx, details, input) -> { + throw new IllegalArgumentException( + "at least one of StartToCloseTimeout or ScheduleToCloseTimeout is required"); + }); + } + } + @ServiceImpl(service = TestNexusServices.TestNexusService1.class) public class TestNexusServiceImpl { @OperationImpl