From 47e0bcc8a63c9420755b615fcf98341df1c20f5d Mon Sep 17 00:00:00 2001 From: Sebastian Bruckner Date: Fri, 9 Oct 2026 14:39:16 +0200 Subject: [PATCH] Send temporal-namespace header from ActivityClient WorkflowClient, ScheduleClient and NexusClient wrap their service stubs in NamespaceInjectWorkflowServiceStubs so every call carries the temporal-namespace header. ActivityClient did not, so Standalone Activity calls such as StartActivityExecution went out without it, and Temporal Cloud rejects them for API-key authentication with PERMISSION_DENIED: Request unauthorized. Co-Authored-By: Claude Opus 5.5 --- .../temporal/client/ActivityClientImpl.java | 2 ++ .../authorization/AuthorizationTokenTest.java | 34 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/temporal-sdk/src/main/java/io/temporal/client/ActivityClientImpl.java b/temporal-sdk/src/main/java/io/temporal/client/ActivityClientImpl.java index 77bf2bd794..f73c80db9e 100644 --- a/temporal-sdk/src/main/java/io/temporal/client/ActivityClientImpl.java +++ b/temporal-sdk/src/main/java/io/temporal/client/ActivityClientImpl.java @@ -8,6 +8,7 @@ import io.temporal.common.interceptors.Header; import io.temporal.internal.client.ActivityClientInternal; import io.temporal.internal.client.ActivityHandleImpl; +import io.temporal.internal.client.NamespaceInjectWorkflowServiceStubs; import io.temporal.internal.client.RootActivityClientInvoker; import io.temporal.internal.client.external.GenericWorkflowClientImpl; import io.temporal.internal.client.external.ManualActivityCompletionClientFactory; @@ -37,6 +38,7 @@ class ActivityClientImpl implements ActivityClient, ActivityClientInternal { private final Scope metricsScope; ActivityClientImpl(WorkflowServiceStubs stubs, ActivityClientOptions options) { + stubs = new NamespaceInjectWorkflowServiceStubs(stubs, options.getNamespace()); this.stubs = stubs; this.options = options; this.metricsScope = diff --git a/temporal-sdk/src/test/java/io/temporal/authorization/AuthorizationTokenTest.java b/temporal-sdk/src/test/java/io/temporal/authorization/AuthorizationTokenTest.java index b69327eae6..85adeea53c 100644 --- a/temporal-sdk/src/test/java/io/temporal/authorization/AuthorizationTokenTest.java +++ b/temporal-sdk/src/test/java/io/temporal/authorization/AuthorizationTokenTest.java @@ -3,6 +3,8 @@ import static org.junit.Assert.*; import io.grpc.*; +import io.temporal.client.ActivityClient; +import io.temporal.client.StartActivityOptions; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowOptions; import io.temporal.serviceclient.WorkflowServiceStubsOptions; @@ -18,6 +20,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -125,6 +128,37 @@ public void allRequestsShouldHaveAnAuthToken() { } } + @Test + public void activityClientRequestsShouldHaveANamespace() { + testEnvironment.start(); + ActivityClient client = testEnvironment.getActivityClient(); + StartActivityOptions options = + StartActivityOptions.newBuilder() + .setId("activity-id") + .setTaskQueue(TASK_QUEUE) + .setStartToCloseTimeout(Duration.ofMinutes(1)) + .build(); + + // The test server does not implement StartActivityExecution; only the outgoing headers matter. + assertThrows(StatusRuntimeException.class, () -> client.start("SomeActivity", options)); + + List startRequests = + loggedRequests.stream() + .filter(request -> request.methodName.equals("StartActivityExecution")) + .collect(Collectors.toList()); + assertFalse("StartActivityExecution should have been called", startRequests.isEmpty()); + for (GrpcRequest grpcRequest : startRequests) { + assertEquals( + "StartActivityExecution should have an auth token", + AUTH_TOKEN, + grpcRequest.authTokenValue); + assertEquals( + "StartActivityExecution should have a namespace", + testEnvironment.getNamespace(), + grpcRequest.namespace); + } + } + public static class EmptyWorkflowImpl implements TestWorkflows.TestWorkflow1 { @Override