mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-03 00:47:14 +00:00
fix(subscriptions): avoid shared mutable state during generation (#5270)
* fix(subscriptions): avoid shared mutable state during generation * fix(subscriptions): serve external-link-only subs in JSON/Clash; load remark settings per request The ForRequest refactor added an early `len(inbounds) == 0` return to GetJson/GetClash that fired before external links were fetched, so a subscription whose only entries are external links (or whose inbounds are all disabled) rendered empty in the JSON and Clash formats. Drop the premature check — the existing inbounds+externalLinks empty guard already covers the truly-empty case. Also load datepicker/emailInRemark in PrepareForRequest rather than only in getSubs, so JSON and Clash remarks honor these settings instead of seeing the zero values (emailInRemark previously depended on the shared-state leak this PR fixes). Add a regression test covering an external-link-only sub across both formats. --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
+33
-16
@@ -49,11 +49,18 @@ func NewSubService(showInfo bool, remarkModel string) *SubService {
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareForRequest sets per-request state (host + nodes map) on the
|
||||
// shared SubService. Called by every entry point — GetSubs, GetJson,
|
||||
// GetClash — so resolveInboundAddress sees the right host and the
|
||||
// freshly-loaded node map regardless of which sub flavour the client
|
||||
// hit.
|
||||
// ForRequest returns a shallow copy with request-scoped state populated.
|
||||
// Subscription controllers share one base SubService, so request-specific
|
||||
// fields such as address and nodesByID must live on a per-request copy.
|
||||
func (s *SubService) ForRequest(host string) *SubService {
|
||||
req := *s
|
||||
req.PrepareForRequest(host)
|
||||
return &req
|
||||
}
|
||||
|
||||
// PrepareForRequest sets per-request state (host + nodes map) on this
|
||||
// SubService instance. HTTP handlers should call ForRequest instead so the
|
||||
// controller's shared base service is never mutated by concurrent requests.
|
||||
func (s *SubService) PrepareForRequest(host string) {
|
||||
if !isRoutableHost(host) {
|
||||
if d := s.configuredPublicHost(); d != "" {
|
||||
@@ -64,6 +71,23 @@ func (s *SubService) PrepareForRequest(host string) {
|
||||
}
|
||||
s.address = host
|
||||
s.loadNodes()
|
||||
s.loadRemarkSettings()
|
||||
}
|
||||
|
||||
// loadRemarkSettings populates the per-request remark formatting state so
|
||||
// every subscription format — raw, JSON, Clash — renders remarks the same
|
||||
// way. genRemark reads emailInRemark and the date formatter reads datepicker;
|
||||
// loading these only in getSubs left JSON/Clash with the zero values.
|
||||
func (s *SubService) loadRemarkSettings() {
|
||||
var err error
|
||||
s.datepicker, err = s.settingService.GetDatepicker()
|
||||
if err != nil {
|
||||
s.datepicker = "gregorian"
|
||||
}
|
||||
s.emailInRemark, err = s.settingService.GetSubEmailInRemark()
|
||||
if err != nil {
|
||||
s.emailInRemark = true
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SubService) configuredPublicHost() string {
|
||||
@@ -139,7 +163,10 @@ func (s *SubService) matchingClients(inbound *model.Inbound, subId string) []mod
|
||||
|
||||
// GetSubs retrieves subscription links for a given subscription ID and host.
|
||||
func (s *SubService) GetSubs(subId string, host string) ([]string, []string, int64, xray.ClientTraffic, error) {
|
||||
s.PrepareForRequest(host)
|
||||
return s.ForRequest(host).getSubs(subId)
|
||||
}
|
||||
|
||||
func (s *SubService) getSubs(subId string) ([]string, []string, int64, xray.ClientTraffic, error) {
|
||||
var result []string
|
||||
var emails []string
|
||||
var traffic xray.ClientTraffic
|
||||
@@ -157,16 +184,6 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, []string, int
|
||||
return nil, nil, 0, traffic, nil
|
||||
}
|
||||
|
||||
s.datepicker, err = s.settingService.GetDatepicker()
|
||||
if err != nil {
|
||||
s.datepicker = "gregorian"
|
||||
}
|
||||
|
||||
s.emailInRemark, err = s.settingService.GetSubEmailInRemark()
|
||||
if err != nil {
|
||||
s.emailInRemark = true
|
||||
}
|
||||
|
||||
seenEmails := make(map[string]struct{})
|
||||
for _, inbound := range inbounds {
|
||||
clients := s.matchingClients(inbound, subId)
|
||||
|
||||
Reference in New Issue
Block a user