Icon URL Resolution#
Dify stores app and agent icons as file UUIDs (or emoji strings) in the database. When entities are serialized to API responses, image-type icons must be converted into time-limited signed URLs so clients can actually fetch them. This conversion happens at serialization time β usually inside Pydantic response models β via the build_icon_url helper and graphon.file.helpers.get_signed_file_url.
Key components#
| Component | Location | Role |
|---|---|---|
build_icon_url(icon_type, icon) | api/libs/helper.py | Guards the IconType.IMAGE check, then delegates to get_signed_file_url |
file_helpers.get_signed_file_url(upload_file_id) | graphon.file.helpers (external package) | Generates the actual time-limited signed URL from a file UUID |
AppIconUrlField | api/libs/helper.py | Legacy Flask-RESTX field class that wraps build_icon_url for older schema-marshalling code |
FileService.get_icon_url(file_id, tenant_id) | api/services/file_service.py | Service-layer method that chooses Cloud/S3 presigned URLs (get_file_presigned_url) or falls back to get_signed_file_url; used by web app bootstrap |
build_icon_url logic: returns None if icon or icon_type is None, or if icon_type is not IconType.IMAGE (i.e., emoji icons are passed through unchanged) . The signed URL itself is generated by file_helpers.get_signed_file_url(icon), which uses the FILES_URL / FILES_ACCESS_TIMEOUT configuration (default 300 s) .
Where icon_url is added to responses#
Icon URL resolution is applied via Pydantic @computed_field so the resolved URL is automatically included in model_dump() output without any manual call-site wiring.
App responses (api/controllers/console/app/app.py) β all three main app response shapes carry icon_url:
AppPartial.icon_urlβ app list itemsRecentAppResponse.icon_urlβ home-page "Continue Work" cardsAppDetailWithSite.icon_urlβ full app detail / create / update responsesAppDetailSiteResponse.icon_urlβ embedded site sub-object within app detail
Explore / recommended apps (api/controllers/console/explore/recommended_app.py):
RecommendedAppInfoResponse.icon_urlβ uses@computed_fieldwithbuild_icon_url
Site / web public responses (api/controllers/web/site.py):
WebSiteResponse.icon_urlis built byWebAppRuntimeQueryService.get_bootstrap()(inapi/services/web_app_runtime_query_service.py) during the web app bootstrap phase. The service callsFileService.get_icon_url()to select the appropriate URL scheme: in Cloud deployments with S3 storage it usesget_file_presigned_url()for a direct S3 presigned URL; otherwise it usesfile_helpers.get_signed_file_url().
Knowledge dataset related-apps (api/controllers/console/datasets/datasets.py):
RelatedAppResponse.icon_urluses@model_validator(mode="after")instead of@computed_field:self.icon_url = self.icon_url or build_icon_url(self.icon_type, self.icon). This allows a pre-computed URL to be passed in and preserved.
Site model in shared fields (api/controllers/common/fields.py):
Site.icon_urluses@computed_fieldbut callsfile_helpers.get_signed_file_urldirectly rather than going throughbuild_icon_url.
Gap: AgentRosterResponse has no icon_url#
AgentRosterResponse (and the related AgentComposerAgentResponse) expose raw icon and icon_type fields but do not include an icon_url computed field. Consumers of the agent roster API receive only the raw UUID β they must resolve it themselves or accept that no signed URL is provided. This is inconsistent with all of the app-entity response types described above.
Configuration#
Signed URL expiry and base URL are governed by FileAccessConfig :
| Env var | Purpose | Default |
|---|---|---|
FILES_URL | Base URL embedded in all signed file URLs | CONSOLE_API_URL |
FILES_ACCESS_TIMEOUT | Signed URL TTL in seconds | 300 |
INTERNAL_FILES_URL | Internal base URL for plugin daemon access | falls back to FILES_URL |
Quick-reference: patterns in use#
| Pattern | Example model | Mechanism |
|---|---|---|
@computed_field + build_icon_url | AppPartial, RecommendedAppInfoResponse | Most common; automatic on model_dump() |
@computed_field + inline get_signed_file_url | Site (common/fields.py) | Same outcome, bypasses the helper |
@model_validator(mode="after") + build_icon_url | RelatedAppResponse | Allows pre-set override |
| Service-layer resolution (bootstrap phase) | WebSiteResponse (via WebAppRuntimeQueryService) | FileService.get_icon_url() called during bootstrap; supports Cloud/S3 presigned URLs |
| Not resolved | AgentRosterResponse, AgentComposerAgentResponse | Raw UUID exposed; no icon_url field |