fix(frontend): seed full Zod-schema defaults for stream slices + QUIC params (B17)

XHTTP showed blank Selects for Session Placement / Sequence Placement /
Padding Method / Uplink HTTP Method (and several other knobs). Those
fields have a literal "" (empty string) value in the schema, which the
Select renders as "Default (path)" / "Default (repeat-x)" / etc.
The form field was `undefined`, not `""`, so the Select showed blank
instead of the labelled default option.

newStreamSlice in InboundFormModal hand-rolled per-network seed
objects with only a handful of fields. Replaced with
{Tcp,Kcp,Ws,Grpc,HttpUpgrade,XHttp}StreamSettingsSchema.parse({}) so
every default declared in the schema populates the form on network
switch. Same change in buildAddModeValues for the initial TCP state.

QUIC Params (FinalMaskForm) had the same shape on a smaller scale —
defaultQuicParams() only seeded congestion + debug + udpHop. The
schema's other fields are .optional() (no Zod default) so a schema
parse won't help. Hard-coded the xray-core / hysteria recommended
values (maxIdleTimeout 30, keepAlivePeriod 10, brutalUp/Down 0,
maxIncomingStreams 1024, four window sizes) so the InputNumber
controls render with usable starting values instead of blank.
This commit is contained in:
MHSanaei
2026-05-26 16:31:57 +02:00
parent ece20d16f7
commit a3dfafadb1
2 changed files with 37 additions and 22 deletions

View File

@@ -80,10 +80,26 @@ function defaultNoiseItem(): Record<string, unknown> {
}
function defaultQuicParams(): Record<string, unknown> {
// Seeded with the xray-core / hysteria recommended defaults so the QUIC
// Params sub-form doesn't show blank InputNumber fields when first
// enabled. The schema declares these as .optional() (no Zod default)
// because the wire shape omits them when xray's built-in default
// applies — but the panel needs values to render the controls.
return {
congestion: 'bbr',
debug: false,
brutalUp: 0,
brutalDown: 0,
hasUdpHop: false,
udpHop: { ports: '20000-50000', interval: 5 },
maxIdleTimeout: 30,
keepAlivePeriod: 10,
disablePathMTUDiscovery: false,
maxIncomingStreams: 1024,
initStreamReceiveWindow: 8388608,
maxStreamReceiveWindow: 8388608,
initConnectionReceiveWindow: 20971520,
maxConnectionReceiveWindow: 20971520,
};
}

View File

@@ -64,6 +64,12 @@ import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt'
import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls';
import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality';
import { SniffingSchema } from '@/schemas/primitives/sniffing';
import { TcpStreamSettingsSchema } from '@/schemas/protocols/stream/tcp';
import { KcpStreamSettingsSchema } from '@/schemas/protocols/stream/kcp';
import { WsStreamSettingsSchema } from '@/schemas/protocols/stream/ws';
import { GrpcStreamSettingsSchema } from '@/schemas/protocols/stream/grpc';
import { HttpUpgradeStreamSettingsSchema } from '@/schemas/protocols/stream/httpupgrade';
import { XHttpStreamSettingsSchema } from '@/schemas/protocols/stream/xhttp';
import DateTimePicker from '@/components/DateTimePicker';
import FinalMaskForm from '@/components/FinalMaskForm';
import HeaderMapEditor from '@/components/HeaderMapEditor';
@@ -264,7 +270,7 @@ function buildAddModeValues(): InboundFormValues {
streamSettings: {
network: 'tcp',
security: 'none',
tcpSettings: { header: { type: 'none' } },
tcpSettings: TcpStreamSettingsSchema.parse({ header: { type: 'none' } }),
},
sniffing: SniffingSchema.parse({}),
port: RandomUtil.randomInteger(10000, 60000),
@@ -1305,29 +1311,22 @@ export default function InboundFormModal({
// network's blob and seed the new one with the schema defaults so the
// Form.Items inside it have valid initial values (KCP needs MTU=1350
// etc., not empty strings).
// Seed each network's settings blob with its Zod schema defaults so
// every Form.Item inside the network sub-form has a defined starting
// value. XHTTP in particular has ~20 fields (sessionPlacement,
// seqPlacement, xPaddingMethod, uplinkHTTPMethod, ...) whose value
// is the literal "" sentinel meaning "let xray-core pick its
// default". Without seeding "", the Form.Item reads `undefined` and
// the Select shows blank instead of the "Default (path)" option.
const newStreamSlice = (n: string): Record<string, unknown> => {
switch (n) {
case 'tcp':
return { header: { type: 'none' } };
case 'kcp':
return {
mtu: 1350, tti: 20,
uplinkCapacity: 5, downlinkCapacity: 20,
cwndMultiplier: 1, maxSendingWindow: 2097152,
};
case 'ws':
return { path: '/', host: '', headers: {}, heartbeatPeriod: 0 };
case 'grpc':
return { serviceName: '', authority: '', multiMode: false };
case 'httpupgrade':
return { path: '/', host: '', headers: {} };
case 'xhttp':
return {
path: '/', host: '', mode: 'auto', headers: {},
xPaddingBytes: '100-1000', scMaxEachPostBytes: '1000000',
};
default:
return {};
case 'tcp': return TcpStreamSettingsSchema.parse({ header: { type: 'none' } });
case 'kcp': return KcpStreamSettingsSchema.parse({});
case 'ws': return WsStreamSettingsSchema.parse({});
case 'grpc': return GrpcStreamSettingsSchema.parse({});
case 'httpupgrade': return HttpUpgradeStreamSettingsSchema.parse({});
case 'xhttp': return XHttpStreamSettingsSchema.parse({});
default: return {};
}
};
const onNetworkChange = (next: string) => {