From 9f3c44d00cbd544e323f9a90bcaf43a54adbce15 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 29 Aug 2025 11:58:47 +0900 Subject: [PATCH 1/5] fix(rsc): use `req.originalUrl` for server handler --- packages/plugin-rsc/src/plugin.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 6366d940..84fed1d6 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -471,6 +471,10 @@ export default function vitePluginRsc( `[vite-rsc] failed to resolve server handler '${source}'`, ) const mod = await environment.runner.import(resolved.id) + // prserve original request url for SSR framework. + // for example, Vite automatically strips `base` from url. + // https://github.com/vitejs/vite/blob/84079a84ad94de4c1ef4f1bdb2ab448ff2c01196/packages/vite/src/node/server/middlewares/base.ts#L18-L20 + req.url = req.originalUrl // ensure catching rejected promise // https://github.com/mjackson/remix-the-web/blob/b5aa2ae24558f5d926af576482caf6e9b35461dc/packages/node-fetch-server/src/lib/request-listener.ts#L87 await createRequestListener(mod.default)(req, res) @@ -506,6 +510,7 @@ export default function vitePluginRsc( return () => { server.middlewares.use(async (req, res, next) => { try { + req.url = req.originalUrl await handler(req, res) } catch (e) { next(e) From d2bc90dacb89ea8fe6eda0e4d2ee196f752c4394 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 29 Aug 2025 12:27:00 +0900 Subject: [PATCH 2/5] chore: cleanup --- packages/plugin-rsc/src/plugin.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 84fed1d6..fae9ca90 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -471,10 +471,10 @@ export default function vitePluginRsc( `[vite-rsc] failed to resolve server handler '${source}'`, ) const mod = await environment.runner.import(resolved.id) - // prserve original request url for SSR framework. + // expose original request url to server handler. // for example, Vite automatically strips `base` from url. // https://github.com/vitejs/vite/blob/84079a84ad94de4c1ef4f1bdb2ab448ff2c01196/packages/vite/src/node/server/middlewares/base.ts#L18-L20 - req.url = req.originalUrl + req.url = req.originalUrl ?? req.url // ensure catching rejected promise // https://github.com/mjackson/remix-the-web/blob/b5aa2ae24558f5d926af576482caf6e9b35461dc/packages/node-fetch-server/src/lib/request-listener.ts#L87 await createRequestListener(mod.default)(req, res) @@ -510,7 +510,7 @@ export default function vitePluginRsc( return () => { server.middlewares.use(async (req, res, next) => { try { - req.url = req.originalUrl + req.url = req.originalUrl ?? req.url await handler(req, res) } catch (e) { next(e) From 89bd57418b2f57a349db9d07c8796f10c89640d7 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 29 Aug 2025 12:27:48 +0900 Subject: [PATCH 3/5] chore: comment --- packages/plugin-rsc/src/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index fae9ca90..d00db1ad 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -472,7 +472,7 @@ export default function vitePluginRsc( ) const mod = await environment.runner.import(resolved.id) // expose original request url to server handler. - // for example, Vite automatically strips `base` from url. + // for example, this restores `base` which is automatically stripped by Vite. // https://github.com/vitejs/vite/blob/84079a84ad94de4c1ef4f1bdb2ab448ff2c01196/packages/vite/src/node/server/middlewares/base.ts#L18-L20 req.url = req.originalUrl ?? req.url // ensure catching rejected promise From 0ea1502f9c61242d2a110ebb667c3f25b7452ead Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 29 Aug 2025 12:39:42 +0900 Subject: [PATCH 4/5] chore: update example --- .../examples/starter/src/framework/entry.rsc.tsx | 8 ++++++-- packages/plugin-rsc/examples/starter/src/root.tsx | 15 ++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/plugin-rsc/examples/starter/src/framework/entry.rsc.tsx b/packages/plugin-rsc/examples/starter/src/framework/entry.rsc.tsx index 5f346b56..fa1c2784 100644 --- a/packages/plugin-rsc/examples/starter/src/framework/entry.rsc.tsx +++ b/packages/plugin-rsc/examples/starter/src/framework/entry.rsc.tsx @@ -58,14 +58,18 @@ export default async function handler(request: Request): Promise { // we render RSC stream after handling server function request // so that new render reflects updated state from server function call // to achieve single round trip to mutate and fetch from server. - const rscPayload: RscPayload = { root: , formState, returnValue } + const url = new URL(request.url) + const rscPayload: RscPayload = { + root: , + formState, + returnValue, + } const rscOptions = { temporaryReferences } const rscStream = renderToReadableStream(rscPayload, rscOptions) // respond RSC stream without HTML rendering based on framework's convention. // here we use request header `content-type`. // additionally we allow `?__rsc` and `?__html` to easily view payload directly. - const url = new URL(request.url) const isRscRequest = (!request.headers.get('accept')?.includes('text/html') && !url.searchParams.has('__html')) || diff --git a/packages/plugin-rsc/examples/starter/src/root.tsx b/packages/plugin-rsc/examples/starter/src/root.tsx index a42c6a92..c6a64970 100644 --- a/packages/plugin-rsc/examples/starter/src/root.tsx +++ b/packages/plugin-rsc/examples/starter/src/root.tsx @@ -4,7 +4,7 @@ import { getServerCounter, updateServerCounter } from './action.tsx' import reactLogo from './assets/react.svg' import { ClientCounter } from './client.tsx' -export function Root() { +export function Root(props: { url: URL }) { return ( @@ -14,13 +14,13 @@ export function Root() { Vite + RSC - + ) } -function App() { +function App(props: { url: URL }) { return (
@@ -43,6 +43,7 @@ function App() {
+
Request URL: {props.url?.href}
  • Edit src/client.tsx to test client HMR. @@ -52,15 +53,15 @@ function App() {
  • Visit{' '} - - /?__rsc + + ?__rsc {' '} to view RSC stream payload.
  • Visit{' '} - - /?__nojs + + ?__nojs {' '} to test server action without js enabled.
  • From 080c846e0af6d2e60931b9b2e27d78ff09d82b85 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 29 Aug 2025 12:51:04 +0900 Subject: [PATCH 5/5] test: e2e --- packages/plugin-rsc/e2e/base.test.ts | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/plugin-rsc/e2e/base.test.ts b/packages/plugin-rsc/e2e/base.test.ts index 6ab6cc3d..1f651b9d 100644 --- a/packages/plugin-rsc/e2e/base.test.ts +++ b/packages/plugin-rsc/e2e/base.test.ts @@ -1,5 +1,5 @@ -import { test } from '@playwright/test' -import { setupInlineFixture, useFixture } from './fixture' +import { expect, test } from '@playwright/test' +import { setupInlineFixture, useFixture, type Fixture } from './fixture' import { defineStarterTest } from './starter' test.describe(() => { @@ -27,17 +27,31 @@ test.describe(() => { test.describe('dev-base', () => { const f = useFixture({ root, mode: 'dev' }) - defineStarterTest({ + const f2: Fixture = { ...f, url: (url) => new URL(url ?? './', f.url('./custom-base/')).href, - }) + } + defineStarterTest(f2) + testRequestUrl(f2) }) test.describe('build-base', () => { const f = useFixture({ root, mode: 'build' }) - defineStarterTest({ + const f2: Fixture = { ...f, url: (url) => new URL(url ?? './', f.url('./custom-base/')).href, - }) + } + defineStarterTest(f2) + testRequestUrl(f2) }) + + function testRequestUrl(f: Fixture) { + test('request url', async ({ page }) => { + await page.goto(f.url()) + await page.waitForSelector('#root') + await expect(page.locator('.card').nth(2)).toHaveText( + `Request URL: ${f.url()}`, + ) + }) + } })