Merge ec820735c7 into a9ab2b62da
This commit is contained in:
commit
bcf56cb0bd
|
|
@ -382,6 +382,12 @@ Use `--tls-keyfile key.pem --tls-certfile cert.pem` to enable TLS/SSL, the app w
|
|||
> Note: Windows users can use [alexisrolland/docker-openssl](https://github.com/alexisrolland/docker-openssl) or one of the [3rd party binary distributions](https://wiki.openssl.org/index.php/Binaries) to run the command example above.
|
||||
<br/><br/>If you use a container, note that the volume mount `-v` can be a relative path so `... -v ".\:/openssl-certs" ...` would create the key & cert files in the current directory of your command prompt or powershell terminal.
|
||||
|
||||
## How to browse the HTTP API?
|
||||
|
||||
Launch with `--enable-api-docs` to serve the OpenAPI specification at `/openapi.yaml` and a browsable API reference at `/api-docs`.
|
||||
|
||||
Both are off by default. The reference page loads its viewer from a CDN, so it needs outbound network access to render; the specification itself is served locally and can be read with any offline viewer, such as `npx @redocly/cli preview-docs openapi.yaml`.
|
||||
|
||||
## Support and dev channel
|
||||
|
||||
[Discord](https://comfy.org/discord): Try the #help or #feedback channels.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
"""Serves openapi.yaml and renders it as browsable API docs.
|
||||
|
||||
Uses Redoc, not Swagger UI: the local server is unauthenticated by default, so
|
||||
the viewer must not be able to execute requests.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
SPEC_PATH = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "openapi.yaml"
|
||||
)
|
||||
|
||||
# The spec URL is relative so it resolves from both /api-docs and /api/api-docs.
|
||||
# The viewer is pinned and loaded from a CDN, so the page needs network access;
|
||||
# if it fails to load, the fallback below points at the locally served spec.
|
||||
API_DOCS_HTML = """<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>ComfyUI API Reference</title>
|
||||
</head>
|
||||
<body style="margin: 0">
|
||||
<redoc spec-url="openapi.yaml"></redoc>
|
||||
<div id="fallback" style="display: none; margin: 3rem; line-height: 1.6">
|
||||
The API docs viewer is loaded from a CDN and could not be reached. The
|
||||
specification itself is served locally: <a href="openapi.yaml">openapi.yaml</a>
|
||||
</div>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/redoc@2.5.0/bundles/redoc.standalone.js"
|
||||
onerror="document.getElementById('fallback').style.display='block'"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def add_api_docs_routes(routes: web.RouteTableDef) -> None:
|
||||
"""Register /openapi.yaml and /api-docs on the given route table."""
|
||||
|
||||
@routes.get("/openapi.yaml")
|
||||
async def get_openapi_spec(request):
|
||||
# The spec is edited in place during development, so never let the
|
||||
# browser hold a stale copy. no-cache still allows a 304 via the ETag
|
||||
# FileResponse sets, which matters for a ~230 KB file.
|
||||
return web.FileResponse(SPEC_PATH, headers={
|
||||
"Content-Type": "application/yaml",
|
||||
"Cache-Control": "no-cache",
|
||||
})
|
||||
|
||||
@routes.get("/api-docs")
|
||||
async def get_api_docs(request):
|
||||
return web.Response(
|
||||
text=API_DOCS_HTML,
|
||||
content_type="text/html",
|
||||
headers={"Cache-Control": "no-cache"},
|
||||
)
|
||||
|
|
@ -66,6 +66,7 @@ parser.add_argument("--tls-keyfile", type=str, help="Path to TLS (SSL) key file.
|
|||
parser.add_argument("--tls-certfile", type=str, help="Path to TLS (SSL) certificate file. Enables TLS, makes app accessible at https://... requires --tls-keyfile to function")
|
||||
parser.add_argument("--enable-cors-header", type=str, default=None, metavar="ORIGIN", nargs="?", const="*", help="Enable CORS (Cross-Origin Resource Sharing) with optional origin or allow all with default '*'.")
|
||||
parser.add_argument("--max-upload-size", type=float, default=100, help="Set the maximum upload size in MB.")
|
||||
parser.add_argument("--enable-api-docs", action="store_true", help="Serve the OpenAPI spec at /openapi.yaml and browsable API docs at /api-docs. The docs page loads its viewer from a CDN, so it needs outbound network access to render.")
|
||||
|
||||
parser.add_argument("--base-directory", type=str, default=None, help="Set the ComfyUI base directory for models, custom_nodes, input, output, temp, and user directories.")
|
||||
parser.add_argument("--extra-model-paths-config", type=str, default=None, metavar="PATH", nargs='+', action='append', help="Load one or more extra_model_paths.yaml files.")
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ from comfy_api import feature_flags
|
|||
from comfy.comfy_api_env import get_environment_overrides
|
||||
import node_helpers
|
||||
from comfyui_version import __version__
|
||||
from app.api_docs import add_api_docs_routes
|
||||
from app.frontend_management import FrontendManager, parse_version
|
||||
from comfy_api.internal import _ComfyNodeInternal
|
||||
from app.assets.seeder import asset_seeder
|
||||
|
|
@ -1223,6 +1224,8 @@ class PromptServer():
|
|||
self.custom_node_manager.add_routes(self.routes, self.app, nodes.LOADED_MODULE_DIRS.items())
|
||||
self.subgraph_manager.add_routes(self.routes, nodes.LOADED_MODULE_DIRS.items())
|
||||
self.node_replace_manager.add_routes(self.routes)
|
||||
if args.enable_api_docs:
|
||||
add_api_docs_routes(self.routes)
|
||||
self.app.add_subapp('/internal', self.internal_routes.get_app())
|
||||
|
||||
# Prefix every route with /api for easier matching for delegation.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
"""Tests for the OpenAPI spec and API docs routes."""
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
import yaml
|
||||
from aiohttp import web
|
||||
|
||||
from app.api_docs import add_api_docs_routes
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def client(aiohttp_client):
|
||||
app = web.Application()
|
||||
routes = web.RouteTableDef()
|
||||
add_api_docs_routes(routes)
|
||||
app.add_routes(routes)
|
||||
return await aiohttp_client(app)
|
||||
|
||||
|
||||
async def test_get_spec(client):
|
||||
resp = await client.get("/openapi.yaml")
|
||||
assert resp.status == 200
|
||||
assert resp.headers["Content-Type"] == "application/yaml"
|
||||
assert resp.headers["Cache-Control"] == "no-cache"
|
||||
spec = yaml.safe_load(await resp.text())
|
||||
assert spec["openapi"].startswith("3.")
|
||||
assert spec["paths"]
|
||||
|
||||
|
||||
async def test_missing_spec_returns_404(client, monkeypatch):
|
||||
monkeypatch.setattr("app.api_docs.SPEC_PATH", "/nonexistent/openapi.yaml")
|
||||
resp = await client.get("/openapi.yaml")
|
||||
assert resp.status == 404
|
||||
|
||||
|
||||
async def test_docs_page(client):
|
||||
resp = await client.get("/api-docs")
|
||||
assert resp.status == 200
|
||||
assert resp.content_type == "text/html"
|
||||
body = await resp.text()
|
||||
# Redoc has no request execution; Swagger UI's "Try it out" would give
|
||||
# one-click access to destructive endpoints on an unauthenticated server.
|
||||
assert "<redoc" in body
|
||||
assert "openapi.yaml" in body
|
||||
|
||||
|
||||
async def test_docs_page_has_offline_fallback(client):
|
||||
"""Offline installs should get a notice, not a blank page."""
|
||||
body = await (await client.get("/api-docs")).text()
|
||||
assert 'id="fallback"' in body
|
||||
|
||||
|
||||
async def test_spec_url_is_relative(client):
|
||||
"""What makes the page work under both /api-docs and /api/api-docs.
|
||||
|
||||
server.py re-registers every route under an /api prefix; a relative spec
|
||||
URL resolves to the sibling spec from either mount point.
|
||||
"""
|
||||
body = await (await client.get("/api-docs")).text()
|
||||
assert 'spec-url="openapi.yaml"' in body
|
||||
|
||||
|
||||
async def test_flag_is_off_by_default():
|
||||
"""Serving the API surface of an unauthenticated server is opt-in."""
|
||||
from comfy.cli_args import parser
|
||||
|
||||
assert parser.parse_args([]).enable_api_docs is False
|
||||
assert parser.parse_args(["--enable-api-docs"]).enable_api_docs is True
|
||||
Loading…
Reference in New Issue