Skip to content

Instantly share code, notes, and snippets.

@xfournet
Last active June 27, 2025 21:38
Show Gist options
  • Save xfournet/068592b3d1ddd488427b874b23f707bf to your computer and use it in GitHub Desktop.
Save xfournet/068592b3d1ddd488427b874b23f707bf to your computer and use it in GitHub Desktop.
Vite support for HTTP2 and proxy
import proxy from 'http2-proxy';
import type { Plugin, ProxyOptions } from 'vite';
export const pluginHttp2Proxy = (): Plugin => {
let routes: Record<string, string | ProxyOptions>;
return {
name: 'vite-plugin-http2-proxy',
config: (config) => {
const { server } = config;
routes = server?.proxy ?? {};
if (server) {
server.proxy = undefined;
}
return config;
},
configureServer: ({ config: { logger }, middlewares }) => {
Object.entries(routes).forEach(([route, target]) => {
if (typeof target !== 'string') {
throw new Error('ProxyOptions target are not supported yet, only string target are currently supported');
}
const { protocol, hostname, port } = new URL(target);
const options = {
protocol: protocol as 'http' | 'https',
hostname,
port: Number(port),
proxyTimeout: 60000,
};
middlewares.use(route, (req, res) => {
proxy.web(req, res, { ...options, path: req.originalUrl }, (err) => {
if (err) {
logger.error(`[http2-proxy] Error when proxying request on '${req.originalUrl}'`, { timestamp: true, error: err });
}
});
});
});
},
};
};
@NullVoxPopuli
Copy link

NullVoxPopuli commented Jun 27, 2025

@c1aphas I'm seeing a lack of 304 status on unchanged files (the thousands of them I have ๐Ÿ™ˆ )

is there a secret to retaining browser-cache?

Edit: my network tab's "Disable browser cache" checkbox was checked again ๐Ÿ™ƒ

But now I get a bunch of net::ERR_HTTP2_PROTOCOL_ERROR

My code (includes mkcert)
import { $ } from 'execa';
import http2Proxy from 'http2-proxy';
import { mkdir, readFile, stat } from 'node:fs/promises';
import path from 'node:path';

const defaultCacheDir = 'node_modules/.vite';

const domains = ['localhost'];

async function createCert(keyPath, certPath) {
	const dir = path.dirname(keyPath);
	const keyFile = path.basename(keyPath);
	const certFile = path.basename(certPath);

	await mkdir(dir, { recursive: true });
	await $({ cwd: dir })`mkcert --key-file ${keyFile} --cert-file ${certFile} ${domains.join(' ')}`;
}

/**
 * Original implementation: https://github.com/vitejs/vite-plugin-basic-ssl/blob/main/src/index.ts
 *
 * Also: https://gist.github.com/xfournet/068592b3d1ddd488427b874b23f707bf
 *    and: https://gist.github.com/xfournet/068592b3d1ddd488427b874b23f707bf?permalink_comment_id=5050309#gistcomment-5050309
 *
 * Also: https://github.com/vitejs/vite/issues/2725
 */
export function ssl() {
	let proxy;

	return {
		name: 'vite-ssl-http2-proxy',
		async configResolved(config) {
			if (config.mode === 'test') {
				console.warn(
					`WARNING: not using SSL for tests. This is due to how CI is currently missing mkcert. But also we fully build our tests for running in the CLI, so SSL isn't needed anyway.`,
				);
				return;
			}

			proxy = config.server?.proxy;

			/**
			 * We don't want vite to handle the proxy, else it disables http2
			 */
			if (config.server) {
				config.server.proxy = undefined;
			}

			const certPair = await getCertPair((config.cacheDir ?? defaultCacheDir) + '/auditboard-vite-ssl');

			const https = () => ({
				cert: certPair.cert,
				key: certPair.key,
				maxSessionMemory: 1000,
				peerMaxConcurrentStreams: 1000,
			});

			if (config.server.https === undefined || !!config.server.https) {
				config.server.https = Object.assign({}, config.server.https, https());
			}
			if (config.preview.https === undefined || !!config.preview.https) {
				config.preview.https = Object.assign({}, config.preview.https, https());
			}
		},
		configureServer({ config: { logger }, middlewares }) {
			if (!proxy) return;

			Object.entries(proxy).forEach(([route, proxyOptions]) => {
				const options = getOptions(proxyOptions);

				middlewares.use(route, (req, res) => {
					const http2Options = {
						...options,
					};

					if (typeof proxyOptions !== 'string') {
						if (proxyOptions.rewrite) {
							http2Options.path = proxyOptions.rewrite(req.originalUrl);
						} else {
							http2Options.path = req.originalUrl;
						}

						if (proxyOptions.configure) {
							proxyOptions.configure(http2Options, proxyOptions);
						}
					}

					http2Proxy.web(req, res, http2Options, (err) => {
						if (err) {
							logger.error(`[http2-proxy] Error when proxying request on '${req.originalUrl}'`, {
								timestamp: true,
								error: err,
							});
							logger.error(err);
						}
					});
				});
			});
		},
	};
}

const SECOND = 1000;
const HOUR = SECOND * 60 * 60;
const MONTH = HOUR * 24 * 30;

/**
 * @returns {Promise<{ cert: string, key: string }>}
 */
export async function getCertPair(cacheDir) {
	const keyPath = path.join(cacheDir, 'key.pem');
	const certPath = path.join(cacheDir, 'cert.pem');

	async function readFiles() {
		const [key, cert] = await Promise.all([readFile(keyPath), readFile(certPath)]);
		return { key, cert };
	}

	try {
		const [stats, content] = await Promise.all([stat(keyPath), readFiles()]);

		if (Date.now() - stats.ctime.valueOf() > MONTH) {
			throw new Error('cache is outdated.');
		}

		return content;
	} catch {
		await createCert(keyPath, certPath);

		return readFiles();
	}
}

function getOptions(proxyOptions) {
	if (typeof proxyOptions !== 'string') {
		const { protocol, hostname, port } = new URL(proxyOptions.target);
		const { proxyTimeout } = proxyOptions;

		return {
			protocol,
			hostname,
			port: Number(port),
			proxyTimeout,
			rejectUnauthorized: false,
		};
	}

	const { protocol, hostname, port } = new URL(proxyOptions);

	return {
		protocol,
		hostname,
		port: Number(port),
		proxyTimeout: 60000,
		rejectUnauthorized: false,
	};
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment