Skip to content

Commit 33dbed6

Browse files
committed
fix: move heavy state data to URL fragment to avoid HTTP 414
Airfoil geometry, spacing knots, layout JSON, and filter models are now serialized into the URL fragment (#h=...) instead of query params. The fragment is never sent to the server, preventing "URI Too Long" errors on page refresh or when opening shared links. Old URLs with these params in the query string are still parsed correctly. Made-with: Cursor
1 parent a06d8ad commit 33dbed6

1 file changed

Lines changed: 30 additions & 11 deletions

File tree

flexfoil-ui/src/lib/routeState.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ import { loadFromUrl, parseNacaFromName } from './urlState';
2222

2323
export type RouteTheme = 'dark' | 'light';
2424

25+
interface HeavyRouteData {
26+
foil?: SerializedAirfoilData;
27+
spacing?: SpacingKnot[];
28+
layout?: IJsonModel;
29+
filters?: unknown;
30+
}
31+
2532
interface SerializedAirfoilData {
2633
name: string;
2734
coordinates: AirfoilPoint[];
@@ -251,8 +258,9 @@ export function serializeRouteState(snapshot: CanonicalRouteStateSnapshot, baseP
251258
if (airfoil.spacingPanelMode) params.set('spacingMode', airfoil.spacingPanelMode);
252259
if (airfoil.sspInterpolation) params.set('sspInterp', airfoil.sspInterpolation);
253260
if (airfoil.sspVisualization) params.set('sspViz', airfoil.sspVisualization);
254-
if (airfoil.spacingKnots?.length) params.set('spacing', encodeCompressed(airfoil.spacingKnots));
255-
if (airfoil.exactGeometry) params.set('foil', encodeCompressed(airfoil.exactGeometry));
261+
const heavy: HeavyRouteData = {};
262+
if (airfoil.spacingKnots?.length) heavy.spacing = airfoil.spacingKnots;
263+
if (airfoil.exactGeometry) heavy.foil = airfoil.exactGeometry;
256264

257265
setBooleanParam(params, 'grid', visualization.showGrid ?? DEFAULT_VISUALIZATION_STATE.showGrid, DEFAULT_VISUALIZATION_STATE.showGrid);
258266
setBooleanParam(params, 'curve', visualization.showCurve ?? DEFAULT_VISUALIZATION_STATE.showCurve, DEFAULT_VISUALIZATION_STATE.showCurve);
@@ -322,17 +330,21 @@ export function serializeRouteState(snapshot: CanonicalRouteStateSnapshot, baseP
322330
params.set('splom', (ui.dataExplorerSplomKeys ?? DEFAULT_ROUTE_UI_STATE.dataExplorerSplomKeys).join(','));
323331
}
324332
if (ui.dataExplorerColorBy) params.set('colorBy', String(ui.dataExplorerColorBy));
325-
if (ui.dataExplorerFilterModel) params.set('filters', encodeCompressed(ui.dataExplorerFilterModel));
333+
if (ui.dataExplorerFilterModel) heavy.filters = ui.dataExplorerFilterModel;
326334
setNumberParam(params, 'cx', ui.viewport?.centerX ?? DEFAULT_ROUTE_UI_STATE.viewport.centerX, DEFAULT_ROUTE_UI_STATE.viewport.centerX);
327335
setNumberParam(params, 'cy', ui.viewport?.centerY ?? DEFAULT_ROUTE_UI_STATE.viewport.centerY, DEFAULT_ROUTE_UI_STATE.viewport.centerY);
328336
setNumberParam(params, 'zoom', ui.viewport?.zoom ?? DEFAULT_ROUTE_UI_STATE.viewport.zoom, DEFAULT_ROUTE_UI_STATE.viewport.zoom);
329337
if (!isDefaultLayout(ui.layoutJson ?? null)) {
330-
params.set('layout', encodeCompressed(ui.layoutJson ?? defaultLayoutJson));
338+
heavy.layout = ui.layoutJson ?? defaultLayoutJson;
331339
}
332340

333341
const pathname = buildPathname(basePath, snapshot.panel);
334342
const search = params.toString();
335-
return search ? `${pathname}?${search}` : pathname;
343+
const hasHeavy = Object.keys(heavy).length > 0;
344+
const fragment = hasHeavy ? `#h=${encodeCompressed(heavy)}` : '';
345+
346+
if (search) return `${pathname}?${search}${fragment}`;
347+
return `${pathname}${fragment}`;
336348
}
337349

338350
function buildLegacyFallback(basePath: string, hash?: string): RouteStateSnapshot | null {
@@ -404,9 +416,16 @@ export function parseRouteStateFromLocation(
404416
const panel = extractPanelFromPath(locationLike.pathname) ?? DEFAULT_ROUTE_UI_STATE.activePanel;
405417
const params = new URLSearchParams(locationLike.search);
406418

407-
if ([...params.keys()].length === 0) {
419+
// Decode heavy data from fragment (new format) or fall back to query params (old URLs)
420+
let heavy: HeavyRouteData | null = null;
421+
const hash = locationLike.hash;
422+
if (hash.startsWith('#h=')) {
423+
heavy = decodeCompressed<HeavyRouteData>(hash.slice(3));
424+
}
425+
426+
if ([...params.keys()].length === 0 && !heavy) {
408427
return (
409-
buildLegacyFallback(basePath, locationLike.hash) ?? {
428+
buildLegacyFallback(basePath, hash) ?? {
410429
basePath,
411430
panel,
412431
theme: 'dark',
@@ -420,10 +439,10 @@ export function parseRouteStateFromLocation(
420439
);
421440
}
422441

423-
const spacingKnots = decodeCompressed<SpacingKnot[]>(params.get('spacing')) ?? undefined;
424-
const exactGeometry = decodeCompressed<SerializedAirfoilData>(params.get('foil')) ?? undefined;
425-
const layoutJson = decodeCompressed<IJsonModel>(params.get('layout'));
426-
const filterModel = decodeCompressed(params.get('filters'));
442+
const spacingKnots = heavy?.spacing ?? decodeCompressed<SpacingKnot[]>(params.get('spacing')) ?? undefined;
443+
const exactGeometry = heavy?.foil ?? decodeCompressed<SerializedAirfoilData>(params.get('foil')) ?? undefined;
444+
const layoutJson = heavy?.layout ?? decodeCompressed<IJsonModel>(params.get('layout'));
445+
const filterModel = heavy?.filters ?? decodeCompressed(params.get('filters'));
427446

428447
const plotGroup = params.get('plotGroup');
429448
const dataView = params.get('dataView');

0 commit comments

Comments
 (0)