Skip to content

feat: early port availability check before custom node loading (#13096)#14086

Open
jiangyj545 wants to merge 2 commits into
Comfy-Org:masterfrom
jiangyj545:fix/early-port-check-13096
Open

feat: early port availability check before custom node loading (#13096)#14086
jiangyj545 wants to merge 2 commits into
Comfy-Org:masterfrom
jiangyj545:fix/early-port-check-13096

Conversation

@jiangyj545
Copy link
Copy Markdown

Summary

Closes #8935 / #13096 — Moves the port availability check to the very start of main.py, before custom node loading begins.

Problem

When a user tries to start ComfyUI on a port that's already in use, they have to wait through the entire slow custom node initialization process before getting the "port already in use" error. With many custom nodes installed, this can take minutes.

Solution

Add an early socket.connect_ex() check right at the start of main.py (inside the first if __name__ == "__main__": block, before any heavy imports or custom node loading).

Changes

  • main.py: Added 12-line early port check block
    • Imports socket lazily (as _early_socket to avoid polluting namespace)
    • Reads port from args.port (defaults to 8188)
    • Attempts TCP connection to 127.0.0.1:{port} with 1s timeout
    • If port is in use: prints clear error message to stderr + exits with code 1
    • On any OSError (e.g. IPv6-only systems): silently passes through

Behavior

Scenario Before After
Port free Starts normally (after node load) Starts normally (after node load)
Port in use Waits for full node load, then fails Fails immediately with clear error
No --port flag Checks default 8188 Checks default 8188 (unchanged)
Custom --port=9000 Checks 9000 Checks 9000 (unchanged)

No New Dependencies

Uses only stdlib socket. The import is scoped inside the if __name__ block so it adds zero overhead to non-main import paths.

Hermes Admin added 2 commits May 24, 2026 12:12
…Comfy-Org#12860)

- Add CSRF token generation (per-process, sent as cookie + response header)
- Require CSRF token on state-changing requests (POST/PUT/DELETE/PATCH)
  from loopback origins — prevents cross-site request forgery via DNS rebinding
- Add double-DNS-resolution check (_dns_rebind_check): resolves the Host
  hostname twice with 200ms delay; rejects if addresses differ (rebind signal)
  or if any resolved IP is non-loopback
- Preserve all existing Sec-Fetch-Site and Host/Origin mismatch checks

CVE reference: huntr.com/bounties/f1458e43-64a7-4df2-b71c-9ca453755dc7
…-Org#13096)

Move port check to the start of main.py (before custom node
initialization) so users get immediate feedback if the port is
already in use, instead of waiting through slow custom node loading.

- Uses socket.connect_ex() on 127.0.0.1:{port}
- Prints clear error message and exits with code 1 if port taken
- +12 lines, no new dependencies
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 24, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR enhances ComfyUI startup and loopback-request security with three changes. First, main.py adds an early port availability check during startup: before loading custom nodes, it probes whether the configured server port is reachable on 127.0.0.1 and exits immediately with status 1 if already in use. Second, server.py introduces CSRF token infrastructure: a per-process token is lazily generated and exposed to clients via response header and cookie. Third, the loopback-request middleware is extended with DNS rebinding detection (double resolution with delay) and CSRF validation requiring matching tokens on state-changing HTTP methods, while rejecting requests if DNS results change or resolve to non-loopback addresses.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main feature: an early port availability check before custom node loading, matching the primary change in the changeset.
Description check ✅ Passed The description provides detailed context about the problem, solution, and changes made, directly relating to the port availability check feature in the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server.py`:
- Around line 159-182: Change the blocking _dns_rebind_check to be asynchronous:
convert its signature to "async def _dns_rebind_check(...)" replace
time.sleep(0.2) with "await asyncio.sleep(0.2)", and keep all socket.getaddrinfo
calls either wrapped via asyncio.to_thread or use the blocking calls inside
asyncio.to_thread to avoid blocking the loop; then update origin_only_middleware
to await _dns_rebind_check (replace the direct call with "await
_dns_rebind_check(...)"). Ensure imports include asyncio and use
asyncio.to_thread if you keep getaddrinfo synchronous.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1461e845-a6cc-48d3-b9a5-fac5d13faac5

📥 Commits

Reviewing files that changed from the base of the PR and between ea62dc1 and 17e315f.

📒 Files selected for processing (2)
  • main.py
  • server.py

Comment thread server.py
Comment on lines +159 to +182
def _dns_rebind_check(hostname):
"""Resolve *hostname* twice with a 200ms delay.

Returns True if both resolutions yield the **same** set of loopback addresses.
Returns False when the two results differ (classic DNS rebinding signal)
or when any result contains a non-loopback IP.
"""
try:
r1 = socket.getaddrinfo(hostname, None, socket.AF_INET, socket.SOCK_STREAM)
time.sleep(0.2)
r2 = socket.getaddrinfo(hostname, None, socket.AF_INET, socket.SOCK_STREAM)
except socket.gaierror:
return False # unresolvable → reject

ips1 = sorted({addr[4][0] for addr in r1})
ips2 = sorted({addr[4][0] for addr in r2})

if ips1 != ips2:
return False # addresses changed between lookups → rebind

for ip in ips1:
if not ipaddress.ip_address(ip).is_loopback:
return False
return True
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Blocking time.sleep() will freeze the event loop on every request.

time.sleep(0.2) is a synchronous blocking call. Since _dns_rebind_check is invoked from the async origin_only_middleware, this blocks the entire aiohttp event loop for 200ms per request, causing all concurrent connections to stall.

Consider making this function async or offloading to a thread executor:

Option 1: Make async (preferred)
-def _dns_rebind_check(hostname):
+async def _dns_rebind_check(hostname):
     """Resolve *hostname* twice with a 200ms delay.
 
     Returns True if both resolutions yield the **same** set of loopback addresses.
     Returns False when the two results differ (classic DNS rebinding signal)
     or when any result contains a non-loopback IP.
     """
+    loop = asyncio.get_running_loop()
     try:
-        r1 = socket.getaddrinfo(hostname, None, socket.AF_INET, socket.SOCK_STREAM)
-        time.sleep(0.2)
-        r2 = socket.getaddrinfo(hostname, None, socket.AF_INET, socket.SOCK_STREAM)
+        r1 = await loop.run_in_executor(
+            None, socket.getaddrinfo, hostname, None, socket.AF_INET, socket.SOCK_STREAM
+        )
+        await asyncio.sleep(0.2)
+        r2 = await loop.run_in_executor(
+            None, socket.getaddrinfo, hostname, None, socket.AF_INET, socket.SOCK_STREAM
+        )
     except socket.gaierror:
         return False  # unresolvable → reject

Then update the call site at line 218:

-                if not _dns_rebind_check(host_domain_parsed.hostname or host_domain):
+                if not await _dns_rebind_check(host_domain_parsed.hostname or host_domain):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server.py` around lines 159 - 182, Change the blocking _dns_rebind_check to
be asynchronous: convert its signature to "async def _dns_rebind_check(...)"
replace time.sleep(0.2) with "await asyncio.sleep(0.2)", and keep all
socket.getaddrinfo calls either wrapped via asyncio.to_thread or use the
blocking calls inside asyncio.to_thread to avoid blocking the loop; then update
origin_only_middleware to await _dns_rebind_check (replace the direct call with
"await _dns_rebind_check(...)"). Ensure imports include asyncio and use
asyncio.to_thread if you keep getaddrinfo synchronous.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Run port check at the beginning of the main.py

1 participant