Skip to content

Commit 7c5db23

Browse files
committed
Fix durable client 500 when host sends config fields as null
The Durable Functions host client-config JSON can send fields explicitly as null (observed with maxGrpcMessageSizeInBytes on newer extension bundles) rather than omitting them. dict.get(key, default) only substitutes the default for absent keys, so a present-but-null value returned None -- crashing the 'maxGrpcMessageSizeInBytes > 0' guard in __init__ (TypeError: '>' not supported between NoneType and int) and failing every durable-client binding decode with a 500. Parse every field with 'client.get(key) or default' so an explicit null (like a missing key) collapses to its falsy default. Diagnosed from the e2e func host log; add regression tests for a single null field and for all fields sent as null.
1 parent 72525bc commit 7c5db23

2 files changed

Lines changed: 64 additions & 10 deletions

File tree

‎azure-functions-durable/azure/durable_functions/client.py‎

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -133,17 +133,26 @@ def _parse_client_configuration(self, client_as_string: str) -> None:
133133
"""
134134
client = json.loads(client_as_string)
135135

136-
self.taskHubName = client.get("taskHubName", "")
137-
self.connectionName = client.get("connectionName", "")
138-
self.creationUrls = client.get("creationUrls", {})
139-
self.managementUrls = client.get("managementUrls", {})
140-
self.baseUrl = client.get("baseUrl", "")
141-
self.requiredQueryStringParameters = client.get("requiredQueryStringParameters", "")
142-
self.rpcBaseUrl = client.get("rpcBaseUrl", "")
143-
self.httpBaseUrl = client.get("httpBaseUrl", "")
144-
self.maxGrpcMessageSizeInBytes = client.get("maxGrpcMessageSizeInBytes", 0)
136+
# Depending on the extension-bundle version, the host may send a field
137+
# explicitly as ``null`` rather than omitting it. ``dict.get(key,
138+
# default)`` only substitutes the default for *absent* keys, so a
139+
# present-but-``null`` value slips through as ``None`` (this crashed the
140+
# ``maxGrpcMessageSizeInBytes > 0`` guard in __init__ on newer bundles).
141+
# Use ``... or default`` so an explicit ``null`` -- like a missing key --
142+
# always collapses to the intended default. Every default here is falsy
143+
# and no field carries a meaningful falsy value, so this never discards a
144+
# real value.
145+
self.taskHubName = client.get("taskHubName") or ""
146+
self.connectionName = client.get("connectionName") or ""
147+
self.creationUrls = client.get("creationUrls") or {}
148+
self.managementUrls = client.get("managementUrls") or {}
149+
self.baseUrl = client.get("baseUrl") or ""
150+
self.requiredQueryStringParameters = client.get("requiredQueryStringParameters") or ""
151+
self.rpcBaseUrl = client.get("rpcBaseUrl") or ""
152+
self.httpBaseUrl = client.get("httpBaseUrl") or ""
153+
self.maxGrpcMessageSizeInBytes = client.get("maxGrpcMessageSizeInBytes") or 0
145154
# TODO: convert the string value back to timedelta - annoying regex?
146-
self.grpcHttpClientTimeout = client.get("grpcHttpClientTimeout", timedelta(seconds=30))
155+
self.grpcHttpClientTimeout = client.get("grpcHttpClientTimeout") or timedelta(seconds=30)
147156

148157
def create_check_status_response(self, request: func.HttpRequest, instance_id: str) -> func.HttpResponse:
149158
"""Creates an HTTP response for checking the status of a Durable Function instance.

‎tests/azure-functions-durable/test_client_compat.py‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,51 @@ def _make_client() -> df.DurableFunctionsClient:
3434
return df.DurableFunctionsClient(_CLIENT_CONFIG)
3535

3636

37+
def test_client_handles_null_max_grpc_message_size():
38+
# The Durable Functions host may send ``maxGrpcMessageSizeInBytes``
39+
# explicitly as ``null`` (not just omit it). ``dict.get(key, 0)`` returns
40+
# ``None`` in that case, which previously blew up the ``> 0`` guard in
41+
# ``__init__`` (``TypeError: '>' not supported between 'NoneType' and
42+
# 'int'``) and made every durable-client binding decode fail with a 500.
43+
config = json.dumps({
44+
"taskHubName": "TestHub",
45+
"rpcBaseUrl": "http://localhost:8080/",
46+
"maxGrpcMessageSizeInBytes": None,
47+
})
48+
client = df.DurableFunctionsClient(config)
49+
assert client.maxGrpcMessageSizeInBytes == 0
50+
51+
52+
def test_client_handles_all_config_fields_sent_as_null():
53+
# Newer host extension bundles serialize the full client configuration and
54+
# can send any field explicitly as ``null``. Every field must collapse to
55+
# its (falsy) default rather than ``None`` so construction never crashes and
56+
# downstream code sees well-typed values.
57+
config = json.dumps({
58+
"taskHubName": None,
59+
"connectionName": None,
60+
"creationUrls": None,
61+
"managementUrls": None,
62+
"baseUrl": None,
63+
"requiredQueryStringParameters": None,
64+
"rpcBaseUrl": None,
65+
"httpBaseUrl": None,
66+
"maxGrpcMessageSizeInBytes": None,
67+
"grpcHttpClientTimeout": None,
68+
})
69+
client = df.DurableFunctionsClient(config)
70+
assert client.taskHubName == ""
71+
assert client.connectionName == ""
72+
assert client.creationUrls == {}
73+
assert client.managementUrls == {}
74+
assert client.baseUrl == ""
75+
assert client.requiredQueryStringParameters == ""
76+
assert client.rpcBaseUrl == ""
77+
assert client.httpBaseUrl == ""
78+
assert client.maxGrpcMessageSizeInBytes == 0
79+
assert client.grpcHttpClientTimeout == timedelta(seconds=30)
80+
81+
3782
# ---------------------------------------------------------------------------
3883
# RetryOptions shim
3984
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)