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 CLIENT_TOKEN_QUERY_PARAM = 'oc_client_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([
|
||||
'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 = [
|
||||
{
|
||||
name: 'vite',
|
||||
@@ -1412,14 +1442,16 @@ export const createPreviewProxyRuntime = ({
|
||||
return `${strippedPath}${withoutUrlAuthToken}`;
|
||||
},
|
||||
on: {
|
||||
proxyReq: (proxyReq) => {
|
||||
proxyReq: (proxyReq, req) => {
|
||||
applyPreviewPassthroughRequestHeaders(req, proxyReq);
|
||||
// Keep local dev servers from receiving OpenChamber credentials.
|
||||
proxyReq.removeHeader('cookie');
|
||||
proxyReq.removeHeader('authorization');
|
||||
proxyReq.removeHeader('x-openchamber-ui-session');
|
||||
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
|
||||
// server's CSP without dropping its script restrictions wholesale.
|
||||
const bridgeNonce = crypto.randomBytes(16).toString('base64');
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
applyPreviewPassthroughRequestHeaders,
|
||||
applyPreviewPassthroughResponseHeaders,
|
||||
classifyPreviewNavigation,
|
||||
classifyPreviewResourceError,
|
||||
normalizeProxyTargetUrl,
|
||||
@@ -16,6 +18,47 @@ const rewrite = (bodyText, kind) => rewritePreviewBody({
|
||||
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', () => {
|
||||
it('suppresses Astro/Vite stylesheet modules reported as failed scripts', () => {
|
||||
expect(classifyPreviewResourceError({
|
||||
|
||||
Reference in New Issue
Block a user