Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions scripts/generate_oss_openapi_schema.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
import json

from prefect.server.api.server import create_api_app
from prefect.server.api.server import create_api_app, create_ui_app

app = create_api_app()
openapi_schema = app.openapi()
# Generate schema from main API app
api_app = create_api_app()
openapi_schema = api_app.openapi()

# Generate schema from UI app and merge relevant routes
ui_app = create_ui_app(ephemeral=True)
ui_schema = ui_app.openapi()

# Merge /ui-settings path from ui_app into main schema
for path, path_item in ui_schema.get("paths", {}).items():
if "ui-settings" in path:
# Use the path without any base URL prefix
openapi_schema["paths"][path] = path_item
Comment on lines +13 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action Required

1. Ui path prefix leaks 🐞 Bug

• The OSS schema generator claims it will remove any UI base-path prefix, but it copies the UI app’s
  OpenAPI path verbatim.
• Since the UI route is registered under PREFECT_UI_SERVE_BASE (via stripped_base_url),
  oss_schema.json can end up with /ui/ui-settings instead of /ui-settings, making client
  generation/configuration inconsistent.
• The substring filter ("ui-settings" in path) is also fragile and can unintentionally merge other
  future endpoints containing that substring.
Agent Prompt
## Issue description
`scripts/generate_oss_openapi_schema.py` intends to merge `/ui-settings` without any UI base-path prefix, but it currently copies the UI app OpenAPI path verbatim and selects by substring match.

This makes `oss_schema.json` (and any generated clients) dependent on `PREFECT_UI_SERVE_BASE` and fragile if new routes contain `ui-settings` in their path.

## Issue Context
The UI app registers the endpoint at `f"{stripped_base_url}/ui-settings"`, where `stripped_base_url` comes from `PREFECT_UI_SERVE_BASE`.

## Fix Focus Areas
- scripts/generate_oss_openapi_schema.py[13-17]
- src/prefect/server/api/server.py[456-484]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


# Merge UISettings schema component
for schema_name, schema_def in (
ui_schema.get("components", {}).get("schemas", {}).items()
):
openapi_schema["components"]["schemas"][schema_name] = schema_def
Comment on lines +19 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation Recommended

2. Overbroad schema merge 🐞 Bug

• The generator comment says it is merging the UISettings schema component, but it actually copies
  *all* schemas from the UI app into the API schema.
• It overwrites existing component schemas unconditionally, so shared schema names (e.g., FastAPI’s
  common validation schemas) can be silently replaced, changing generated client types unexpectedly.
• This makes the OSS schema sensitive to unrelated UI schema changes and can introduce hard-to-debug
  diffs in downstream generated code.
Agent Prompt
## Issue description
`scripts/generate_oss_openapi_schema.py` currently merges *all* UI OpenAPI component schemas into the API schema and overwrites any existing schema with the same name.

This can silently change widely-used schemas and destabilize generated clients.

## Issue Context
The script comment indicates only `UISettings` should be merged, but the implementation copies every schema from the UI app.

## Fix Focus Areas
- scripts/generate_oss_openapi_schema.py[19-23]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


with open("oss_schema.json", "w") as f:
json.dump(openapi_schema, f)
15 changes: 8 additions & 7 deletions src/prefect/server/api/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from prefect.server.api.background_workers import background_worker
from prefect.server.api.dependencies import EnforceMinimumAPIVersion
from prefect.server.exceptions import ObjectNotFoundError
from prefect.server.schemas.ui import UISettings
from prefect.server.services.base import RunInEphemeralServers, RunInWebservers, Service
from prefect.server.utilities.database import get_dialect
from prefect.settings import (
Expand Down Expand Up @@ -480,15 +481,15 @@ def create_ui_app(ephemeral: bool) -> FastAPI:
mimetypes.add_type("application/javascript", ".js")

@ui_app.get(f"{stripped_base_url}/ui-settings")
def ui_settings() -> dict[str, Any]: # type: ignore[reportUnusedFunction]
return {
"api_url": prefect.settings.PREFECT_UI_API_URL.value(),
"csrf_enabled": prefect.settings.PREFECT_SERVER_CSRF_PROTECTION_ENABLED.value(),
"auth": "BASIC"
def ui_settings() -> UISettings: # type: ignore[reportUnusedFunction]
return UISettings(
api_url=prefect.settings.PREFECT_UI_API_URL.value(),
csrf_enabled=prefect.settings.PREFECT_SERVER_CSRF_PROTECTION_ENABLED.value(),
auth="BASIC"
if prefect.settings.PREFECT_SERVER_API_AUTH_STRING.value()
else None,
"flags": [],
}
flags=[],
)

def reference_file_matches_base_url() -> bool:
reference_file_path = os.path.join(static_dir, reference_file_name)
Expand Down
13 changes: 13 additions & 0 deletions src/prefect/server/schemas/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,23 @@

from typing import Optional

from pydantic import BaseModel, Field

from prefect.server.schemas.core import TaskRun as CoreTaskRun


class UITaskRun(CoreTaskRun):
"""A task run with additional details for display in the UI."""

flow_run_name: Optional[str] = None


class UISettings(BaseModel):
"""Runtime UI configuration returned by /ui-settings endpoint."""

api_url: str = Field(description="The base URL for API requests.")
csrf_enabled: bool = Field(description="Whether CSRF protection is enabled.")
auth: Optional[str] = Field(
description="Authentication method (e.g., 'BASIC') or null if disabled."
)
flags: list[str] = Field(description="List of enabled feature flags.")
63 changes: 63 additions & 0 deletions ui-v2/src/api/prefect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2982,6 +2982,23 @@ export interface paths {
patch?: never;
trace?: never;
};
"/ui-settings": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Ui Settings */
get: operations["ui_settings_ui_settings_get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
Expand Down Expand Up @@ -12057,6 +12074,32 @@ export interface components {
*/
port: number;
};
/**
* UISettings
* @description Runtime UI configuration returned by /ui-settings endpoint.
*/
UISettings: {
/**
* Api Url
* @description The base URL for API requests.
*/
api_url: string;
/**
* Csrf Enabled
* @description Whether CSRF protection is enabled.
*/
csrf_enabled: boolean;
/**
* Auth
* @description Authentication method (e.g., 'BASIC') or null if disabled.
*/
auth: string | null;
/**
* Flags
* @description List of enabled feature flags.
*/
flags?: string[];
};
};
responses: never;
parameters: never;
Expand Down Expand Up @@ -18284,4 +18327,24 @@ export interface operations {
};
};
};
ui_settings_ui_settings_get: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["UISettings"];
};
};
};
};
}
Loading