diff --git a/frontend/src/components/ui/data-table.test.tsx b/frontend/src/components/ui/data-table.test.tsx index df46d15b..d5c43a1e 100644 --- a/frontend/src/components/ui/data-table.test.tsx +++ b/frontend/src/components/ui/data-table.test.tsx @@ -319,6 +319,27 @@ describe('DataTable — empty results', () => { const secondInput = screen.getByRole('textbox'); expect(secondInput.getAttribute('id')).not.toBe(firstId); }); + + it('caps the filter input length so a paste of multi-KB content cannot blow past URL limits', () => { + render( + + columns={COLUMNS} + data={ROWS} + filterColumn="name" + filterValue="" + onFilterChange={() => { + /* no-op */ + }} + />, + { wrapper: Wrapper }, + ); + + const input = screen.getByRole('textbox') as HTMLInputElement; + // The DOM `maxLength` is the only choke point we need — `` + // truncates both typing and paste at this boundary, which keeps + // shared `?q=` URLs under the practical reverse-proxy limit (~2–4 KB). + expect(input.maxLength).toBe(200); + }); }); interface MultiRow { diff --git a/frontend/src/components/ui/data-table.tsx b/frontend/src/components/ui/data-table.tsx index c4c77a0f..352ad3ac 100644 --- a/frontend/src/components/ui/data-table.tsx +++ b/frontend/src/components/ui/data-table.tsx @@ -155,6 +155,13 @@ interface DataTableFilterProps { } const FILTER_DEBOUNCE_MS = 150; +// Hard cap on the filter query length. 200 chars is more than any realistic +// search term and protects against pathological inputs (paste of a multi-KB +// chunk) that would otherwise blow past URL limits — browsers handle ~8 KB, +// reverse-proxies typically cap at 2–4 KB, so a 5 KB share-link becomes +// unreliable. `` truncates typing and paste at the DOM +// boundary, which is the only entry point users have here. +const FILTER_MAX_LENGTH = 200; /** * Search input for the table's global filter. Keystrokes update an internal @@ -211,6 +218,7 @@ const DataTableFilter = ({ onQueryChange, placeholder, query }: DataTableFilterP aria-label={placeholder} autoComplete="off" id={fieldId} + maxLength={FILTER_MAX_LENGTH} name={fieldId} onChange={(event) => setLocalValue(event.target.value)} placeholder={placeholder}