Next.js Routing and Redirects#
Overview#
Dify's web frontend uses Next.js App Router with a configurable basePath for sub-path deployments (e.g., running the UI at /dify instead of /). Routing behavior is centralized in next.config.ts, with authentication redirect flows handled by a dedicated server-side route handler.
Key Files#
| File | Role |
|---|---|
web/next.config.ts | Next.js config: basePath, static redirects |
web/proxy.ts | Next.js middleware: CSP, frame protection headers |
web/app/auth/refresh/route.ts | Server-side token refresh + redirect handler |
web/app/(commonLayout)/hydration-boundary.tsx | RSC boundary that triggers refresh redirects on 401 |
web/utils/var.ts | Exports basePath from env.NEXT_PUBLIC_BASE_PATH |
web/next/navigation.ts | Re-exports Next.js navigation primitives (canonical import point) |
basePath Configuration#
basePath is set in next.config.ts from env.NEXT_PUBLIC_BASE_PATH. This is the only build-time variable baked into the image at build (rather than injected at runtime) . Valid values must start with / and not end with / (e.g., /dify); the default is an empty string.
Auto-prepending behavior: When basePath is set in next.config.ts, Next.js automatically prepends it to all <Link> hrefs, router.push() calls, redirect() calls, and asset paths β neither client nor server code needs to manually add the prefix. This applies uniformly across client components, server components, and route handlers.
Application-level usage: The basePath export from web/utils/var.ts is used in limited cases where the base path must be computed or inspected directly. For example, auth/refresh/route.ts uses it to define the AUTH_REFRESH_PATH constant and to prepend it to signin redirect URLs constructed with NextResponse.redirect(). Server-side redirects triggered via next/navigation's redirect() function do not require manual prepending; Next.js handles basePath automatically.
Static Redirects#
next.config.ts defines a static redirect: /explore/apps β / (permanent, 308)
Frame protection (anti-clickjacking): Iframe embedding security is enforced at the application level by the Next.js proxy middleware (web/proxy.ts). The proxy sets X-Frame-Options: DENY and Content-Security-Policy: frame-ancestors 'none' on protected routes (including /device and console routes) while allowing published app routes (/chat, /workflow, /completion, /webapp-signin, /agent, /chatbot) to remain embeddable. See the "Iframe Embedding Security" documentation for details on the proxy's frame protection implementation.
Auth Refresh Redirect Flow#
The token refresh flow uses a dedicated Next.js Route Handler at GET /auth/refresh.
Trigger: CommonLayoutHydrationBoundary in the common layout RSC catches 401 API responses and calls redirectToAuthRefresh(), which redirects to /auth/refresh?redirect_url=<current-path> using next/navigation's redirect() function. Because basePath is configured in next.config.ts, Next.js automatically prepends it to the destinationβno manual prepending is needed. The current path is read from x-dify-pathname and x-dify-search request headers injected by middleware .
Refresh handler logic (route.ts):
- Extract and validate
redirect_urlfrom query params viaresolveSafeRedirectTarget() - POSTs to the backend
/refresh-tokenendpoint using the request's cookies - On success: returns HTTP 303 to the validated
redirect_url, forwardingSet-Cookieheaders from the backend - On failure (no cookie, no refresh URL, or non-OK response): returns HTTP 303 to
${basePath}/signin?redirect_url=<encoded-target>
redirect_url validation prevents open redirects and loops :
- Absolute same-origin URLs are not allowed (
allowSameOriginAbsolute: false) - Uses
resolveLoginRedirectTarget()to classify the target asinternalor absolute; if neither, falls back to the server login fallback - Internal targets without
basePathhave it prepended viaaddBasePathToInternalTarget() - Redirecting back to
/auth/refreshitself is blocked (redirect loop prevention)
Relative redirect URLs: All Location headers are relative paths, not absolute URLs. This was introduced in PR #36847 to prevent leaking internal service hostnames in split-origin deployments (where the web frontend and API backend run on different hosts/ports).
Navigation Primitives#
web/next/navigation.ts is a thin re-export of next/navigation. All internal code should import navigation hooks (useRouter, usePathname, redirect, etc.) from @/next/navigation rather than directly from next/navigation. This provides a single indirection point if the import source ever needs to change.
Related PRs#
| PR | Summary |
|---|---|
| #36847 fix(auth): avoid leaking request origin in refresh redirects | Switched redirect Location headers from absolute to relative paths to prevent internal hostname disclosure |
| #38538 test(web): align auth e2e with console home | Aligned E2E tests to use / as console home; added session-refresh and basePath-aware unit test coverage |
| #39273 fix(web): stop doubling basePath in auth refresh redirects | Removed manual basePath prepending from hydration-boundary.tsx redirect callsβnext/navigation's redirect() applies basePath automatically, so manual prepending caused double-prefixing (e.g., /workflow/workflow/auth/refresh instead of /workflow/auth/refresh) |
| #40124 fix(web): enforce frame ancestors on protected routes | Moved anti-framing headers from next.config.ts to the proxy middleware; consolidated frame protection logic to enforce frame-ancestors 'none' alongside X-Frame-Options: DENY on all protected routes (including /device), while preserving embedability for published app routes |