mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-03-30 21:43:43 +08:00
Merge 1f8a74b76f into b353a7c863
This commit is contained in:
commit
9940e48ed3
@ -555,7 +555,7 @@ class PromptServer():
|
|||||||
buffer.seek(0)
|
buffer.seek(0)
|
||||||
|
|
||||||
return web.Response(body=buffer.read(), content_type=f'image/{image_format}',
|
return web.Response(body=buffer.read(), content_type=f'image/{image_format}',
|
||||||
headers={"Content-Disposition": f"filename=\"{filename}\""})
|
headers={"Content-Disposition": f"attachment; filename=\"{filename}\""})
|
||||||
|
|
||||||
if 'channel' not in request.rel_url.query:
|
if 'channel' not in request.rel_url.query:
|
||||||
channel = 'rgba'
|
channel = 'rgba'
|
||||||
@ -575,7 +575,7 @@ class PromptServer():
|
|||||||
buffer.seek(0)
|
buffer.seek(0)
|
||||||
|
|
||||||
return web.Response(body=buffer.read(), content_type='image/png',
|
return web.Response(body=buffer.read(), content_type='image/png',
|
||||||
headers={"Content-Disposition": f"filename=\"{filename}\""})
|
headers={"Content-Disposition": f"attachment; filename=\"{filename}\""})
|
||||||
|
|
||||||
elif channel == 'a':
|
elif channel == 'a':
|
||||||
with Image.open(file) as img:
|
with Image.open(file) as img:
|
||||||
@ -592,7 +592,7 @@ class PromptServer():
|
|||||||
alpha_buffer.seek(0)
|
alpha_buffer.seek(0)
|
||||||
|
|
||||||
return web.Response(body=alpha_buffer.read(), content_type='image/png',
|
return web.Response(body=alpha_buffer.read(), content_type='image/png',
|
||||||
headers={"Content-Disposition": f"filename=\"{filename}\""})
|
headers={"Content-Disposition": f"attachment; filename=\"{filename}\""})
|
||||||
else:
|
else:
|
||||||
# Use the content type from asset resolution if available,
|
# Use the content type from asset resolution if available,
|
||||||
# otherwise guess from the filename.
|
# otherwise guess from the filename.
|
||||||
@ -609,7 +609,7 @@ class PromptServer():
|
|||||||
return web.FileResponse(
|
return web.FileResponse(
|
||||||
file,
|
file,
|
||||||
headers={
|
headers={
|
||||||
"Content-Disposition": f"filename=\"{filename}\"",
|
"Content-Disposition": f"attachment; filename=\"{filename}\"",
|
||||||
"Content-Type": content_type
|
"Content-Type": content_type
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
156
tests/test_view_content_disposition.py
Normal file
156
tests/test_view_content_disposition.py
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
"""
|
||||||
|
Tests for the /view endpoint Content-Disposition header fix.
|
||||||
|
|
||||||
|
Verifies that the Content-Disposition header complies with RFC 2183
|
||||||
|
by using `attachment; filename="..."` format instead of just `filename="..."`.
|
||||||
|
|
||||||
|
See: https://github.com/Comfy-Org/ComfyUI/issues/8914
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import struct
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# Simple 1x1 PNG image (minimal valid PNG)
|
||||||
|
MINIMAL_PNG = bytes([
|
||||||
|
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, # PNG signature
|
||||||
|
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, # IHDR chunk length + type
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, # 1x1 dimensions
|
||||||
|
0x08, 0x02, 0x00, 0x00, 0x00, # bit depth, color type, etc
|
||||||
|
0x90, 0x77, 0x53, 0xDE, # IHDR CRC
|
||||||
|
0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, # IDAT chunk
|
||||||
|
0x08, 0xD7, 0x63, 0xF8, 0xFF, 0xFF, 0xFF, 0x00, # image data
|
||||||
|
0x05, 0xFE, 0x02, 0xFE, # IDAT CRC
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, # IEND chunk
|
||||||
|
0xAE, 0x42, 0x60, 0x82, # IEND CRC
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.execution
|
||||||
|
class TestViewContentDisposition:
|
||||||
|
"""Test suite for Content-Disposition header in /view endpoint."""
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class", autouse=True)
|
||||||
|
def output_dir(self, args_pytest):
|
||||||
|
"""Use the test output directory."""
|
||||||
|
return args_pytest["output_dir"]
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class", autouse=True)
|
||||||
|
def _server(self, args_pytest):
|
||||||
|
"""Start ComfyUI server for testing."""
|
||||||
|
pargs = [
|
||||||
|
'python', 'main.py',
|
||||||
|
'--output-directory', args_pytest["output_dir"],
|
||||||
|
'--listen', args_pytest["listen"],
|
||||||
|
'--port', str(args_pytest["port"]),
|
||||||
|
'--cpu',
|
||||||
|
]
|
||||||
|
p = subprocess.Popen(pargs, cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
yield
|
||||||
|
p.kill()
|
||||||
|
p.wait()
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
def server_url(self, args_pytest):
|
||||||
|
"""Get the server base URL."""
|
||||||
|
return f"http://{args_pytest['listen']}:{args_pytest['port']}"
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
def client(self, server_url, _server):
|
||||||
|
"""Create HTTP client with retry."""
|
||||||
|
import requests
|
||||||
|
session = requests.Session()
|
||||||
|
n_tries = 10
|
||||||
|
for i in range(n_tries):
|
||||||
|
time.sleep(2)
|
||||||
|
try:
|
||||||
|
resp = session.get(f"{server_url}/system_stats", timeout=5)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
break
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
if i == n_tries - 1:
|
||||||
|
raise
|
||||||
|
return session
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_image_file(self, output_dir):
|
||||||
|
"""Create a temporary test image file and clean it up after the test."""
|
||||||
|
filename = "test_content_disposition.png"
|
||||||
|
filepath = os.path.join(output_dir, filename)
|
||||||
|
with open(filepath, 'wb') as f:
|
||||||
|
f.write(MINIMAL_PNG)
|
||||||
|
yield filename, filepath
|
||||||
|
# Cleanup
|
||||||
|
if os.path.exists(filepath):
|
||||||
|
os.remove(filepath)
|
||||||
|
|
||||||
|
def test_view_content_disposition_is_attachment_format(self, client, server_url, test_image_file):
|
||||||
|
"""Test that /view endpoint returns Content-Disposition with RFC 2183 compliant format.
|
||||||
|
|
||||||
|
The header should be: Content-Disposition: attachment; filename="test_content_disposition.png"
|
||||||
|
NOT: Content-Disposition: filename="test_content_disposition.png"
|
||||||
|
|
||||||
|
This test validates the fix for: https://github.com/Comfy-Org/ComfyUI/issues/8914
|
||||||
|
"""
|
||||||
|
filename, _ = test_image_file
|
||||||
|
response = client.get(
|
||||||
|
f"{server_url}/view",
|
||||||
|
params={"filename": filename},
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200, f"Expected 200, got {response.status_code}"
|
||||||
|
|
||||||
|
content_disposition = response.headers.get("Content-Disposition", "")
|
||||||
|
assert content_disposition, "Content-Disposition header should be present"
|
||||||
|
|
||||||
|
# RFC 2183 compliant format: "attachment; filename=\"name.ext\""
|
||||||
|
expected_prefix = f"attachment; filename=\"{filename}\""
|
||||||
|
assert content_disposition == expected_prefix, (
|
||||||
|
f"Content-Disposition header should be RFC 2183 compliant.\n"
|
||||||
|
f"Expected: {expected_prefix}\n"
|
||||||
|
f"Got: {content_disposition}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_view_with_subfolder_content_disposition(self, client, server_url, output_dir):
|
||||||
|
"""Test Content-Disposition with subfolder parameter."""
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Create a subfolder and test image
|
||||||
|
subfolder = "test_subfolder"
|
||||||
|
subfolder_path = os.path.join(output_dir, subfolder)
|
||||||
|
os.makedirs(subfolder_path, exist_ok=True)
|
||||||
|
|
||||||
|
filename = "test_subfolder.png"
|
||||||
|
filepath = os.path.join(subfolder_path, filename)
|
||||||
|
with open(filepath, 'wb') as f:
|
||||||
|
f.write(MINIMAL_PNG)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = client.get(
|
||||||
|
f"{server_url}/view",
|
||||||
|
params={"filename": filename, "subfolder": subfolder},
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200, f"Expected 200, got {response.status_code}"
|
||||||
|
|
||||||
|
content_disposition = response.headers.get("Content-Disposition", "")
|
||||||
|
assert content_disposition, "Content-Disposition header should be present"
|
||||||
|
|
||||||
|
expected_prefix = f"attachment; filename=\"{filename}\""
|
||||||
|
assert content_disposition == expected_prefix, (
|
||||||
|
f"Content-Disposition header should be RFC 2183 compliant.\n"
|
||||||
|
f"Expected: {expected_prefix}\n"
|
||||||
|
f"Got: {content_disposition}"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
# Cleanup
|
||||||
|
if os.path.exists(filepath):
|
||||||
|
os.remove(filepath)
|
||||||
|
if os.path.exists(subfolder_path):
|
||||||
|
os.rmdir(subfolder_path)
|
||||||
Loading…
Reference in New Issue
Block a user