fix: forward Inertia headers through preview proxy
Preserves Inertia navigation inside preview panel Forwards request and response Inertia headers Adds preview proxy header passthrough tests
This commit is contained in:
@@ -3,6 +3,8 @@ const TOKEN_COOKIE_NAME = 'oc_preview_token';
|
|||||||
const TOKEN_QUERY_PARAM = 'oc_preview_token';
|
const TOKEN_QUERY_PARAM = 'oc_preview_token';
|
||||||
const CLIENT_TOKEN_QUERY_PARAM = 'oc_client_token';
|
const CLIENT_TOKEN_QUERY_PARAM = 'oc_client_token';
|
||||||
const URL_AUTH_TOKEN_QUERY_PARAM = 'oc_url_token';
|
const URL_AUTH_TOKEN_QUERY_PARAM = 'oc_url_token';
|
||||||
|
const PREVIEW_PASSTHROUGH_REQUEST_HEADERS = ['x-inertia', 'x-inertia-version'];
|
||||||
|
const PREVIEW_PASSTHROUGH_RESPONSE_HEADERS = ['x-inertia', 'x-inertia-location'];
|
||||||
|
|
||||||
const LOOPBACK_HOSTS = new Set([
|
const LOOPBACK_HOSTS = new Set([
|
||||||
'localhost',
|
'localhost',
|
||||||
@@ -25,6 +27,34 @@ const parsePreviewResourcePath = (url) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const readHeader = (headers, name) => {
|
||||||
|
if (!headers || typeof headers !== 'object') return undefined;
|
||||||
|
const direct = headers[name];
|
||||||
|
if (direct !== undefined) return direct;
|
||||||
|
const lowerName = name.toLowerCase();
|
||||||
|
const key = Object.keys(headers).find((entry) => entry.toLowerCase() === lowerName);
|
||||||
|
return key ? headers[key] : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const applyPreviewPassthroughRequestHeaders = (req, proxyReq) => {
|
||||||
|
for (const headerName of PREVIEW_PASSTHROUGH_REQUEST_HEADERS) {
|
||||||
|
const value = readHeader(req?.headers, headerName);
|
||||||
|
if (value !== undefined) {
|
||||||
|
proxyReq.setHeader(headerName, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const applyPreviewPassthroughResponseHeaders = (proxyRes, res) => {
|
||||||
|
if (!res || res.headersSent || typeof res.setHeader !== 'function') return;
|
||||||
|
for (const headerName of PREVIEW_PASSTHROUGH_RESPONSE_HEADERS) {
|
||||||
|
const value = readHeader(proxyRes?.headers, headerName);
|
||||||
|
if (value !== undefined) {
|
||||||
|
res.setHeader(headerName, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const previewResourceNoiseRuleSets = [
|
const previewResourceNoiseRuleSets = [
|
||||||
{
|
{
|
||||||
name: 'vite',
|
name: 'vite',
|
||||||
@@ -1412,14 +1442,16 @@ export const createPreviewProxyRuntime = ({
|
|||||||
return `${strippedPath}${withoutUrlAuthToken}`;
|
return `${strippedPath}${withoutUrlAuthToken}`;
|
||||||
},
|
},
|
||||||
on: {
|
on: {
|
||||||
proxyReq: (proxyReq) => {
|
proxyReq: (proxyReq, req) => {
|
||||||
|
applyPreviewPassthroughRequestHeaders(req, proxyReq);
|
||||||
// Keep local dev servers from receiving OpenChamber credentials.
|
// Keep local dev servers from receiving OpenChamber credentials.
|
||||||
proxyReq.removeHeader('cookie');
|
proxyReq.removeHeader('cookie');
|
||||||
proxyReq.removeHeader('authorization');
|
proxyReq.removeHeader('authorization');
|
||||||
proxyReq.removeHeader('x-openchamber-ui-session');
|
proxyReq.removeHeader('x-openchamber-ui-session');
|
||||||
proxyReq.setHeader('accept-encoding', 'identity');
|
proxyReq.setHeader('accept-encoding', 'identity');
|
||||||
},
|
},
|
||||||
proxyRes: responseInterceptor(async (responseBuffer, proxyRes, req) => {
|
proxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => {
|
||||||
|
applyPreviewPassthroughResponseHeaders(proxyRes, res);
|
||||||
// Per-response nonce lets the injected bridge run under the dev
|
// Per-response nonce lets the injected bridge run under the dev
|
||||||
// server's CSP without dropping its script restrictions wholesale.
|
// server's CSP without dropping its script restrictions wholesale.
|
||||||
const bridgeNonce = crypto.randomBytes(16).toString('base64');
|
const bridgeNonce = crypto.randomBytes(16).toString('base64');
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
applyPreviewPassthroughRequestHeaders,
|
||||||
|
applyPreviewPassthroughResponseHeaders,
|
||||||
classifyPreviewNavigation,
|
classifyPreviewNavigation,
|
||||||
classifyPreviewResourceError,
|
classifyPreviewResourceError,
|
||||||
normalizeProxyTargetUrl,
|
normalizeProxyTargetUrl,
|
||||||
@@ -16,6 +18,47 @@ const rewrite = (bodyText, kind) => rewritePreviewBody({
|
|||||||
targetOrigin: 'http://127.0.0.1:3000',
|
targetOrigin: 'http://127.0.0.1:3000',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('preview Inertia header passthrough', () => {
|
||||||
|
it('forwards Inertia request headers to the preview target', () => {
|
||||||
|
const forwarded = new Map();
|
||||||
|
const proxyReq = {
|
||||||
|
setHeader: (name, value) => forwarded.set(name, value),
|
||||||
|
};
|
||||||
|
|
||||||
|
applyPreviewPassthroughRequestHeaders({
|
||||||
|
headers: {
|
||||||
|
'x-inertia': 'true',
|
||||||
|
'x-inertia-version': 'asset-hash',
|
||||||
|
'x-unrelated': 'ignored',
|
||||||
|
},
|
||||||
|
}, proxyReq);
|
||||||
|
|
||||||
|
expect(forwarded.get('x-inertia')).toBe('true');
|
||||||
|
expect(forwarded.get('x-inertia-version')).toBe('asset-hash');
|
||||||
|
expect(forwarded.has('x-unrelated')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('forwards Inertia response headers back to the preview client', () => {
|
||||||
|
const forwarded = new Map();
|
||||||
|
const res = {
|
||||||
|
headersSent: false,
|
||||||
|
setHeader: (name, value) => forwarded.set(name, value),
|
||||||
|
};
|
||||||
|
|
||||||
|
applyPreviewPassthroughResponseHeaders({
|
||||||
|
headers: {
|
||||||
|
'x-inertia': 'true',
|
||||||
|
'x-inertia-location': 'http://127.0.0.1:8000/login',
|
||||||
|
'x-unrelated': 'ignored',
|
||||||
|
},
|
||||||
|
}, res);
|
||||||
|
|
||||||
|
expect(forwarded.get('x-inertia')).toBe('true');
|
||||||
|
expect(forwarded.get('x-inertia-location')).toBe('http://127.0.0.1:8000/login');
|
||||||
|
expect(forwarded.has('x-unrelated')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('preview resource error classification', () => {
|
describe('preview resource error classification', () => {
|
||||||
it('suppresses Astro/Vite stylesheet modules reported as failed scripts', () => {
|
it('suppresses Astro/Vite stylesheet modules reported as failed scripts', () => {
|
||||||
expect(classifyPreviewResourceError({
|
expect(classifyPreviewResourceError({
|
||||||
|
|||||||
Reference in New Issue
Block a user