Preview proxy tuneup
This commit is contained in:
@@ -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={{
|
||||||
|
|||||||
Reference in New Issue
Block a user