Preview proxy tuneup

This commit is contained in:
2026-02-22 16:00:52 +00:00
parent ca45301e3e
commit a8aba2aee2
2 changed files with 60 additions and 18 deletions
+42 -18
View File
@@ -192,29 +192,24 @@ devServerRouter.get('/logs', async (ctx) => {
// Proxy router — mounted outside protected router (no auth needed for sub-resources). // Proxy router — mounted outside protected router (no auth needed for sub-resources).
// Security: only proxies to ports that were started via authenticated /start calls. // Security: only proxies to ports that were started via authenticated /start calls.
devServerProxyRouter.all('/:slug/*', async (ctx) => {
const slug = ctx.req.param('slug');
if (!slug) return ctx.text('Not found', 404);
const entry = findEntryBySlug(slug); type ProxyParams = { entry: ServerEntry; slug: string; proxyPath: string; search: string; method: string; rawReq: Request };
if (!entry) return ctx.text('No dev server running', 404);
async function proxyRequest({ entry, slug, proxyPath, search, method, rawReq }: ProxyParams): Promise<Response> {
const prefix = `/api/dev-server-proxy/${slug}`;
const targetUrl = `http://localhost:${entry.port}${proxyPath}${search}`;
touchEntry(entry); touchEntry(entry);
const originalUrl = new URL(ctx.req.url);
const prefix = `/api/dev-server-proxy/${slug}`;
const proxyPath = originalUrl.pathname.replace(prefix, '') || '/';
const targetUrl = `http://localhost:${entry.port}${proxyPath}${originalUrl.search}`;
try { try {
const headers = new Headers(ctx.req.raw.headers); const headers = new Headers(rawReq.headers);
headers.delete('host'); headers.delete('host');
headers.delete('authorization'); headers.delete('authorization');
const res = await fetch(targetUrl, { const res = await fetch(targetUrl, {
method: ctx.req.method, method,
headers, headers,
body: ctx.req.method !== 'GET' && ctx.req.method !== 'HEAD' ? ctx.req.raw.body : undefined, body: method !== 'GET' && method !== 'HEAD' ? rawReq.body : undefined,
redirect: 'manual', redirect: 'manual',
}); });
@@ -235,13 +230,42 @@ devServerProxyRouter.all('/:slug/*', async (ctx) => {
return new Response(rewritten, { status: res.status, headers: newHeaders }); return new Response(rewritten, { status: res.status, headers: newHeaders });
} }
return new Response(res.body, { return new Response(res.body, { status: res.status, headers: res.headers });
status: res.status,
headers: res.headers,
});
} catch { } catch {
return ctx.text('Dev server not reachable', 502); return new Response('Dev server not reachable', { status: 502 });
} }
}
// Proxy handler: tries slug from URL first, falls back to Referer for misrouted
// chunk requests caused by relative imports resolving at wrong path depth.
devServerProxyRouter.all('/:slug/*', async (ctx) => {
const slug = ctx.req.param('slug');
if (!slug) return ctx.text('Not found', 404);
const originalUrl = new URL(ctx.req.url);
const entry = findEntryBySlug(slug);
if (entry) {
const prefix = `/api/dev-server-proxy/${slug}`;
const proxyPath = originalUrl.pathname.replace(prefix, '') || '/';
return proxyRequest({ entry, slug, proxyPath, search: originalUrl.search, method: ctx.req.method, rawReq: ctx.req.raw });
}
// Slug didn't match a dev server — check Referer for the real slug
const referer = ctx.req.header('referer');
if (referer) {
const match = referer.match(/\/api\/dev-server-proxy\/([^/]+)/);
if (match) {
const realSlug = match[1]!;
const realEntry = findEntryBySlug(realSlug);
if (realEntry) {
const proxyPath = originalUrl.pathname.replace('/api/dev-server-proxy', '') || '/';
return proxyRequest({ entry: realEntry, slug: realSlug, proxyPath, search: originalUrl.search, method: ctx.req.method, rawReq: ctx.req.raw });
}
}
}
return ctx.text('No dev server running', 404);
}); });
// Injected into proxied HTML to intercept fetch/XHR so absolute paths (e.g. /api/hello) // Injected into proxied HTML to intercept fetch/XHR so absolute paths (e.g. /api/hello)
@@ -87,6 +87,24 @@ export const PreviewProvider = ({ children }: PreviewProviderProps) => {
return () => { cancelled = true; }; return () => { cancelled = true; };
}, [slug]); }, [slug]);
// Poll status while a server is supposedly running — auto-restart if it died (e.g. idle timeout)
useEffect(() => {
if (!slug || !url) return;
const interval = setInterval(async () => {
try {
const status = await client.get<DevServerStatus>(`/dev-server/status?slug=${encodeURIComponent(slug)}`);
if (!status.running) {
startServer(slug);
}
} catch {
// ignore transient failures
}
}, 30_000);
return () => clearInterval(interval);
}, [slug, url]);
return ( return (
<PreviewContext <PreviewContext
value={{ value={{