From 3d95e1e13313a8d78b28cb5a9cec5ba21476a628 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Fri, 23 Oct 2026 17:05:09 -0700 Subject: [PATCH 1/2] Fix data converter assignment for update headers --- README.md | 9 ++ src/Internal/Client/WorkflowStarter.php | 7 +- src/Internal/Client/WorkflowStub.php | 7 +- tests/Unit/Client/UpdateHeaderTestCase.php | 118 +++++++++++++++++++++ 4 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 tests/Unit/Client/UpdateHeaderTestCase.php diff --git a/README.md b/README.md index bbf988c69..46167e812 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,15 @@ $response = $operatorClient->DeleteNamespace( > that change the behavior of the entire PHP worker. > It's recommended to disable deprecated behavior. +#### Update headers + +Client interceptors can supply update headers with `Temporal\Interceptor\Header::fromValues()`. +For updates and the update portion of `updateWithStart()`, PHP header values use the client's +configured data converter unless the header already has an explicitly supplied converter. +Prebuilt `Payload` entries supplied through `Header::fromPayloadCollection()` or `withValue()` +are forwarded unchanged, even when the same header also contains PHP values. Prepare those +payloads with any application-specific encoding they require; they are not decoded or re-encoded. + ## Testing The PHP SDK includes a toolkit for testing Workflows. diff --git a/src/Internal/Client/WorkflowStarter.php b/src/Internal/Client/WorkflowStarter.php index 7e08e4a3b..0b29ff53d 100644 --- a/src/Internal/Client/WorkflowStarter.php +++ b/src/Internal/Client/WorkflowStarter.php @@ -175,7 +175,12 @@ function (UpdateWithStartInput $input): UpdateWithStartOutput { $i = new \Temporal\Api\Update\V1\Input(); $i->setName($input->updateInput->updateName); $input->updateInput->arguments->isEmpty() or $i->setArgs($input->updateInput->arguments->toPayloads()); - $input->updateInput->header->isEmpty() or $i->setHeader($input->updateInput->header->toHeader()); + $header = $input->updateInput->header; + \assert($header instanceof Header); + if ($header->getDataConverter() === null) { + $header->setDataConverter($this->converter); + } + $header->isEmpty() or $i->setHeader($header->toHeader()); $r->setInput($i); $ops = [ diff --git a/src/Internal/Client/WorkflowStub.php b/src/Internal/Client/WorkflowStub.php index fb4d7e259..aaf70edf8 100644 --- a/src/Internal/Client/WorkflowStub.php +++ b/src/Internal/Client/WorkflowStub.php @@ -314,7 +314,12 @@ static function ( $input->arguments->setDataConverter($converter); $input->arguments->setSerializationContext($context); $input->arguments->isEmpty() or $i->setArgs($input->arguments->toPayloads()); - $input->header->isEmpty() or $i->setHeader($input->header->toHeader()); + $header = $input->header; + \assert($header instanceof Header); + if ($header->getDataConverter() === null) { + $header->setDataConverter($converter); + } + $header->isEmpty() or $i->setHeader($header->toHeader()); $r->setInput($i); try { diff --git a/tests/Unit/Client/UpdateHeaderTestCase.php b/tests/Unit/Client/UpdateHeaderTestCase.php new file mode 100644 index 000000000..b4d581561 --- /dev/null +++ b/tests/Unit/Client/UpdateHeaderTestCase.php @@ -0,0 +1,118 @@ + + */ + public static function headers(): iterable + { + yield 'update, client converter' => [false, false]; + yield 'update with start, client converter' => [true, false]; + yield 'update, explicit converter' => [false, true]; + yield 'update with start, explicit converter' => [true, true]; + } + + #[DataProvider('headers')] + public function testHeaderConversion(bool $withStart, bool $explicitConverter): void + { + $converter = $this->createMock(DataConverterInterface::class); + $headerConverter = $explicitConverter ? $this->createMock(DataConverterInterface::class) : $converter; + $encoded = new Payload(['metadata' => ['encoding' => 'custom/header'], 'data' => 'converted']); + $raw = new Payload(['metadata' => ['encoding' => 'custom/raw'], 'data' => "\x00raw\xff"]); + $rawBytes = $raw->serializeToString(); + + $headerConverter->expects(self::once())->method('toPayload')->with('value')->willReturn($encoded); + $headerConverter->expects(self::never())->method('fromPayload'); + if ($explicitConverter) { + $converter->expects(self::never())->method('toPayload'); + $converter->expects(self::never())->method('fromPayload'); + } + $header = $explicitConverter + ? Header::fromPayloadCollection(['raw' => $raw], $headerConverter)->withValue('value', 'value') + : Header::fromValues(['value' => 'value'])->withValue('raw', $raw); + + $interceptor = $this->createMock(WorkflowClientCallsInterceptor::class); + $interceptor->method('update')->willReturnCallback( + static fn(UpdateInput $input, callable $next) => $next($input->with(header: $header)), + ); + $interceptor->method('updateWithStart')->willReturnCallback( + static fn(UpdateWithStartInput $input, callable $next) => $next( + $input->with(updateInput: $input->updateInput->with(header: $header)), + ), + ); + + $service = $this->createMock(ServiceClientInterface::class); + $service->method('getContext')->willReturn(Context::default()); + $service->method('withContext')->willReturnSelf(); + $response = (new UpdateWorkflowExecutionResponse())->setUpdateRef( + (new UpdateRef())->setUpdateId('update-id'), + ); + $checkHeader = static function (UpdateWorkflowExecutionRequest $request) use ($encoded, $rawBytes): void { + $fields = $request->getRequest()->getInput()->getHeader()->getFields(); + self::assertCount(2, $fields); + self::assertSame($encoded->serializeToString(), $fields['value']->serializeToString()); + self::assertSame($rawBytes, $fields['raw']->serializeToString()); + }; + if ($withStart) { + $service->expects(self::once())->method('ExecuteMultiOperation')->willReturnCallback( + static function (ExecuteMultiOperationRequest $request) use ($checkHeader, $response) { + $checkHeader($request->getOperations()[1]->getUpdateWorkflow()); + + return (new ExecuteMultiOperationResponse())->setResponses([ + (new Response())->setStartWorkflow( + (new StartWorkflowExecutionResponse())->setRunId('run-id'), + ), + (new Response())->setUpdateWorkflow($response), + ]); + }, + ); + } else { + $service->expects(self::once())->method('UpdateWorkflowExecution')->willReturnCallback( + static function (UpdateWorkflowExecutionRequest $request) use ($checkHeader, $response) { + $checkHeader($request); + + return $response; + }, + ); + } + $client = new WorkflowClient( + $service, + converter: $converter, + interceptorProvider: new SimplePipelineProvider([$interceptor]), + ); + + if ($withStart) { + $client->updateWithStart($client->newUntypedWorkflowStub('workflow'), 'update'); + } else { + $client->newUntypedRunningWorkflowStub('workflow-id')->startUpdate('update'); + } + + self::assertSame($headerConverter, $header->getDataConverter()); + self::assertSame($rawBytes, $raw->serializeToString()); + } +} From dbad13f19d8da931ee2b0e7c8e34db61da7b4b5e Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 26 Oct 2026 10:09:26 -0700 Subject: [PATCH 2/2] Remove obsolete update header Psalm baseline entries --- psalm-baseline.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 57d72366a..ad7cd2897 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -374,9 +374,6 @@ - - - @@ -411,9 +408,6 @@ - - -