Fix AttributeError when port is already in use#8675
Conversation
This commit fixes two related issues when Docker port binding fails: 1. AttributeError on ex.explanation.decode() - In docker-py >= 7.0, the explanation property is already a str, not bytes. Calling .decode() on a string raises AttributeError, completely masking the actual "port already in use" error. 2. Case sensitivity in error detection - The code checked for "Ports are not available" (capital P) but Docker may return "ports are not available" (lowercase p), causing the condition to fail and miss the specific error handling. Changes: - Added safe_decode_docker_message() function in utils.py that handles both bytes (docker-py < 7.0) and str (docker-py >= 7.0) - Updated container.py to use case-insensitive check (.lower()) - Added comprehensive unit tests covering both scenarios This ensures users see the helpful port error message instead of a cryptic AttributeError, regardless of their docker-py version. Fixes aws#7244 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
| with self.assertRaises(APIError): | ||
| self.container.start() | ||
|
|
||
| def test_docker_api_error_port_in_use_with_str_explanation(self): |
There was a problem hiding this comment.
Can we shorten the tests here with parameterization?
roger-zhangg
left a comment
There was a problem hiding this comment.
LGTM — the fix is correct and minimal. The safe_decode_docker_message utility properly handles both bytes (docker-py < 7.0) and str (docker-py >= 7.0) return types, and the case-insensitive check is a good defensive improvement.
Two minor observations (non-blocking):
- If
ex.explanationis everNone(theAPIError.__init__signature allowsexplanation=None), the utility will returnNone. This is unlikely in practice since the"ports are not available"check onstr(ex)implies the error is well-formed, but you could add aif message is None: return ""guard for extra safety. - Agree with the existing feedback from @vicheey — the three test cases for
test_container.pycould be consolidated with parameterization (e.g.,@parameterized.expand) to reduce repetition.
Thanks for the contribution!
Handle edge case where ex.explanation is None (returns empty string) and ensure non-bytes/non-str types are coerced via str().
| with self.assertRaises(APIError): | ||
| self.container.start() | ||
|
|
||
| def test_docker_api_error_port_in_use_with_str_explanation(self): |
There was a problem hiding this comment.
[GENERAL] Re-raising unresolved feedback from @vicheey: the three new tests (test_docker_api_error_port_in_use_with_str_explanation, test_docker_api_error_port_in_use_with_bytes_explanation, test_docker_api_error_port_in_use_case_insensitive) share nearly identical setup and assertions, differing only in the explanation value passed to APIError and the expected substring. The file already imports parameterized at line 11, so consolidating these into a single parameterized test would reduce duplication and make it easier to add future cases (e.g. mixed-case, None, extra whitespace) without another copy-paste block.
Example:
@parameterized.expand([
(
"str_explanation_docker_py_ge_7",
"ports are not available: listen tcp 127.0.0.1:5005: bind: address already in use",
"ports are not available",
),
(
"bytes_explanation_docker_py_lt_7",
b"ports are not available: listen tcp 127.0.0.1:5005: bind: address already in use",
"ports are not available",
),
(
"case_insensitive_uppercase_ports",
"Ports are not available: listen tcp 127.0.0.1:5005: bind: address already in use",
"Ports are not available",
),
])
def test_docker_api_error_port_in_use(self, name, explanation, expectedsubstring):
self.container.is_created.return_value = True
container_mock = Mock()
self.mock_docker_client.containers.get.return_value = container_mock
message = explanation.decode() if isinstance(explanation, bytes) else explanation
container_mock.start.side_effect = APIError(message, response=None, explanation=explanation)
with self.assertRaises(PortAlreadyInUse) as context:
self.container.start()
self.assertIn(expected_substring, str(context.exception))This has not been explicitly dismissed by the author, so flagging again per review policy.
Consolidate three nearly-identical test methods into a single @parameterized test covering str, bytes, and case-insensitive variants.
roger-zhangg
left a comment
There was a problem hiding this comment.
All review feedback addressed:
- None guard added to
safe_decode_docker_message - Tests parameterized per @vicheey's suggestion
Ready to merge.
Issue
Fixes #7244
Problem
When SAM CLI attempts to bind to a port that's already in use, users see a cryptic
AttributeError: 'str' object has no attribute 'decode'instead of a helpful error message about which port is occupied.This occurs because:
APIError.explanationis astr(notbytes).decode()on it, causing AttributeErrorSolution
safe_decode_docker_message()function that handles bothbytes(docker-py < 7.0) andstr(docker-py >= 7.0).lower())Testing
samdev local invokeon occupied portResult
Users now see the actual port error message:
Instead of:
Checklist
make test)make lint)make black-check)