Skip to content

Raise PortAlreadyInUse when Docker reports port is already allocated - #9347

Open
hippi345 wants to merge 2 commits into
aws:developfrom
hippi345:fix/port-already-allocated-9295
Open

hippi345 wants to merge 2 commits into
aws:developfrom
hippi345:fix/port-already-allocated-9295

Conversation

@hippi345

Copy link
Copy Markdown

Summary

This PR fixes issue #9295 by detecting the "port is already allocated" error during container creation and raising the appropriate PortAlreadyInUse exception instead of the generic DockerContainerCreationFailedException.

Problem

When container engines like Finch bind host ports during containers.create() (rather than during start() like Docker), a port conflict results in a misleading error message:

Container creation failed: failed to parse port mapping "0.0.0.0:7311:8080/tcp": bind for 0.0.0.0:7311 failed: port is already allocated, check template for potential issue

The error incorrectly directs users to check their template, when the actual issue is that the host port is already in use.

Solution

Modified Container.create() in samcli/local/docker/container.py to:

  1. Check if the Docker APIError message contains "port is already allocated"
  2. If so, raise PortAlreadyInUse (the same exception already used in the start() method for Docker)
  3. Otherwise, maintain the existing behavior with DockerContainerCreationFailedException

This provides users with accurate error information regardless of which container engine they use or when the engine performs port binding.

Testing

  • Added unit test test_create_raises_port_already_in_use_when_port_allocated that mocks a Docker APIError with "port is already allocated" message
  • Verified the test fails without the fix (raises DockerContainerCreationFailedException)
  • Verified the test passes with the fix (raises PortAlreadyInUse)
  • All 85 existing unit tests in test_container.py continue to pass

Fixes #9295


Target branch: develop

Detect Docker APIError containing 'port is already allocated' during
container creation and raise PortAlreadyInUse instead of the generic
DockerContainerCreationFailedException. This provides users with
accurate error information when a host port is already in use,
particularly on container engines like Finch that bind ports during
create() rather than start().

Add unit test to verify PortAlreadyInUse is raised when the Docker API
returns a port allocation error during container creation.

Fixes aws#9295
@github-actions github-actions Bot added area/local/start-api sam local start-api command area/local/invoke sam local invoke command area/local/start-invoke pr/external stage/needs-triage Automatically applied to new issues and PRs, indicating they haven't been looked at. labels Oct 15, 2026
@hippi345
hippi345 marked this pull request as ready for review October 15, 2026 15:34
@hippi345
hippi345 requested a review from a team as a code owner October 15, 2026 15:34
Comment on lines +292 to +293
if "port is already allocated" in str(ex):
raise PortAlreadyInUse(str(ex)) from ex

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

According to the issue #9347 (in expected result section), changing the error class to PortAlreadyInUse will mess up downstream error catching. The suggestion was simply to change the message.

@reedham-aws reedham-aws added area/finch and removed stage/needs-triage Automatically applied to new issues and PRs, indicating they haven't been looked at. labels Oct 16, 2026
@aws-sam-tooling-bot

Copy link
Copy Markdown

🧪 Hands-on test plan (not run: external contributor)

Commit abd061e. This plan was not run, so nothing in it has been tested.

Summary: Run the PR's new unit test and the existing container and local-service unit suites. Then feed realistic Docker APIErrors into the real Container.create() to compare the exception type and message on base vs PR, and check that unrelated create errors are unchanged. Finally, send an invoke through the real LocalLambdaHttpService Flask app whose runner hits Container.create(), to see whether the HTTP response for a port conflict changes from 501 ContainerCreationFailed to a generic 500 ServiceException.

Why: The PR changes which exception Container.create() raises for a Docker APIError containing 'port is already allocated'. Callers (sam local invoke CLI, LocalLambdaHttpService, LocalApigwService, LambdaRuntime.create) handle ContainerCreationFailedException and PortAlreadyInUse differently, so the user-visible message and HTTP status can change. Real Docker binds ports at start(), not create(), and Finch is not available, so the run drives the real Container and LocalLambdaHttpService code with a mocked Docker client that raises realistic APIErrors (with an HTTP 500 response). The diff is 2 lines of library code plus a unit test, with no install-time code or new dependencies, so a local run is safe.

Needs Docker: no

Setup:

python -m venv .venv
. .venv/bin/activate && pip install --upgrade pip && SAM_CLI_DEV=1 pip install -e '.[dev]'
mkdir -p /tmp/handson

Scenarios (5)

S1: Run the PR's new unit test and the full existing test_container.py suite.

. .venv/bin/activate && python -m pytest tests/unit/local/docker/test_container.py -k test_create_raises_port_already_in_use_when_port_allocated -p no:cacheprovider -q
. .venv/bin/activate && python -m pytest tests/unit/local/docker/test_container.py -p no:cacheprovider -q

Expected: On the PR checkout both commands exit 0; the new test passes and no existing test in test_container.py fails. (On base the -k selection finds no test, so only the second command is meaningful there.)

S2: Main path: a realistic Docker APIError (HTTP 500 response, explanation holding the Finch-style 'port is already allocated' text) raised by containers.create() goes through the real Container.create(). Record the exception type, str() and __cause__, and whether the message is clean or includes the r… (compared with the base branch)

cat > /tmp/handson/create_err.py <<'EOF'
import sys, requests
from unittest.mock import Mock
from docker.errors import APIError
from samcli.local.docker.container import Container, ContainerContext

def api_error(expl):
    r = requests.models.Response()
    r.status_code = 500
    r.reason = 'Internal Server Error'
    r.url = 'http+docker://localhost/v1.47/containers/create'
    http = requests.exceptions.HTTPError('500 Server Error: Internal Server Error for url: ' + r.url, response=r)
    return APIError(http, response=r, explanation=expl)

EXPL = {
 'allocated': 'failed to parse port mapping "0.0.0.0:7311:8080/tcp": bind for 0.0.0.0:7311 failed: port is already allocated',
 'notavail': 'Ports are not available: exposing port TCP 0.0.0.0:7311 -> 0.0.0.0:0: listen tcp 0.0.0.0:7311: bind: address already in use',
 'other': 'invalid mount config for type "bind": bind source path does not exist: /nope',
}
expl = EXPL[sys.argv[1]]
client = Mock()
client.containers.create.side_effect = api_error(expl)
c = Container('public.ecr.aws/lambda/python:3.12', 'cmd', '/var/task', None, docker_client=client)
try:
    c.create(ContainerContext.INVOKE)
    print('NO_EXCEPTION')
except Exception as ex:
    print('TYPE=' + type(ex).__name__)
    print('MSG=' + str(ex))
    print('CAUSE=' + type(ex.__cause__).__name__)
    print('IS_CREATED=' + str(c.is_created()))
EOF
. .venv/bin/activate && python /tmp/handson/create_err.py allocated

Expected: PR: exit 0, prints TYPE=PortAlreadyInUse, CAUSE=APIError, IS_CREATED=False, and MSG contains 'port is already allocated'. Also record whether MSG includes the raw '500 Server Error for http+docker://localhost/v1.47/containers/create: Internal Server Error (' prefix; the PR uses str(ex), while start…

S3: Edge cases: an unrelated create error (bad bind mount) must keep raising ContainerCreationFailedException with the 'check template' hint. The Docker Desktop wording 'Ports are not available' at create time shows whether that wording is handled too or still gets the misleading 'check template' messa… (compared with the base branch)

. .venv/bin/activate && python /tmp/handson/create_err.py other
. .venv/bin/activate && python /tmp/handson/create_err.py notavail

Expected: 'other': on both base and PR, TYPE=ContainerCreationFailedException and MSG='Container creation failed: invalid mount config ... /nope, check template for potential issue'. 'notavail': on both base and PR, expect TYPE=ContainerCreationFailedException with the 'check template' hint, since the PR onl…

S4: sam local start-lambda HTTP path: LocalLambdaHttpService catches ContainerCreationFailedException and returns 501 ContainerCreationFailed but does not catch PortAlreadyInUse. Send an invoke whose runner calls the real Container.create() with the port-allocated error and compare the HTTP status and … (compared with the base branch)

cat > /tmp/handson/http_invoke.py <<'EOF'
import sys, logging
sys.path.insert(0, '/tmp/handson')
from unittest.mock import MagicMock, Mock
import create_err_lib as lib
from samcli.local.docker.container import Container, ContainerContext
from samcli.local.lambda_service.local_lambda_http_service import LocalLambdaHttpService

def fake_invoke(*a, **k):
    client = Mock()
    client.containers.create.side_effect = lib.api_error(lib.EXPL[sys.argv[1]])
    Container('public.ecr.aws/lambda/python:3.12', 'cmd', '/var/task', None, docker_client=client).create(ContainerContext.INVOKE)

runner = MagicMock()
runner.is_debugging.return_value = False
fn = MagicMock()
fn.durable_config = None
runner.get_function.return_value = fn
runner.invoke.side_effect = fake_invoke
svc = LocalLambdaHttpService(runner, port=3001, host='127.0.0.1')
svc.create()
resp = svc._app.test_client().post('/2015-03-31/functions/HelloWorld/invocations', data='{}', headers={'Content-Type': 'application/json'})
print('STATUS=' + str(resp.status_code))
print('ERRTYPE=' + str(resp.headers.get('x-amzn-errortype')))
print('BODY=' + resp.get_data(as_text=True))
EOF
sed -n '1,/^expl = /p' /tmp/handson/create_err.py | sed '$d' > /tmp/handson/create_err_lib.py
. .venv/bin/activate && python /tmp/handson/http_invoke.py allocated
. .venv/bin/activate && python /tmp/handson/http_invoke.py other

Expected: Base, 'allocated': STATUS=501, ERRTYPE=ContainerCreationFailed, BODY holds 'Container creation failed: ... port is already allocated, check template ...'. PR, 'allocated': record the actual status. If it is STATUS=500 with ERRTYPE=Service / a generic ServiceException body and no port message, repor…

S5: Check that the local-invoke, start-api and start-lambda unit suites that consume these exceptions still pass with the change. (compared with the base branch)

. .venv/bin/activate && python -m pytest tests/unit/local/lambdafn tests/unit/local/lambda_service tests/unit/local/apigw tests/unit/commands/local -p no:cacheprovider -q -x

Expected: Exit code 0 on the PR checkout with no failures; the pass count matches the base run.

🤖 Automated hands-on test — not a review approval · model claude-opus-5.5 · commit abd061e

History (1)
Commit Result When (UTC) Build
abd061e Plan only (not run) 2026-10-26 20:12

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: a taken host port is reported as a template problem on Finch, because Finch fails at container create rather than start

2 participants