File URL Resolution#
Overview#
Dify uses two base URL settings to serve file content: FILES_URL for external/public access and INTERNAL_FILES_URL for internal service-to-service communication (primarily the plugin daemon). When backend code needs to fetch a Dify-owned signed file URL, it goes through remote_fetcher.py, which resolves the file locally from storage (database lookup + ext_storage) rather than making an outbound HTTP round-trip. Unrecognized URLs fall back to the SSRF-proxied network client.
Configuration: FILES_URL vs INTERNAL_FILES_URL#
Both settings live in FileAccessConfig in api/configs/feature/__init__.py:
| Setting | Env vars (in priority order) | Purpose |
|---|---|---|
FILES_URL | FILES_URL, CONSOLE_API_URL | External base URL for signed file URLs β used in browser-facing links and multimodal model inputs. |
INTERNAL_FILES_URL | INTERNAL_FILES_URL, SERVER_CONSOLE_API_URL | Internal base URL for Docker-internal service access (plugin daemon). Falls back to FILES_URL if unset. |
FILES_ACCESS_TIMEOUT | FILES_ACCESS_TIMEOUT | Expiry for all signed file URLs; default 300 s. |
The INTERNAL_FILES_URL field's description explicitly states it is "used for plugin daemon and internal service communication" . In a typical Docker Compose deployment, INTERNAL_FILES_URL is set to http://api:5001 so the plugin daemon can reach the API container directly, while FILES_URL points to the public-facing domain.
URI Generation and URL Binding#
Dify now separates file URL resolution into two stages: URI generation (origin-free signed paths) and URL binding (attaching the appropriate base URL for the target audience).
URI Generation#
Signature functions in api/core/tools/signature.py produce origin-free /files/... URIs with HMAC signatures:
sign_tool_file_urireturns/files/tools/{id}{ext}?timestamp=...&nonce=...&sign=...without any base URL.get_signed_file_uri_for_pluginreturns/files/upload/for-plugin?...for plugin upload endpoints.
These URIs are network-agnostic: they contain no origin and can be bound to different base URLs for different consumers (plugins, workflows, browser clients, internal services).
URL Binding#
bind_file_uri attaches a base URL to an origin-free /files/... URI:
bind_file_uri(uri, base_url)
The base_url parameter can be:
- Explicit: a caller-provided URL for custom contexts.
FILES_URLfor browser-facing links or external clients.INTERNAL_FILES_URL(falling back toFILES_URLif unset) for internal services.
Backward-Compatible Wrappers#
sign_tool_file combines URI generation and binding for convenience:
for_external=True(default): callssign_tool_file_urithen binds the result toFILES_URLβ suitable for client-facing URLs.for_external=False: callssign_tool_file_urithen binds the result toINTERNAL_FILES_URL or FILES_URLβ used when plugins or internal workflow steps need to fetch the file.
sign_upload_file_preview_url always uses FILES_URL β it is only for external preview/download.
Local Resolution in remote_fetcher.py#
api/core/file/remote_fetcher.py is the canonical client for any backend code that needs to fetch the content of a file URL (workflow nodes, tool callers, pipeline steps). Its module docstring distinguishes it from ssrf_proxy.py, which is for generic outbound HTTP.
Resolution logic in make_request:
- For
GETandHEADrequests,_resolve_dify_signed_file_urlis called first. _is_dify_file_originchecks if the URL's origin (scheme + host + port) matches eitherFILES_URLorINTERNAL_FILES_URL. Both are in the allowed set β this means a signed URL built with the internal base URL is also resolved locally.- If origin matches, the path is matched against three patterns :
/files/<id>/<file-preview|image-preview>β upload file/files/tools/<id>β tool file/files/datasources/<id>β datasource file
- The HMAC signature is verified locally (same algorithm as the HTTP endpoint).
- On success, file content is loaded from
ext_storageviastorage.load_onceand returned as a synthetichttpx.Responseβ no network request is made. - If any check fails (wrong origin, bad signature, unknown path), the call falls through to
ssrf_proxy.make_request.
SSRF Proxy Bypass#
Because Dify file URLs are served from FILES_URL, which often points to localhost or the Docker API hostname, they would normally be blocked by the Squid SSRF proxy (which denies private/internal addresses by default). The local resolution in remote_fetcher.py sidesteps this entirely: recognized Dify file URLs are served from storage before the SSRF proxy is ever involved.
Do not use ssrf_proxy.make_request directly for file content. The remote_fetcher.py module docstring explicitly calls this out: ssrf_proxy is for generic outbound HTTP (HTTP Request nodes, tool calls, auth discovery), not for Dify file content retrieval.
Plugin Access and SSL Issues#
Plugins (running in the plugin daemon) access files via the URL embedded in the File object passed to them. The URL is built with INTERNAL_FILES_URL (or FILES_URL if INTERNAL_FILES_URL is unset). When INTERNAL_FILES_URL is not set and FILES_URL is an HTTPS endpoint with a self-signed certificate, plugins may encounter SSL errors . The fix is to set INTERNAL_FILES_URL=http://api:5001 so plugins reach the API container over plain HTTP on the Docker network.
Key Files#
| File | Role |
|---|---|
api/core/file/remote_fetcher.py | Main entry point for backend file fetching; implements local resolution |
api/core/tools/signature.py | Generates signed URIs and binds them to base URLs |
api/core/app/workflow/file_runtime.py | Workflow file runtime; provides resolve_file_uri for origin-free URIs |
api/configs/feature/__init__.py | Defines FILES_URL, INTERNAL_FILES_URL, FILES_ACCESS_TIMEOUT |
api/core/helper/ssrf_proxy.py | Generic outbound HTTP; used as fallback by remote_fetcher |