Skip to content

Report why the Node SSR server failed, and serve without SSR on the expected port - #13681

Merged
abidlabs merged 8 commits into
mainfrom
fix-ssr-postcss-missing
Jul 29, 2026
Merged

Report why the Node SSR server failed, and serve without SSR on the expected port#13681
abidlabs merged 8 commits into
mainfrom
fix-ssr-postcss-missing

Conversation

@abidlabs

@abidlabs abidlabs commented Jul 28, 2026

Copy link
Copy Markdown
Member

Description

Follow-up to #13673, which fixed the postcss bundling 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 DEVNULL unless debug=True, and when startup verification timed out Gradio printed Please install Node 20 or higher — Node was fine; it was Cannot 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_proxy now 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

  • I used AI to... investigate the regression, write the fix and tests, and draft this description. I self-reviewed every changed line.
  • I did not use AI

Testing and Formatting Your Code

python -m pytest test/test_node_proxy.py -q     # 17 passed
./scripts/type_check_backend.sh                 # passes
bash scripts/format_backend.sh

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):

Cannot start Node server on any port in the range 7860-7860.
The Node server reported:
[500] HEAD /
Error: Cannot find module 'postcss'
UserWarning: Failed to start Node front proxy for SSR; serving without SSR on http://127.0.0.1:7860/ (see the Node server output above).
* Running on local URL:  http://127.0.0.1:7860

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.noExternal fix 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 caught postcss because sanitize sits 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

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>
@gradio-pr-bot

gradio-pr-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

🪼 branch checks and previews

Name Status URL
Spaces ready! Spaces preview
Website ready! Website preview
Storybook ready! Storybook preview
🦄 Changes detected! Details

Install Gradio from this PR

pip install https://huggingface.co/buckets/gradio/pypi-previews/resolve/4ea89dc803bd5e280a7c05d1d689da1227243a69/gradio-6.20.0-py3-none-any.whl

Install 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";

@gradio-pr-bot

gradio-pr-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

🦄 change detected

This Pull Request includes changes to the following packages.

Package Version
gradio minor

  • Report why the Node SSR server failed, and serve without SSR on the expected port

Something isn't right?

  • Maintainers can change the version label to modify the version bump.
  • If the bot has failed to detect any changes, or if this pull request needs to update multiple packages to different versions or requires a more comprehensive changelog entry, maintainers can update the changelog file directly.

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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 postcss into the SSR build output (via ssr.noExternal) to prevent Cannot 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’s node_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.

Comment thread gradio/blocks.py
Comment on lines +3084 to +3089
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."
)
Comment thread .changeset/loud-nodes-render.md Outdated
"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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

abidlabs and others added 3 commits July 28, 2026 12:57
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.
@abidlabs abidlabs changed the title Bundle postcss into the SSR build so ssr_mode works in installed builds Report why the Node SSR server failed, and serve without SSR on the expected port Jul 29, 2026
@abidlabs
abidlabs requested a review from Copilot July 29, 2026 02:18
@abidlabs
abidlabs marked this pull request as ready for review July 29, 2026 02:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@abidlabs
abidlabs requested a review from dawoodkhan82 July 29, 2026 02:18

@dawoodkhan82 dawoodkhan82 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lgtm!

@abidlabs

Copy link
Copy Markdown
Member Author

Thanks @dawoodkhan82 for the review!

@abidlabs
abidlabs merged commit eed6ebd into main Jul 29, 2026
26 checks passed
@abidlabs
abidlabs deleted the fix-ssr-postcss-missing branch July 29, 2026 05:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SSR wheel renders fail when postcss is not bundled

4 participants