Report why the Node SSR server failed, and serve without SSR on the expected port - #13681
Conversation
sanitize-html require()s postcss at module load to parse style attributes. Before the vite 8 upgrade postcss was bundled into the SSR output; after it, it became external and is looked up at runtime — where the only node_modules the build output has is the one after_build.js installs. Locally and in CI the repo's own node_modules satisfies it, so every SSR render 500s only once gradio is installed from a wheel, which left Python on its internal port and crashed HF Spaces. Adds postcss to ssr.noExternal, a post-build check that fails when the SSR output loads a package it doesn't ship, and surfaces Node's stderr instead of blaming the Node installation when startup verification fails. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🪼 branch checks and previews
Install Gradio from this PR pip install https://huggingface.co/buckets/gradio/pypi-previews/resolve/4ea89dc803bd5e280a7c05d1d689da1227243a69/gradio-6.20.0-py3-none-any.whlInstall Gradio Python Client from this PR pip install "gradio-client @ git+https://github.com/gradio-app/gradio@4ea89dc803bd5e280a7c05d1d689da1227243a69#subdirectory=client/python"Import Gradio JS Client from this PR via CDN import { Client } from "https://huggingface.co/buckets/gradio/npm-previews/resolve/4ea89dc803bd5e280a7c05d1d689da1227243a69/browser.js"; |
🦄 change detectedThis Pull Request includes changes to the following packages.
|
In proxy mode Python binds an internal port and lets Node own the user-facing one, so a Node server that never serves left the app answering only where nothing routes — a Space is judged by the user-facing port, so it was reported as crashed while Python was healthy. Move Python onto the user-facing port instead and serve client-side rendered. The new listener is started before the internal one is released, so failing to take the port leaves the app reachable where it already was. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes SSR mode failures in installed (wheel) builds by ensuring SSR server chunks don’t rely on runtime-resolved dependencies that aren’t shipped, and improves startup diagnostics + graceful fallback behavior when the Node SSR proxy cannot serve.
Changes:
- Bundle
postcssinto the SSR build output (viassr.noExternal) to preventCannot find module 'postcss'at render time in installed builds. - Add a post-build verifier (
verify_build.js) that scans SSR server output for runtime-loaded bare specifiers that would resolve only via the repo’snode_modules. - Improve Node SSR startup failure reporting (surface Node stderr) and add a Python-side fallback to serve on the user-facing port without SSR when Node fails.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
js/app/vite.config.ts |
Adds postcss to ssr.noExternal so SSR output is self-contained in installed builds. |
js/app/verify_build.js |
New build-time verifier to detect unshipped runtime dependencies in SSR server output. |
js/app/package.json |
Runs verify_build.js as part of the build script after after_build.js. |
gradio/node_server.py |
Captures and surfaces Node stderr on startup failure (bounded tail) instead of a generic Node-install hint. |
gradio/blocks.py |
Implements SSR degradation path that rebinds Python onto the user-facing port when Node proxy fails. |
test/test_node_proxy.py |
Adds tests for Node stderr surfacing and for user-facing-port fallback behavior. |
.changeset/loud-nodes-render.md |
Adds a changeset entry documenting the SSR fix and fallback behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| warnings.warn( | ||
| "Failed to start Node front proxy for SSR; serving " | ||
| "without SSR on " | ||
| f"{self.protocol}://{self.server_name}:{self.server_port}. " | ||
| "See the Node server output above." | ||
| ) |
| "gradio": patch | ||
| --- | ||
|
|
||
| fix:Fix SSR mode failing on every render (`Cannot find module 'postcss'`) in installed builds, and serve without SSR on the expected port when the Node server can't start |
There was a problem hiding this comment.
Leaving this as-is: no space after fix: is this repo's existing convention — most changesets on main are written that way (fix:Preserve original filenames..., fix:Reset audio playback position..., feat:workflow: add model endpoint integration), and .changeset/changeset.cjs strips the prefix when generating the changelog, so the space doesn't affect the output.
Applied the other suggestion — the warning now uses self.local_url so it can't print 0.0.0.0.
The build check matched any one-argument call with a string literal as a
require, so `headers.get("cookie")` read as a dependency on the `cookie`
package. It only passed locally because pnpm's layout can't resolve `cookie`
from the build output, while CI's can — the check failed every frontend build.
Resolve `require` through its binding instead: follow the name imported as
`createRequire` to the name its result is bound to, which is what the minified
CJS interop calls. With no false positives left, drop the "resolves in the
repo" filter and treat any unshipped runtime dependency as a failure.
Also close the internal server before binding the user-facing port. The two
share an app, so overlapping them ran its lifespan twice and let the first
server's shutdown delete the app's cache files from under the second. If the
port is lost after releasing ours, go back to the internal one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Per Copilot review: start_server already normalizes 0.0.0.0 to localhost when it builds local_url, so the warning should use that rather than reassembling the address from server_name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#13673 landed the same `ssr.noExternal` change and its own js/app/verify_build.js, so take main's version of both and of the build script. What's left here is the Python side that PR didn't cover: surfacing Node's stderr instead of blaming the Node install, and serving without SSR on the user-facing port instead of stranding the app on an internal one. Also drop the hand-written changeset — per AGENTS.md #7 (merged in #13682) the bot generates it from the PR title.
|
Thanks @dawoodkhan82 for the review! |
Description
Follow-up to #13673, which fixed the
postcssbundling itself. This is the Python half of the same incident — the two reasons a broken SSR bundle was so hard to diagnose and took the whole Space down with it.1. The failure was reported as the wrong problem. Node's output went to
DEVNULLunlessdebug=True, and when startup verification timed out Gradio printedPlease install Node 20 or higher— Node was fine; it wasCannot find module 'postcss'on every render. Node's stderr is now captured through a drained pipe (bounded to the last 50 lines, so nothing grows unbounded and a full pipe can't block Node) and printed on failure. The Node-install hint still prints when Node produced no output.2. The app was left on a port nothing routes to. In proxy mode Python deliberately binds an internal port and lets Node own the user-facing one, so when Node never came up the app answered only on
:7861. A Space is judged by the user-facing port, so it was reported as crashed while Python was healthy.Blocks._serve_without_node_proxynow moves Python onto the user-facing port and serves client-side rendered instead.The port move bind-tests first, then closes the internal server before binding — the two can't overlap, because they share a FastAPI app and overlapping them would run its lifespan twice and let the first server's shutdown delete the app's cache files from under the second. If the port is lost after ours is released, it goes back to the internal one. The startup line no longer claims SSR when it degraded.
Closes: #13672 (the diagnostics/fallback half; #13673 fixed the bundling)
AI Disclosure
Testing and Formatting Your Code
Five new tests in
test/test_node_proxy.py: Node's stderr is surfaced and the version hint suppressed when Node explains itself; the hint still appears when it doesn't; the app moves to the user-facing port when Node fails; and it keeps serving where it is when that port is held by something else.Verified end to end against a deliberately broken bundle (a
require("postcss")injected into a server chunk, with the build output copied outside the repo so it can't resolve, as in a wheel):Python ends up on 7860 serving 200, the internal port is released, and no orphan Node process is left behind.
Note on scope
This PR originally also carried the
ssr.noExternalfix and a build check; #13673 landed an equivalent fix, so those are dropped and this is now Python-only.One thing worth knowing about the merged check: it renders only
/against an unreachable backend, so it only loads the chunks that route imports. Injecting the same missing-module fault into a lazily-loaded component chunk (Index14) passes it. It caughtpostcssbecausesanitizesits on the/path; a future externalized dependency in one of the other ~200 server chunks would not be caught. Happy to open a separate PR adding a static scan across all chunks if that's wanted — deliberately not bundled here to keep this small.🤖 Generated with Claude Code