Skip to content

Conversation

jazev-stripe
Copy link
Contributor

@jazev-stripe jazev-stripe commented Aug 30, 2025

What was changed

This PR makes it possible to set gRPC binary metadata values on outbound requests, both on a client-wide basis and on a per-RPC basis.

This extends the type of most gRPC metadata bags throughout the SDK from Mapping[str, str]Mapping[str, Union[str, bytes]]. This is the main user-facing change. Internally, the core SDK bridge interprets this (using pyo3's support for turning Python unions into Rust enums) and sets the appropriate core-SDK-client field or tonic metadata field as appropriate.

This PR depends on the changes in temporalio/sdk-core#997 (and updates the core SDK submodule to point to a sufficiently newer commit).

Why?

#1063

Checklist

  1. Closes [Feature Request] Ensure gRPC binary metadata headers are supported #1063

  2. How was this tested:

    • Updated tests/api/test_grpc_stub.py
    • Ran poe test (although this doesn't seem to run the tests/api/... tests 😕)
    • Ran poe test ./tests/api, which does run the updated test
    • Lastly, I ran the example in the README against a production build of the wheel:
    import asyncio
    from temporalio import workflow, activity
    from temporalio.client import Client
    from temporalio.worker import Worker
    
    
    @workflow.defn
    class SayHello:
        @workflow.run
        async def run(self, name: str) -> str:
            return f"Hello, {name}!"
    
    
    async def main():
        client = await Client.connect("localhost:7233")
        async with Worker(client, task_queue="my-task-queue", workflows=[SayHello]):
            result = await client.execute_workflow(
                SayHello.run,
                "Temporal",
                id="my-workflow-id",
                task_queue="my-task-queue",
                rpc_metadata={
                    "x-ascii-header": "abc",
                    "x-binary-header-bin": b"abc",
                },
            )
            print(f"Result: {result}")
    
    
    if __name__ == "__main__":
        asyncio.run(main())

    This worked, as expected:

    $ python3 --version
    # Python 3.11.6
    
    $ python3 ./worker.py
    # Result: Hello, Temporal!
  3. Any docs updates needed?

    • I don't think so. The main change is to the autogenerated Python SDK API docs, which should reflect the type signature change.

@CLAassistant
Copy link

CLAassistant commented Aug 30, 2025

CLA assistant check
All committers have signed the CLA.

@jazev-stripe jazev-stripe force-pushed the jazev-stripe/binary-grpc-headers branch from 6a68726 to 5001e92 Compare September 5, 2025 19:50
@Sushisource
Copy link
Member

Would close temporalio/features#671

@jazev-stripe jazev-stripe changed the title [WIP] Add support for gRPC binary metadata values Add support for gRPC binary metadata values Sep 12, 2025
@jazev-stripe jazev-stripe marked this pull request as ready for review September 12, 2025 16:34
@jazev-stripe jazev-stripe requested a review from a team as a code owner September 12, 2025 16:34
Copy link
Member

@cretz cretz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I think poe lint is failing here in tests/api/test_grpc_stub.py (see CI run).

Comment on lines +132 to +139
self.retry_client
.get_client()
.set_headers(ascii_headers)
.map_err(|err| PyValueError::new_err(err.to_string()))?;
self.retry_client
.get_client()
.set_binary_headers(binary_headers)
.map_err(|err| PyValueError::new_err(err.to_string()))?;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are both fallible and non-atomic 😕. Right now there's a bit of a risk that the client is left in an inconsistent state (in a way that other code could observe) if one of them fails.

I suspect you'll run into this in the other SDKs, too.

I made a test to reproduce/document the issue:

# Setting invalid RPC metadata in a mixed client will partially fail:
client.rpc_metadata = {
"x-my-binary-bin": b"\x00",
"x-my-ascii": "foo",
}
assert client.rpc_metadata == {
"x-my-binary-bin": b"\x00",
"x-my-ascii": "foo",
}
with pytest.raises(
ValueError,
match="Invalid binary header key 'x-invalid-ascii-with-bin-value': invalid gRPC metadata key name",
):
client.rpc_metadata = {
"x-invalid-ascii-with-bin-value": b"not-ascii",
"x-my-ascii": "bar",
}
assert client.rpc_metadata == {
"x-my-binary-bin": b"\x00",
"x-my-ascii": "foo",
}
await client.workflow_service.get_system_info(GetSystemInfoRequest())
workflow_server.assert_last_metadata(
{
"authorization": "Bearer my-api-key",
# This is inconsistent with what `client.rpc_metadata` returns
# (`x-my-ascii` was updated):
"x-my-binary-bin": b"\x00",
"x-my-ascii": "bar",
}
)

Let me know if we should fix this. I think another PR to SDK core that either:

  1. Adds an atomic ConfiguredClient::set_ascii_and_binary_headers(&self, ascii_headers: HashMap<String, String>, binary_headers: HashMap<String, Vec<u8>>) -> Result<(), InvalidHeaderError>
  2. Adds ConfiguredClient::clear_headers(&self) -> () / ConfiguredClient::clear_binary_headers(&self) -> () that can be used to infallibly clear headers, as a way to at least move the client into a consistent state if there any errors.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not that concerned, though arguably we could have changed set_headers to accept both types of headers instead of just one. We can just document this above the rpc metadata setter I think. I appreciate that ascii is done first since I think y'all may be the only one doing binary :-)

Copy link
Contributor Author

@jazev-stripe jazev-stripe Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could have changed set_headers to accept both types of headers instead of just one.

FWIW the reason I didn't do something like that originally is that it would have broken semver on the Core SDK client crate (although maybe I could have done some impl Into<...> magic to make existing callers still work?)

We can just document this above the rpc metadata setter I think.

Ack, I can make that change.

I appreciate that ascii is done first since I think y'all may be the only one doing binary :-)

Heh, fair. Yeah I think if the ASCII is invalid (and setting it fails), this same issue doesn't exist.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the docs in 8b1dd3e:

image


target_url: str
metadata: Mapping[str, str]
metadata: Mapping[str, Union[str, bytes]]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to use str | bytes luckily ran into a CI issue about Python 3.9 incompatibility (which I hadn't realized). These all use Union[str, bytes] as a result.

Comment on lines +81 to +87
#[derive(FromPyObject)]
enum RpcMetadataValue {
#[pyo3(transparent, annotation = "str")]
Str(String),
#[pyo3(transparent, annotation = "bytes")]
Bytes(Vec<u8>),
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the tests in tests/api/test_grpc_stub.py for what the error message looks like, when an invalid type is specified. I think pyo3 does a good job at producing a reasonably-good error

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 And not too worried about error quality for this rare situation

@cretz cretz merged commit 9d70d44 into temporalio:main Sep 12, 2025
27 of 28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Ensure gRPC binary metadata headers are supported

4 participants