Skip to content

Commit dfdfaf3

Browse files
authored
Don't allow a default port in URLs by default (#85)
We allow client implementors to set a default port for gRPC URLs again, but the `parse_grpc_uri` function and `BaseApiClient` class still don't provide a default port unless explicitly set.
2 parents e6ef3e6 + 81405db commit dfdfaf3

File tree

4 files changed

+45
-18
lines changed

4 files changed

+45
-18
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
## Upgrading
88

99
- `GrpcStreamBroadcaster` now takes a `AsyncIterable` instead of a `AsyncIterator` as the `stream_method`. This is to match the type of streaming methods generated by `grpc`, so no conversion to an `AsyncIterator` is needed.
10+
- gRPC URLs don't have a default port anymore, unless a default is set via `ChannelOptions`. If you want to set a default port for URLs, please pass custom `ChannelOptions` as `defaults` to `parse_grpc_uri` or as `channel_defaults` to `BaseApiClient`.
1011

1112
## New Features
1213

src/frequenz/client/base/channel.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class SslOptions:
4545
class ChannelOptions:
4646
"""Options for a gRPC channel."""
4747

48-
port: int = 9090
48+
port: int | None = None
4949
"""The port number to connect to."""
5050

5151
ssl: SslOptions = SslOptions()
@@ -68,7 +68,8 @@ def parse_grpc_uri(
6868
A few things to consider about URI components:
6969
7070
- If any other components are present in the URI, a [`ValueError`][] is raised.
71-
- If the port is omitted, the `default_port` is used.
71+
- If the port is omitted, the `default_port` is used unless it is `None`, in which
72+
case a `ValueError` is raised
7273
- If a query parameter is passed many times, the last value is used.
7374
- Boolean query parameters can be specified with the following values
7475
(case-insensitive): `true`, `1`, `on`, `false`, `0`, `off`.
@@ -110,6 +111,11 @@ def parse_grpc_uri(
110111

111112
options = _parse_query_params(uri, parsed_uri.query)
112113

114+
if parsed_uri.port is None and defaults.port is None:
115+
raise ValueError(
116+
f"The gRPC URI '{uri}' doesn't specify a port and there is no default."
117+
)
118+
113119
target = (
114120
parsed_uri.netloc if parsed_uri.port else f"{parsed_uri.netloc}:{defaults.port}"
115121
)

tests/test_channel.py

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class _ValidUrlTestCase:
2424
title: str
2525
uri: str
2626
expected_host: str
27+
expected_port: int | None
2728
expected_options: ChannelOptions
2829
defaults: ChannelOptions = ChannelOptions()
2930

@@ -33,25 +34,40 @@ class _ValidUrlTestCase:
3334
[
3435
_ValidUrlTestCase(
3536
title="default",
37+
uri="grpc://localhost:9090",
38+
expected_host="localhost",
39+
expected_port=9090,
40+
expected_options=ChannelOptions(
41+
ssl=SslOptions(
42+
enabled=True,
43+
root_certificates=None,
44+
private_key=None,
45+
certificate_chain=None,
46+
),
47+
),
48+
),
49+
_ValidUrlTestCase(
50+
title="default with default port",
3651
uri="grpc://localhost",
3752
expected_host="localhost",
53+
expected_port=9090,
3854
expected_options=ChannelOptions(
39-
port=9090,
4055
ssl=SslOptions(
4156
enabled=True,
4257
root_certificates=None,
4358
private_key=None,
4459
certificate_chain=None,
4560
),
4661
),
62+
defaults=ChannelOptions(port=9090),
4763
),
4864
_ValidUrlTestCase(
4965
title="default no SSL defaults",
50-
uri="grpc://localhost",
51-
defaults=ChannelOptions(port=2355, ssl=SslOptions(enabled=False)),
66+
uri="grpc://localhost:2355",
67+
defaults=ChannelOptions(ssl=SslOptions(enabled=False)),
5268
expected_host="localhost",
69+
expected_port=2355,
5370
expected_options=ChannelOptions(
54-
port=2355,
5571
ssl=SslOptions(
5672
enabled=False,
5773
root_certificates=None,
@@ -62,9 +78,8 @@ class _ValidUrlTestCase:
6278
),
6379
_ValidUrlTestCase(
6480
title="default with SSL defaults",
65-
uri="grpc://localhost",
81+
uri="grpc://localhost:2355",
6682
defaults=ChannelOptions(
67-
port=2355,
6883
ssl=SslOptions(
6984
enabled=True,
7085
root_certificates=None,
@@ -73,8 +88,8 @@ class _ValidUrlTestCase:
7388
),
7489
),
7590
expected_host="localhost",
91+
expected_port=2355,
7692
expected_options=ChannelOptions(
77-
port=2355,
7893
ssl=SslOptions(
7994
enabled=True,
8095
root_certificates=None,
@@ -88,8 +103,8 @@ class _ValidUrlTestCase:
88103
uri="grpc://localhost:1234?ssl=1&ssl_root_certificates_path=/root_cert"
89104
"&ssl_private_key_path=/key&ssl_certificate_chain_path=/chain",
90105
expected_host="localhost",
106+
expected_port=1234,
91107
expected_options=ChannelOptions(
92-
port=1234,
93108
ssl=SslOptions(
94109
enabled=True,
95110
root_certificates=pathlib.Path("/root_cert"),
@@ -103,7 +118,6 @@ class _ValidUrlTestCase:
103118
uri="grpc://localhost:1234?ssl=1&ssl_root_certificates_path=/root_cert"
104119
"&ssl_private_key_path=/key&ssl_certificate_chain_path=/chain",
105120
defaults=ChannelOptions(
106-
port=4444,
107121
ssl=SslOptions(
108122
enabled=True,
109123
root_certificates=pathlib.Path("/root_cert_def"),
@@ -112,8 +126,8 @@ class _ValidUrlTestCase:
112126
),
113127
),
114128
expected_host="localhost",
129+
expected_port=1234,
115130
expected_options=ChannelOptions(
116-
port=1234,
117131
ssl=SslOptions(
118132
enabled=True,
119133
root_certificates=pathlib.Path("/root_cert"),
@@ -138,11 +152,7 @@ def test_parse_uri_ok( # pylint: disable=too-many-locals
138152
expected_credentials = mock.MagicMock(
139153
name="mock_credentials", spec=ssl_channel_credentials
140154
)
141-
expected_port = (
142-
expected_options.port
143-
if f":{expected_options.port}" in uri or defaults.port is None
144-
else defaults.port
145-
)
155+
expected_port = case.expected_port
146156
expected_ssl = (
147157
expected_options.ssl.enabled
148158
if "ssl=" in uri or defaults.ssl.enabled is None
@@ -270,3 +280,13 @@ def test_parse_uri_error(
270280
"""Test parsing of invalid gRPC URIs for grpclib."""
271281
with pytest.raises(ValueError, match=error_msg):
272282
parse_grpc_uri(uri)
283+
284+
285+
def test_invalid_url_no_default_port() -> None:
286+
"""Test parsing of invalid gRPC URIs for grpclib."""
287+
uri = "grpc://localhost"
288+
with pytest.raises(
289+
ValueError,
290+
match=r"The gRPC URI 'grpc://localhost' doesn't specify a port and there is no default.",
291+
):
292+
parse_grpc_uri(uri)

tests/test_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def test_base_api_client_init_with_channel_defaults(
105105
mocker: pytest_mock.MockFixture,
106106
) -> None:
107107
"""Test initializing the BaseApiClient with channel defaults."""
108-
channel_defaults = ChannelOptions(port=1234, ssl=SslOptions(enabled=False))
108+
channel_defaults = ChannelOptions(ssl=SslOptions(enabled=False))
109109
client, mocks = create_client_with_mocks(mocker, channel_defaults=channel_defaults)
110110
assert client.server_url == _DEFAULT_SERVER_URL
111111
mocks.parse_grpc_uri.assert_called_once_with(client.server_url, channel_defaults)

0 commit comments

Comments
 (0)