diff --git a/tensorrt_llm/commands/serve.py b/tensorrt_llm/commands/serve.py index 7becb894ce86..b364e59a8b7c 100644 --- a/tensorrt_llm/commands/serve.py +++ b/tensorrt_llm/commands/serve.py @@ -623,16 +623,20 @@ def launch_server( "in-memory store cannot be shared across frontends.") os.environ["TRTLLM_RESPONSES_API_DISABLE_STORE"] = "1" + # port == 0 lets the kernel pick the port; the caller then needs a way + # to learn it, either by service discovery or by report_addr. Validate + # before getaddrinfo, which rejects negative ports with its own error. + if not (port > 0 or (port == 0 and + (disagg_cluster_config is not None or report_addr))): + raise ValueError( + "Port must be a positive integer, or 0 to let the kernel pick " + "one when disagg cluster config or --report_addr is provided") + addr_info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM) address_family = socket.AF_INET6 if all( [info[0] == socket.AF_INET6 for info in addr_info]) else socket.AF_INET with socket.socket(address_family, socket.SOCK_STREAM) as s: - # port == 0 lets the kernel pick the port; the caller then needs a way - # to learn it, either by service discovery or by report_addr. - assert port > 0 or disagg_cluster_config is not None or report_addr, ( - "Port must be specified unless disagg cluster config or " - "--report_addr is provided") # Without SO_REUSEADDR a restart is refused for the whole TIME_WAIT # window (~60s) by the tombstones of connections this server accepted. # The flag has to be set on the socket that owns the port first, since diff --git a/tests/integration/test_lists/test-db/l0_cpu.yml b/tests/integration/test_lists/test-db/l0_cpu.yml index 691b44f8c565..f2486a3c4928 100644 --- a/tests/integration/test_lists/test-db/l0_cpu.yml +++ b/tests/integration/test_lists/test-db/l0_cpu.yml @@ -82,6 +82,7 @@ l0_cpu: - unittest/llmapi/apps/test_responses_streaming_events.py - unittest/llmapi/apps/test_responses_streaming_tool_calls.py - unittest/llmapi/apps/test_responses_usage.py + - unittest/llmapi/apps/test_serve_port_validation.py - unittest/llmapi/apps/test_tool_parsers.py - unittest/llmapi/apps/test_web_search_shared.py - unittest/llmapi/test_bench_async.py diff --git a/tests/unittest/llmapi/apps/test_serve_port_validation.py b/tests/unittest/llmapi/apps/test_serve_port_validation.py new file mode 100644 index 000000000000..2e2b4dcd4dd7 --- /dev/null +++ b/tests/unittest/llmapi/apps/test_serve_port_validation.py @@ -0,0 +1,48 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import socket +from unittest import mock + +import pytest + +import tensorrt_llm.commands.serve as serve_mod +from tensorrt_llm.commands.serve import launch_server + + +@pytest.mark.parametrize("port", [0, -1]) +def test_launch_server_rejects_nonpositive_port_without_disagg(port: int) -> None: + # --port is user input; a non-positive port without a disagg cluster + # config must raise a clear ValueError (survives `python -O`) rather than + # an AssertionError, and must never bind a socket. socket is mocked so the + # negative-port case does not fail earlier in getaddrinfo. + with mock.patch.object(serve_mod, "socket") as mock_socket: + mock_socket.AF_UNSPEC = socket.AF_UNSPEC + mock_socket.AF_INET = socket.AF_INET + mock_socket.AF_INET6 = socket.AF_INET6 + mock_socket.SOCK_STREAM = socket.SOCK_STREAM + mock_socket.getaddrinfo.return_value = [ + (socket.AF_INET, None, None, None, ("127.0.0.1", port)) + ] + sock = mock_socket.socket.return_value.__enter__.return_value + + with pytest.raises(ValueError, match="Port must be a positive integer"): + launch_server( + host="localhost", + port=port, + llm_args={"backend": "pytorch", "model": "dummy-model"}, + disagg_cluster_config=None, + ) + + sock.bind.assert_not_called()