fix(vless): scope testseed to xtls-rprx-vision flow

testseed is only meaningful for the exact xtls-rprx-vision flow, but the
panel was emitting it for any non-empty flow (including the UDP variant)
and keeping it on the inbound after the flow was cleared via the client
modal. Tighten the gate end-to-end:

- VLESSSettings.toJson (inbound + outbound) now only emits testseed when
  the flow is exactly xtls-rprx-vision and the array is 4 positive ints;
  default state is empty so unmodified inbounds omit the field entirely.
- canEnableVisionSeed drops the udp443 variant per spec.
- Form adds a tooltip + theme-aware help text and an inline error when
  the user partially fills the four inputs; submit is blocked in that
  state. Reset clears to empty (= use server defaults).
- UpdateInboundClient strips a now-orphaned testseed when the spliced
  client no longer leaves any XRV flow in the inbound.
- MigrationRequirements cleans up legacy rows where testseed lingered
  after flow changes or was saved for non-XRV flows by older versions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MHSanaei
2026-05-07 14:44:33 +02:00
parent 3349dcbc13
commit 79a7e7a5b5
5 changed files with 173 additions and 102 deletions

View File

@@ -1763,12 +1763,13 @@ class Inbound extends XrayCommonClass {
return false; return false;
} }
// Vision seed applies only when vision flow is selected // Vision seed applies only when the XTLS Vision (TCP/TLS) flow is selected.
// Excludes the UDP variant per spec.
canEnableVisionSeed() { canEnableVisionSeed() {
if (!this.canEnableTlsFlow()) return false; if (!this.canEnableTlsFlow()) return false;
const clients = this.settings?.vlesses; const clients = this.settings?.vlesses;
if (!Array.isArray(clients)) return false; if (!Array.isArray(clients)) return false;
return clients.some(c => c?.flow === TLS_FLOW_CONTROL.VISION || c?.flow === TLS_FLOW_CONTROL.VISION_UDP443); return clients.some(c => c?.flow === TLS_FLOW_CONTROL.VISION);
} }
canEnableReality() { canEnableReality() {
@@ -2543,7 +2544,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
encryption = "none", encryption = "none",
fallbacks = [], fallbacks = [],
selectedAuth = undefined, selectedAuth = undefined,
testseed = [900, 500, 900, 256], testseed = [],
) { ) {
super(protocol); super(protocol);
this.vlesses = vlesses; this.vlesses = vlesses;
@@ -2562,12 +2563,23 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
this.fallbacks.splice(index, 1); this.fallbacks.splice(index, 1);
} }
// Empty array means "use server defaults" (won't be sent).
// Anything else must be exactly 4 positive integers.
static isValidTestseed(arr) {
if (!Array.isArray(arr) || arr.length === 0) return true;
if (arr.length !== 4) return false;
return arr.every(v => Number.isInteger(v) && v > 0);
}
static fromJson(json = {}) { static fromJson(json = {}) {
// Ensure testseed is always initialized as an array // Preserve a saved testseed only if it's a valid 4-positive-int array; otherwise leave empty
let testseed = [900, 500, 900, 256]; // so toJson omits it and the form falls back to placeholder defaults.
if (json.testseed && Array.isArray(json.testseed) && json.testseed.length >= 4) { const saved = json.testseed;
testseed = json.testseed; const testseed = (Array.isArray(saved)
} && saved.length === 4
&& saved.every(v => Number.isInteger(v) && v > 0))
? saved
: [];
const obj = new Inbound.VLESSSettings( const obj = new Inbound.VLESSSettings(
Protocols.VLESS, Protocols.VLESS,
@@ -2576,7 +2588,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
json.encryption, json.encryption,
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []), Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []),
json.selectedAuth, json.selectedAuth,
testseed testseed,
); );
return obj; return obj;
} }
@@ -2602,9 +2614,14 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
json.selectedAuth = this.selectedAuth; json.selectedAuth = this.selectedAuth;
} }
// Only include testseed if at least one client has a flow set // testseed is only meaningful for the exact xtls-rprx-vision flow, and only when
const hasFlow = this.vlesses && this.vlesses.some(vless => vless.flow && vless.flow !== ''); // the user supplied a complete 4-positive-int array. Otherwise omit and let the
if (hasFlow && this.testseed && this.testseed.length >= 4) { // backend fall back to its safe defaults.
const hasVisionFlow = this.vlesses && this.vlesses.some(v => v.flow === TLS_FLOW_CONTROL.VISION);
if (hasVisionFlow
&& Array.isArray(this.testseed)
&& this.testseed.length === 4
&& this.testseed.every(v => Number.isInteger(v) && v > 0)) {
json.testseed = this.testseed; json.testseed = this.testseed;
} }

View File

@@ -1139,11 +1139,11 @@ class Outbound extends CommonClass {
return false; return false;
} }
// Vision seed applies only when vision flow is selected // Vision seed applies only when the XTLS Vision (TCP/TLS) flow is selected.
// Excludes the UDP variant per spec.
canEnableVisionSeed() { canEnableVisionSeed() {
if (!this.canEnableTlsFlow()) return false; if (!this.canEnableTlsFlow()) return false;
const flow = this.settings?.flow; return this.settings?.flow === TLS_FLOW_CONTROL.VISION;
return flow === TLS_FLOW_CONTROL.VISION || flow === TLS_FLOW_CONTROL.VISION_UDP443;
} }
canEnableReality() { canEnableReality() {
@@ -1799,7 +1799,7 @@ Outbound.VmessSettings = class extends CommonClass {
} }
}; };
Outbound.VLESSSettings = class extends CommonClass { Outbound.VLESSSettings = class extends CommonClass {
constructor(address, port, id, flow, encryption, reverseTag = '', reverseSniffing = new ReverseSniffing(), testpre = 0, testseed = [900, 500, 900, 256]) { constructor(address, port, id, flow, encryption, reverseTag = '', reverseSniffing = new ReverseSniffing(), testpre = 0, testseed = []) {
super(); super();
this.address = address; this.address = address;
this.port = port; this.port = port;
@@ -1814,6 +1814,12 @@ Outbound.VLESSSettings = class extends CommonClass {
static fromJson(json = {}) { static fromJson(json = {}) {
if (ObjectUtil.isEmpty(json.address) || ObjectUtil.isEmpty(json.port)) return new Outbound.VLESSSettings(); if (ObjectUtil.isEmpty(json.address) || ObjectUtil.isEmpty(json.port)) return new Outbound.VLESSSettings();
const saved = json.testseed;
const testseed = (Array.isArray(saved)
&& saved.length === 4
&& saved.every(v => Number.isInteger(v) && v > 0))
? saved
: [];
return new Outbound.VLESSSettings( return new Outbound.VLESSSettings(
json.address, json.address,
json.port, json.port,
@@ -1823,7 +1829,7 @@ Outbound.VLESSSettings = class extends CommonClass {
json.reverse?.tag || '', json.reverse?.tag || '',
ReverseSniffing.fromJson(json.reverse?.sniffing || {}), ReverseSniffing.fromJson(json.reverse?.sniffing || {}),
json.testpre || 0, json.testpre || 0,
json.testseed && json.testseed.length >= 4 ? json.testseed : [900, 500, 900, 256] testseed,
); );
} }
@@ -1843,12 +1849,14 @@ Outbound.VLESSSettings = class extends CommonClass {
sniffing: JSON.stringify(reverseSniffing) === JSON.stringify(defaultReverseSniffing) ? {} : reverseSniffing, sniffing: JSON.stringify(reverseSniffing) === JSON.stringify(defaultReverseSniffing) ? {} : reverseSniffing,
}; };
} }
// Only include Vision settings when flow is set // Vision-specific knobs are only meaningful for the exact xtls-rprx-vision flow.
if (this.flow && this.flow !== '') { if (this.flow === TLS_FLOW_CONTROL.VISION) {
if (this.testpre > 0) { if (this.testpre > 0) {
result.testpre = this.testpre; result.testpre = this.testpre;
} }
if (this.testseed && this.testseed.length >= 4) { if (Array.isArray(this.testseed)
&& this.testseed.length === 4
&& this.testseed.every(v => Number.isInteger(v) && v > 0)) {
result.testseed = this.testseed; result.testseed = this.testseed;
} }
} }

View File

@@ -81,30 +81,38 @@
</template> </template>
<template v-if="inbound.canEnableVisionSeed()"> <template v-if="inbound.canEnableVisionSeed()">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Vision Seed"> <a-form-item
:validate-status="testseedError() ? 'error' : ''"
:help="testseedError() || ''">
<template slot="label">
Vision Seed
<a-tooltip title="Optional. Controls XTLS Vision padding. Provide exactly 4 positive integers, or leave empty to use defaults: [900, 500, 900, 256].">
<a-icon type="question-circle" :style="{ marginLeft: '4px' }"></a-icon>
</a-tooltip>
</template>
<a-row :gutter="8"> <a-row :gutter="8">
<a-col :span="6"> <a-col :span="6">
<a-input-number <a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900" :value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : null"
@change="(val) => updateTestseed(0, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900" @change="(val) => updateTestseed(0, val)" :min="1" :max="9999" :style="{ width: '100%' }" placeholder="900"
addon-before="[0]"></a-input-number> addon-before="[0]"></a-input-number>
</a-col> </a-col>
<a-col :span="6"> <a-col :span="6">
<a-input-number <a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500" :value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : null"
@change="(val) => updateTestseed(1, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="500" @change="(val) => updateTestseed(1, val)" :min="1" :max="9999" :style="{ width: '100%' }" placeholder="500"
addon-before="[1]"></a-input-number> addon-before="[1]"></a-input-number>
</a-col> </a-col>
<a-col :span="6"> <a-col :span="6">
<a-input-number <a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900" :value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : null"
@change="(val) => updateTestseed(2, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900" @change="(val) => updateTestseed(2, val)" :min="1" :max="9999" :style="{ width: '100%' }" placeholder="900"
addon-before="[2]"></a-input-number> addon-before="[2]"></a-input-number>
</a-col> </a-col>
<a-col :span="6"> <a-col :span="6">
<a-input-number <a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256" :value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : null"
@change="(val) => updateTestseed(3, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="256" @change="(val) => updateTestseed(3, val)" :min="1" :max="9999" :style="{ width: '100%' }" placeholder="256"
addon-before="[3]"></a-input-number> addon-before="[3]"></a-input-number>
</a-col> </a-col>
</a-row> </a-row>
@@ -116,6 +124,10 @@
Reset Reset
</a-button> </a-button>
</a-space> </a-space>
<div :style="{ marginTop: '6px', fontSize: '12px', color: 'inherit', opacity: 0.65, lineHeight: 1.4 }">
Optional. Controls XTLS Vision padding behavior (used only for xtls-rprx-vision).
Provide exactly four positive integers to customize padding; otherwise leave empty to use safe defaults.
</div>
</a-form-item> </a-form-item>
</a-form> </a-form>
<a-divider :style="{ margin: '5px 0' }"></a-divider> <a-divider :style="{ margin: '5px 0' }"></a-divider>

View File

@@ -16,6 +16,16 @@
inbound: new Inbound(), inbound: new Inbound(),
dbInbound: new DBInbound(), dbInbound: new DBInbound(),
ok() { ok() {
// Block submit when Vision Seed is XRV-gated and partially/invalidly filled.
const seedErr = inModal.testseedError();
if (seedErr) {
if (typeof Vue !== "undefined" && Vue.prototype && Vue.prototype.$message) {
Vue.prototype.$message.error(seedErr);
} else {
alert(seedErr);
}
return;
}
ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound); ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
}, },
show({ show({
@@ -33,16 +43,12 @@
} else { } else {
this.inbound = new Inbound(); this.inbound = new Inbound();
} }
// Always ensure testseed is initialized for VLESS protocol (even if vision flow is not set yet) // Ensure VLESS settings has a testseed array reference for Vue reactivity,
// This ensures Vue reactivity works properly // but leave it empty so we don't auto-emit defaults — user must explicitly
// fill all four fields, or leave blank to fall back to backend defaults.
if (this.inbound.protocol === Protocols.VLESS && this.inbound.settings) { if (this.inbound.protocol === Protocols.VLESS && this.inbound.settings) {
if ( if (!Array.isArray(this.inbound.settings.testseed)) {
!this.inbound.settings.testseed || this.inbound.settings.testseed = [];
!Array.isArray(this.inbound.settings.testseed) ||
this.inbound.settings.testseed.length < 4
) {
// Create a new array to ensure Vue reactivity
this.inbound.settings.testseed = [900, 500, 900, 256].slice();
} }
} }
if (dbInbound) { if (dbInbound) {
@@ -61,48 +67,50 @@
loading(loading = true) { loading(loading = true) {
inModal.confirmLoading = loading; inModal.confirmLoading = loading;
}, },
// Vision Seed methods - always available regardless of Vue context // Returns an error string when the current testseed state would be rejected,
// or "" when it's valid (empty == use defaults; full 4 positive ints == custom).
testseedError() {
if (!inModal.inbound || inModal.inbound.protocol !== Protocols.VLESS) return "";
if (typeof inModal.inbound.canEnableVisionSeed === "function"
&& !inModal.inbound.canEnableVisionSeed()) return "";
const seed = inModal.inbound.settings && inModal.inbound.settings.testseed;
if (!Array.isArray(seed) || seed.length === 0) return "";
const filled = seed.filter(v => v !== null && v !== undefined && v !== "");
if (filled.length === 0) return "";
if (seed.length !== 4 || filled.length !== 4 ||
!seed.every(v => Number.isInteger(v) && v > 0)) {
return "Provide exactly 4 positive integers or leave empty to use defaults.";
}
return "";
},
// Vision Seed helpers — always available regardless of Vue context
updateTestseed(index, value) { updateTestseed(index, value) {
// Use inModal.inbound explicitly to ensure correct context
if (!inModal.inbound || !inModal.inbound.settings) return; if (!inModal.inbound || !inModal.inbound.settings) return;
// Ensure testseed is initialized if (!Array.isArray(inModal.inbound.settings.testseed)) {
if ( inModal.inbound.settings.testseed = [];
!inModal.inbound.settings.testseed ||
!Array.isArray(inModal.inbound.settings.testseed)
) {
inModal.inbound.settings.testseed = [900, 500, 900, 256];
} }
// Ensure array has enough elements const seed = inModal.inbound.settings.testseed;
while (inModal.inbound.settings.testseed.length <= index) { while (seed.length <= index) seed.push(null);
inModal.inbound.settings.testseed.push(0); seed[index] = value;
// If user cleared every slot, collapse back to empty so we omit testseed entirely.
if (seed.every(v => v === null || v === undefined || v === "")) {
inModal.inbound.settings.testseed = [];
} }
// Update value
inModal.inbound.settings.testseed[index] = value;
}, },
setRandomTestseed() { setRandomTestseed() {
// Use inModal.inbound explicitly to ensure correct context
if (!inModal.inbound || !inModal.inbound.settings) return; if (!inModal.inbound || !inModal.inbound.settings) return;
// Ensure testseed is initialized // Positive integers only (>=1) so the array passes validation and gets emitted.
if (
!inModal.inbound.settings.testseed ||
!Array.isArray(inModal.inbound.settings.testseed) ||
inModal.inbound.settings.testseed.length < 4
) {
inModal.inbound.settings.testseed = [900, 500, 900, 256].slice();
}
// Create new array with random values
inModal.inbound.settings.testseed = [ inModal.inbound.settings.testseed = [
Math.floor(Math.random() * 1000), Math.floor(Math.random() * 999) + 1,
Math.floor(Math.random() * 1000), Math.floor(Math.random() * 999) + 1,
Math.floor(Math.random() * 1000), Math.floor(Math.random() * 999) + 1,
Math.floor(Math.random() * 1000), Math.floor(Math.random() * 999) + 1,
]; ];
}, },
resetTestseed() { resetTestseed() {
// Use inModal.inbound explicitly to ensure correct context
if (!inModal.inbound || !inModal.inbound.settings) return; if (!inModal.inbound || !inModal.inbound.settings) return;
// Reset testseed to default values // Empty == "use server defaults [900, 500, 900, 256]"; placeholders show in the form.
inModal.inbound.settings.testseed = [900, 500, 900, 256].slice(); inModal.inbound.settings.testseed = [];
}, },
}); });
@@ -170,27 +178,17 @@
}); });
} }
}, },
// Ensure testseed is always initialized when vision flow is enabled // Keep testseed as a valid array reference for Vue reactivity while the user
// toggles flows — but do NOT auto-fill defaults. Empty means "use server defaults"
// and is the only way the form omits testseed from the outbound JSON.
"inModal.inbound.settings.vlesses": { "inModal.inbound.settings.vlesses": {
handler() { handler() {
if ( if (
inModal.inbound.protocol === Protocols.VLESS && inModal.inbound.protocol === Protocols.VLESS &&
inModal.inbound.settings && inModal.inbound.settings &&
inModal.inbound.settings.vlesses !Array.isArray(inModal.inbound.settings.testseed)
) { ) {
const hasVisionFlow = inModal.inbound.settings.vlesses.some( inModal.inbound.settings.testseed = [];
(c) =>
c.flow === "xtls-rprx-vision" ||
c.flow === "xtls-rprx-vision-udp443",
);
if (
hasVisionFlow &&
(!inModal.inbound.settings.testseed ||
!Array.isArray(inModal.inbound.settings.testseed) ||
inModal.inbound.settings.testseed.length < 4)
) {
inModal.inbound.settings.testseed = [900, 500, 900, 256];
}
} }
}, },
deep: true, deep: true,
@@ -335,35 +333,36 @@
this.inbound.settings.encryption = "none"; this.inbound.settings.encryption = "none";
this.inbound.settings.selectedAuth = undefined; this.inbound.settings.selectedAuth = undefined;
}, },
// Vision Seed methods - must be in Vue methods for proper binding // Vision Seed methods - must be in Vue methods for proper template binding.
// Mirror the inModal helpers but use Vue.set so the form re-renders.
updateTestseed(index, value) { updateTestseed(index, value) {
// Ensure testseed is initialized if (!Array.isArray(this.inbound.settings.testseed)) {
if ( this.$set(this.inbound.settings, "testseed", []);
!this.inbound.settings.testseed ||
!Array.isArray(this.inbound.settings.testseed)
) {
this.$set(this.inbound.settings, "testseed", [900, 500, 900, 256]);
} }
// Ensure array has enough elements const seed = this.inbound.settings.testseed;
while (this.inbound.settings.testseed.length <= index) { while (seed.length <= index) seed.push(null);
this.inbound.settings.testseed.push(0); this.$set(seed, index, value);
// Collapse to empty when every slot is cleared so testseed is omitted from JSON.
if (seed.every(v => v === null || v === undefined || v === "")) {
this.$set(this.inbound.settings, "testseed", []);
} }
// Update value using Vue.set for reactivity
this.$set(this.inbound.settings.testseed, index, value);
}, },
setRandomTestseed() { setRandomTestseed() {
// Create new array with random values and use Vue.set for reactivity // Positive integers only (>=1) so the resulting array passes validation.
const newSeed = [ const newSeed = [
Math.floor(Math.random() * 1000), Math.floor(Math.random() * 999) + 1,
Math.floor(Math.random() * 1000), Math.floor(Math.random() * 999) + 1,
Math.floor(Math.random() * 1000), Math.floor(Math.random() * 999) + 1,
Math.floor(Math.random() * 1000), Math.floor(Math.random() * 999) + 1,
]; ];
this.$set(this.inbound.settings, "testseed", newSeed); this.$set(this.inbound.settings, "testseed", newSeed);
}, },
resetTestseed() { resetTestseed() {
// Reset testseed to default values using Vue.set for reactivity // Empty == "use server defaults [900, 500, 900, 256]". Placeholders will show in the form.
this.$set(this.inbound.settings, "testseed", [900, 500, 900, 256]); this.$set(this.inbound.settings, "testseed", []);
},
testseedError() {
return inModal.testseedError();
}, },
}, },
}); });

View File

@@ -1213,6 +1213,28 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
settingsClients[clientIndex] = interfaceClients[0] settingsClients[clientIndex] = interfaceClients[0]
oldSettings["clients"] = settingsClients oldSettings["clients"] = settingsClients
// testseed is only meaningful when at least one VLESS client uses the exact
// xtls-rprx-vision flow. The client-edit path only rewrites a single client,
// so re-check the flow set here and strip a stale testseed when nothing in the
// inbound still warrants it. The full-inbound update path already handles this
// on the JS side via VLESSSettings.toJson().
if oldInbound.Protocol == model.VLESS {
hasVisionFlow := false
for _, c := range settingsClients {
cm, ok := c.(map[string]any)
if !ok {
continue
}
if flow, _ := cm["flow"].(string); flow == "xtls-rprx-vision" {
hasVisionFlow = true
break
}
}
if !hasVisionFlow {
delete(oldSettings, "testseed")
}
}
newSettings, err := json.MarshalIndent(oldSettings, "", " ") newSettings, err := json.MarshalIndent(oldSettings, "", " ")
if err != nil { if err != nil {
return false, err return false, err
@@ -2885,6 +2907,7 @@ func (s *InboundService) MigrationRequirements() {
if ok { if ok {
// Fix Client configuration problems // Fix Client configuration problems
var newClients []any var newClients []any
hasVisionFlow := false
for client_index := range clients { for client_index := range clients {
c := clients[client_index].(map[string]any) c := clients[client_index].(map[string]any)
@@ -2910,6 +2933,9 @@ func (s *InboundService) MigrationRequirements() {
c["flow"] = "" c["flow"] = ""
} }
} }
if flow, _ := c["flow"].(string); flow == "xtls-rprx-vision" {
hasVisionFlow = true
}
// Backfill created_at and updated_at // Backfill created_at and updated_at
if _, ok := c["created_at"]; !ok { if _, ok := c["created_at"]; !ok {
c["created_at"] = time.Now().Unix() * 1000 c["created_at"] = time.Now().Unix() * 1000
@@ -2918,6 +2944,15 @@ func (s *InboundService) MigrationRequirements() {
newClients = append(newClients, any(c)) newClients = append(newClients, any(c))
} }
settings["clients"] = newClients settings["clients"] = newClients
// Drop orphaned testseed: VLESS-only field, only meaningful when at least
// one client uses the exact xtls-rprx-vision flow. Older versions saved it
// for any non-empty flow (including the UDP variant) or kept it after the
// flow was cleared from the client modal — clean those up here.
if inbounds[inbound_index].Protocol == model.VLESS && !hasVisionFlow {
delete(settings, "testseed")
}
modifiedSettings, err := json.MarshalIndent(settings, "", " ") modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil { if err != nil {
return return