Skip to content

Commit e758594

Browse files
authored
Add deploy environment header (Comfy-Env) to partner node API calls (#13425)
1 parent ae457da commit e758594

4 files changed

Lines changed: 147 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ web_custom_versions/
2323
.DS_Store
2424
filtered-openapi.yaml
2525
uv.lock
26+
.comfy_environment

comfy/deploy_environment.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import functools
2+
import logging
3+
import os
4+
5+
logger = logging.getLogger(__name__)
6+
7+
_DEFAULT_DEPLOY_ENV = "local-git"
8+
_ENV_FILENAME = ".comfy_environment"
9+
10+
# Resolve the ComfyUI install directory (the parent of this `comfy/` package).
11+
# We deliberately avoid `folder_paths.base_path` here because that is overridden
12+
# by the `--base-directory` CLI arg to a user-supplied path, whereas the
13+
# `.comfy_environment` marker is written by launchers/installers next to the
14+
# ComfyUI install itself.
15+
_COMFY_INSTALL_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
16+
17+
18+
@functools.cache
19+
def get_deploy_environment() -> str:
20+
env_file = os.path.join(_COMFY_INSTALL_DIR, _ENV_FILENAME)
21+
try:
22+
with open(env_file, encoding="utf-8") as f:
23+
# Cap the read so a malformed or maliciously crafted file (e.g.
24+
# a single huge line with no newline) can't blow up memory.
25+
first_line = f.readline(128).strip()
26+
value = "".join(c for c in first_line if 32 <= ord(c) < 127)
27+
if value:
28+
return value
29+
except FileNotFoundError:
30+
pass
31+
except Exception as e:
32+
logger.error("Failed to read %s: %s", env_file, e)
33+
34+
return _DEFAULT_DEPLOY_ENV

comfy_api_nodes/util/client.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
from comfy_api.latest import IO
2020
from server import PromptServer
2121

22+
from comfy.deploy_environment import get_deploy_environment
23+
2224
from . import request_logger
2325
from ._helpers import (
2426
default_base_url,
@@ -624,6 +626,7 @@ async def _monitor(stop_evt: asyncio.Event, start_ts: float):
624626
payload_headers = {"Accept": "*/*"} if expect_binary else {"Accept": "application/json"}
625627
if not parsed_url.scheme and not parsed_url.netloc: # is URL relative?
626628
payload_headers.update(get_auth_header(cfg.node_cls))
629+
payload_headers["Comfy-Env"] = get_deploy_environment()
627630
if cfg.endpoint.headers:
628631
payload_headers.update(cfg.endpoint.headers)
629632

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""Tests for comfy.deploy_environment."""
2+
3+
import os
4+
5+
import pytest
6+
7+
from comfy import deploy_environment
8+
from comfy.deploy_environment import get_deploy_environment
9+
10+
11+
@pytest.fixture(autouse=True)
12+
def _reset_cache_and_install_dir(tmp_path, monkeypatch):
13+
"""Reset the functools cache and point the ComfyUI install dir at a tmp dir for each test."""
14+
get_deploy_environment.cache_clear()
15+
monkeypatch.setattr(deploy_environment, "_COMFY_INSTALL_DIR", str(tmp_path))
16+
yield
17+
get_deploy_environment.cache_clear()
18+
19+
20+
def _write_env_file(tmp_path, content: str) -> str:
21+
"""Write the env file with exact content (no newline translation).
22+
23+
`newline=""` disables Python's text-mode newline translation so the bytes
24+
on disk match the literal string passed in, regardless of host OS.
25+
Newline-style tests (CRLF, lone CR) rely on this.
26+
"""
27+
path = os.path.join(str(tmp_path), ".comfy_environment")
28+
with open(path, "w", encoding="utf-8", newline="") as f:
29+
f.write(content)
30+
return path
31+
32+
33+
class TestGetDeployEnvironment:
34+
def test_returns_local_git_when_file_missing(self):
35+
assert get_deploy_environment() == "local-git"
36+
37+
def test_reads_value_from_file(self, tmp_path):
38+
_write_env_file(tmp_path, "local-desktop2-standalone\n")
39+
assert get_deploy_environment() == "local-desktop2-standalone"
40+
41+
def test_strips_trailing_whitespace_and_newline(self, tmp_path):
42+
_write_env_file(tmp_path, " local-desktop2-standalone \n")
43+
assert get_deploy_environment() == "local-desktop2-standalone"
44+
45+
def test_only_first_line_is_used(self, tmp_path):
46+
_write_env_file(tmp_path, "first-line\nsecond-line\n")
47+
assert get_deploy_environment() == "first-line"
48+
49+
def test_crlf_line_ending(self, tmp_path):
50+
# Windows editors often save text files with CRLF line endings.
51+
# The CR must not end up in the returned value.
52+
_write_env_file(tmp_path, "local-desktop2-standalone\r\n")
53+
assert get_deploy_environment() == "local-desktop2-standalone"
54+
55+
def test_crlf_multiline_only_first_line_used(self, tmp_path):
56+
_write_env_file(tmp_path, "first-line\r\nsecond-line\r\n")
57+
assert get_deploy_environment() == "first-line"
58+
59+
def test_crlf_with_surrounding_whitespace(self, tmp_path):
60+
_write_env_file(tmp_path, " local-desktop2-standalone \r\n")
61+
assert get_deploy_environment() == "local-desktop2-standalone"
62+
63+
def test_lone_cr_line_ending(self, tmp_path):
64+
# Classic-Mac / some legacy editors use a bare CR.
65+
# Universal-newlines decoding treats it as a line terminator too.
66+
_write_env_file(tmp_path, "local-desktop2-standalone\r")
67+
assert get_deploy_environment() == "local-desktop2-standalone"
68+
69+
def test_empty_file_falls_back_to_default(self, tmp_path):
70+
_write_env_file(tmp_path, "")
71+
assert get_deploy_environment() == "local-git"
72+
73+
def test_empty_after_whitespace_strip_falls_back_to_default(self, tmp_path):
74+
_write_env_file(tmp_path, " \n")
75+
assert get_deploy_environment() == "local-git"
76+
77+
def test_strips_control_chars_within_first_line(self, tmp_path):
78+
# Embedded NUL/control chars in the value should be stripped
79+
# (header-injection / smuggling protection).
80+
_write_env_file(tmp_path, "abc\x00\x07xyz\n")
81+
assert get_deploy_environment() == "abcxyz"
82+
83+
def test_strips_non_ascii_characters(self, tmp_path):
84+
_write_env_file(tmp_path, "café-é\n")
85+
assert get_deploy_environment() == "caf-"
86+
87+
def test_caps_read_at_128_bytes(self, tmp_path):
88+
# A single huge line with no newline must not be fully read into memory.
89+
huge = "x" * 10_000
90+
_write_env_file(tmp_path, huge)
91+
result = get_deploy_environment()
92+
assert result == "x" * 128
93+
94+
def test_result_is_cached_across_calls(self, tmp_path):
95+
path = _write_env_file(tmp_path, "first_value\n")
96+
assert get_deploy_environment() == "first_value"
97+
# Overwrite the file — cached value should still be returned.
98+
with open(path, "w", encoding="utf-8") as f:
99+
f.write("second_value\n")
100+
assert get_deploy_environment() == "first_value"
101+
102+
def test_unreadable_file_falls_back_to_default(self, tmp_path, monkeypatch):
103+
_write_env_file(tmp_path, "should_not_be_used\n")
104+
105+
def _boom(*args, **kwargs):
106+
raise OSError("simulated read failure")
107+
108+
monkeypatch.setattr("builtins.open", _boom)
109+
assert get_deploy_environment() == "local-git"

0 commit comments

Comments
 (0)