fix(frontend): cap DataTable filter input at 200 chars

A paste of multi-KB content into the filter would land in the URL
verbatim (`?q=` plus the raw blob), which exceeds the practical
reverse-proxy limit (~2–4 KB) and breaks the share-link experience.
The user-facing entry point is a single `<input>`, so a DOM-level
`maxLength` is sufficient — it truncates both typing and paste before
the value ever reaches React state, the URL, or localStorage. 200 chars
is well above any realistic search term while leaving plenty of
headroom for non-ASCII expansion under percent-encoding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-05-16 21:41:42 +07:00
co-authored by Claude Opus 4.7
parent 022e8f3df0
commit cc6b525f57
2 changed files with 29 additions and 0 deletions
@@ -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(
<DataTable<Row>
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 — `<input>`
// truncates both typing and paste at this boundary, which keeps
// shared `?q=` URLs under the practical reverse-proxy limit (~24 KB).
expect(input.maxLength).toBe(200);
});
});
interface MultiRow {
@@ -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 24 KB, so a 5 KB share-link becomes
// unreliable. `<input maxLength>` 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}