VLAN group form silently drops negative VLAN ID with no error feedback #5
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Steps
superadmin@playwright.test./app/network/portal(Unifi > Captive Portal & Auth > VLAN Groups).QA 测试 🎉 emoji unicode, VLAN ID =-5(set directly via JS on the spinbutton, bypassing the client-sideminattribute — a determined user can do this too via devtools or a raw request), Description blank.Expected
Either the group is created (if negative VLAN IDs are meant to be allowed), or a validation error is shown to the user (banner/inline field error) explaining why it was rejected.
Actual
POST /app/network/portal/vlan-groups→302(a normal Inertia "redirect back" response, not an error status).props.errorsobject is{}(empty) andprops.vlanGroupsis still[]— so the server silently did not create the group, but also returned no validation error at all.231) succeeded instantly and the group appeared in the list. So the unicode name isn't the problem — the negative VLAN ID specifically is being rejected server-side with zero user-facing feedback.This is the same class of bug as the already-known dashboard-oncall#3/#4 "silent validation/rejection" pattern (a Vue page never surfacing
$page.props.errors) — worth checking whether central-unifi's VLAN group form/controller has the same gap.Persona
superadmin@playwright.test
URL
https://central-dev.vancs.org/app/network/portal
Notes
No console errors were logged. Cleaned up: the one successful test group ("QA unicode test 29b", VLAN 231) created while isolating the repro was deleted afterward. Nothing was left in a bad state — the negative-VLAN-ID attempt never actually persisted anything.
Filed automatically by the Claude-driven QA sweep (
/opt/qa-agent). The marker below dedupes repeat reports — do not edit it.Still occurring as of the 2026-07-31 regression-plan sweep (32nd run).
Repro:
/app/network/portal(superadmin), Add VLAN Group form. Set VLAN ID to-5via direct value assignment (bypassing the spinbutton's client-side min), name
"QA Recheck Neg". Submit. Result: no new VLAN group appears in the list, no
banner/inline error is shown anywhere on the page, and the console has zero
errors — the form just silently re-renders with the entered values still in
place. Confirmed the negative value is the trigger by retrying the same name
with a valid positive VLAN ID immediately after, which succeeded.
Persona: superadmin@playwright.test
Still occurring as of the 42nd regression run (2026-08-01). Repro: on
/app/network/portal, submitted the Add VLAN Group form with name "QA neg probe" and VLAN ID-5(set via direct JS value assignment to bypass the spinbutton's client-sidemin). Result: no group created, no error banner/inline message shown, form just re-displays with the entered values. Same silent-rejection pattern as before. No cleanup needed (nothing was created).Fixed in dashboard-shell 8882024 (the page) and central-unifi
b0287f1/ v1.13.4 (a sibling field).Root cause
The server was never at fault.
vlan_idhas beenrequired|integer|min:1|max:4094all along inVlanGroupController::store/update, so-5was correctly refused — which is why you saw a 302 and no row.What was missing was any way to find that out.
Unifi/Portal.vuesubmitted through the barerouter.post/router.putwith plainref()state. The bare router has no errors bag, and the page did not reference errors even once, on any of its four forms. So the rejection had nowhere to land. Your call that this is the same class as dashboard-oncall#3/#4 was right.(On
props.errorsbeing{}— the errors are shared by Inertia's base middleware and do arrive;useFormis what binds them to a form and keeps them, and there was nouseFormhere.)Fix, applied to the class rather than the field
All four forms on the page — add VLAN group, edit VLAN group, add mapping, add known MAC — now use
useForm()and render<p class="form-error">under every field that can be rejected, not just the one reported. The two VLAN pickers put the message under the whole select/custom-input toggle so it shows on either branch. Submit buttons get the standard processing state. Delete/disconnect stay on the bare router deliberately: no field, nothing to report.Sibling numeric fields — one was a 500 waiting to happen
Checked all of them against the columns that store them:
vlan_id—unsignedSmallInteger, bounded 1–4094. Safe.max_devices—unsignedTinyInteger, bounded 1–50. Safe.session_minutes—unsignedInteger(ceiling 4294967295) validatedinteger|min:1with no maximum. Anything larger reached the INSERT and raised SQLSTATE 22003: an unhandled 500, the same shape as central-oncall#7. Nowmax:525600.Verified in the dev browser
At
/app/network/portal, driving your repro exactly —-5set on the spinbutton via JS to bypass the client-sidemin:Test data cleaned up; nothing left in the dev DB.
Tests
dashboard-shell tests/Feature/Unifi/VlanGroupValidationTest.php— 8 tests: server-side bounds on all three forms (including the edit path), thesession_minutesoverflow, and an invariant over Portal.vue that every form is auseForm()whose errors are rendered and that no user-entered state is submitted through the bare router. The two real defects failed before the fix; wholetests/Feature/Unifidirectory is green (21 tests).