This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git


The following commit(s) were added to refs/heads/master by this push:
     new 064c4ba  fix(version): send the update through the configured proxy
064c4ba is described below

commit 064c4bab62f57243271e4d778467932df626266f
Author: Yang Luo <[email protected]>
AuthorDate: Sun Aug 30 22:50:34 2026 +0800

    fix(version): send the update through the configured proxy
---
 conf/app.conf                               |  3 +-
 proxy/proxy.go                              | 60 ++++++++++++++++++++---------
 version/check.go                            | 26 ++++++++++++-
 version/download.go                         |  4 +-
 version/info.go                             |  7 +++-
 version/update.go                           |  3 ++
 web/src/components/shared/version-panel.tsx | 16 ++++++++
 web/src/locales/en/data.json                |  4 +-
 web/src/locales/zh/data.json                |  4 +-
 web/src/types.ts                            |  4 ++
 10 files changed, 105 insertions(+), 26 deletions(-)

diff --git a/conf/app.conf b/conf/app.conf
index 1ad22df..3ece953 100644
--- a/conf/app.conf
+++ b/conf/app.conf
@@ -50,7 +50,8 @@ gatewayEnabled = false
 gatewayHttpPort = 80
 gatewayHttpsPort = 443
 
-; Optional SOCKS5 proxy ("host:port") for outbound traffic such as ACME.
+; Optional SOCKS5 proxy ("host:port") for outbound traffic such as update
+; downloads and ACME.
 httpProxy = ""
 
 ; Casdoor is optional. Leave "casdoorEndpoint" empty to sign in against 
Gateway's
diff --git a/proxy/proxy.go b/proxy/proxy.go
index 10cfbfa..27bfab1 100644
--- a/proxy/proxy.go
+++ b/proxy/proxy.go
@@ -31,29 +31,34 @@ import (
 var ProxyHttpClient *http.Client
 
 var (
-       proxyUrlOnce sync.Once
-       proxyUrl     *url.URL
+       proxyLock sync.Mutex
+       proxyRead bool
+       proxyRaw  string
+       proxyUrl  *url.URL
 
        transportOnce sync.Once
        transport     *http.Transport
 )
 
 func InitHttpClient() {
-       proxyUrlOnce.Do(initProxyUrl)
+       // Reading the setting here rather than on first use keeps the "proxy
+       // enabled" line in the startup log.
+       currentProxyUrl()
        ProxyHttpClient = &http.Client{Transport: Transport()}
 }
 
 // Proxy picks the proxy to reach req through. It is meant to be assigned to
 // http.Transport.Proxy, which calls it per request, so the httpProxy setting 
is
-// read on first use instead of at package initialization: in -tags embed 
builds
-// the embedded conf/app.conf is only loaded once main's init() runs, after 
every
-// imported package has already initialized its own variables.
+// read on use instead of at package initialization: in -tags embed builds the
+// embedded conf/app.conf is only loaded once main's init() runs, after every
+// imported package has already initialized its own variables. Reading it per
+// request also means a proxy set on the Settings page applies straight away.
 func Proxy(req *http.Request) (*url.URL, error) {
-       proxyUrlOnce.Do(initProxyUrl)
-       if proxyUrl == nil {
-               return http.ProxyFromEnvironment(req)
+       if parsedUrl := currentProxyUrl(); parsedUrl != nil {
+               return parsedUrl, nil
        }
-       return proxyUrl, nil
+
+       return http.ProxyFromEnvironment(req)
 }
 
 // Transport returns the shared transport for outbound requests. Sharing one
@@ -66,14 +71,31 @@ func Transport() *http.Transport {
        return transport
 }
 
-// initProxyUrl parses the httpProxy setting. A bare "host:port" means SOCKS5,
-// which is what the setting has always been taken to mean; "socks5://",
-// "socks5h://", "http://"; and "https://"; addresses are honoured as written, 
and
-// may carry credentials.
-func initProxyUrl() {
+// currentProxyUrl parses the httpProxy setting, re-parsing it only when the
+// setting itself has changed.
+func currentProxyUrl() *url.URL {
        httpProxy := 
strings.TrimSpace(conf.GetConfigStringUnquoted("httpProxy"))
+
+       proxyLock.Lock()
+       defer proxyLock.Unlock()
+
+       if proxyRead && httpProxy == proxyRaw {
+               return proxyUrl
+       }
+
+       proxyRead = true
+       proxyRaw = httpProxy
+       proxyUrl = parseProxyUrl(httpProxy)
+       return proxyUrl
+}
+
+// parseProxyUrl reads a proxy address. A bare "host:port" means SOCKS5, which
+// is what the setting has always been taken to mean; "socks5://", 
"socks5h://",
+// "http://"; and "https://"; addresses are honoured as written, and may carry
+// credentials.
+func parseProxyUrl(httpProxy string) *url.URL {
        if httpProxy == "" {
-               return
+               return nil
        }
 
        if !strings.Contains(httpProxy, "://") {
@@ -83,9 +105,9 @@ func initProxyUrl() {
        parsedUrl, err := url.Parse(httpProxy)
        if err != nil || parsedUrl.Host == "" {
                fmt.Printf("httpProxy is not a valid proxy address, outbound 
traffic is left unproxied: %s\n", httpProxy)
-               return
+               return nil
        }
 
-       proxyUrl = parsedUrl
-       fmt.Printf("Proxy enabled for outbound traffic: %s\n", 
proxyUrl.Redacted())
+       fmt.Printf("Proxy enabled for outbound traffic: %s\n", 
parsedUrl.Redacted())
+       return parsedUrl
 }
diff --git a/version/check.go b/version/check.go
index e43ca61..680b4ea 100644
--- a/version/check.go
+++ b/version/check.go
@@ -15,15 +15,21 @@
 package version
 
 import (
+       "context"
        "encoding/json"
+       "errors"
        "fmt"
        "io"
+       "net"
        "net/http"
+       "net/url"
        "regexp"
        "runtime"
        "strings"
        "sync"
        "time"
+
+       "github.com/apache/casbin-gateway/proxy"
 )
 
 const (
@@ -81,7 +87,7 @@ var (
        cached      *Release
        cachedErr   error
        cachedAt    time.Time
-       httpClient  = &http.Client{Timeout: checkTimeout}
+       httpClient  = &http.Client{Timeout: checkTimeout, Transport: 
proxy.Transport()}
        reCommitSha = regexp.MustCompile(`\b[0-9a-f]{40}\b`)
 )
 
@@ -153,6 +159,24 @@ func fetchLatestRelease() (*Release, error) {
        return release, nil
 }
 
+// IsNetworkError reports whether err is this machine failing to reach GitHub
+// rather than the update itself going wrong. Where GitHub is unreachable a
+// proxy is what fixes it, so the web UI says so instead of only quoting the
+// error.
+func IsNetworkError(err error) bool {
+       if err == nil {
+               return false
+       }
+
+       var netErr net.Error
+       var urlErr *url.Error
+       if errors.As(err, &netErr) || errors.As(err, &urlErr) {
+               return true
+       }
+
+       return errors.Is(err, context.DeadlineExceeded) || errors.Is(err, 
io.ErrUnexpectedEOF)
+}
+
 // releaseCommit finds the commit the nightly was built from. The release is
 // created with --target <sha>, so target_commitish normally holds it; the 
notes
 // repeat it, which covers the case where GitHub hands back a branch name.
diff --git a/version/download.go b/version/download.go
index a22b949..0199320 100644
--- a/version/download.go
+++ b/version/download.go
@@ -30,6 +30,8 @@ import (
        "sync"
        "sync/atomic"
        "time"
+
+       "github.com/apache/casbin-gateway/proxy"
 )
 
 // A single connection to the release CDN is throttled, and on some networks
@@ -109,7 +111,7 @@ func download(release *Release, path string) error {
 func newDownloadClient() *http.Client {
        return &http.Client{
                Transport: &http.Transport{
-                       Proxy:                 http.ProxyFromEnvironment,
+                       Proxy:                 proxy.Proxy,
                        DialContext:           (&net.Dialer{Timeout: 
connectTimeout, KeepAlive: 30 * time.Second}).DialContext,
                        TLSNextProto:          map[string]func(string, 
*tls.Conn) http.RoundTripper{},
                        TLSHandshakeTimeout:   connectTimeout,
diff --git a/version/info.go b/version/info.go
index e1a5d00..16441bc 100644
--- a/version/info.go
+++ b/version/info.go
@@ -30,8 +30,10 @@ type Info struct {
        // CheckError is a failed lookup rather than a failed update: the 
version in
        // hand is still worth showing.
        CheckError string `json:"checkError"`
-       Update     Status `json:"update"`
-       ReleaseUrl string `json:"releaseUrl"`
+       // CheckNetwork means the lookup failed because GitHub could not be 
reached.
+       CheckNetwork bool   `json:"checkNetwork"`
+       Update       Status `json:"update"`
+       ReleaseUrl   string `json:"releaseUrl"`
        // InstallCommand is the manual way in, shown when CanUpdate is false.
        InstallCommand string `json:"installCommand"`
 }
@@ -52,6 +54,7 @@ func Describe(force bool) Info {
        release, err := LatestRelease(force)
        if err != nil {
                info.CheckError = err.Error()
+               info.CheckNetwork = IsNetworkError(err)
                return info
        }
 
diff --git a/version/update.go b/version/update.go
index 3cc3b0b..dd06fee 100644
--- a/version/update.go
+++ b/version/update.go
@@ -55,6 +55,8 @@ type Status struct {
        Total      int64  `json:"total"`
        Target     string `json:"target"`
        Error      string `json:"error"`
+       // Network means the failure was reaching GitHub, which a proxy can fix.
+       Network bool `json:"network"`
 }
 
 // stagingDir holds the download and the unpacked executable. It sits next to
@@ -121,6 +123,7 @@ func fail(err error) {
 
        status.Stage = StageFailed
        status.Error = err.Error()
+       status.Network = IsNetworkError(err)
 }
 
 // BlockedReason says why this installation cannot replace its own executable,
diff --git a/web/src/components/shared/version-panel.tsx 
b/web/src/components/shared/version-panel.tsx
index 75cd4b3..fb802c4 100644
--- a/web/src/components/shared/version-panel.tsx
+++ b/web/src/components/shared/version-panel.tsx
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 import * as React from "react";
+import {Link} from "react-router-dom";
 import {ArrowUpCircle, Check, CircleAlert, Copy, ExternalLink, Loader2, 
RefreshCw} from "lucide-react";
 import copy from "copy-to-clipboard";
 import i18next from "i18next";
@@ -164,6 +165,8 @@ export function VersionPanel({
   const [phase, setPhase] = React.useState<Phase>("idle");
   const [status, setStatus] = React.useState<UpdateStatus | null>(null);
   const [failure, setFailure] = React.useState("");
+  // A failure that is the network, not the update: a proxy is what fixes it.
+  const [failureNetwork, setFailureNetwork] = React.useState(false);
 
   const load = React.useCallback((refresh: boolean) => {
     setChecking(refresh);
@@ -202,6 +205,7 @@ export function VersionPanel({
           setStatus(res.data);
           if (res.data.stage === "failed") {
             setFailure(res.data.error);
+            setFailureNetwork(res.data.network);
             setPhase("idle");
           } else if (res.data.stage === "restarting") {
             setPhase("waiting");
@@ -236,6 +240,7 @@ export function VersionPanel({
         stopped = true;
         window.clearInterval(timer);
         setFailure(i18next.t("general:The new version did not start, check the 
Gateway log"));
+        setFailureNetwork(false);
         setPhase("idle");
         return;
       }
@@ -249,6 +254,7 @@ export function VersionPanel({
             stopped = true;
             window.clearInterval(timer);
             setFailure(res.data.error);
+            setFailureNetwork(res.data.network);
             setPhase("idle");
             return;
           }
@@ -271,6 +277,7 @@ export function VersionPanel({
 
   const startUpdate = () => {
     setFailure("");
+    setFailureNetwork(false);
     setStatus(null);
     setPhase("starting");
     MiscBackend.updateGateway()
@@ -396,6 +403,15 @@ export function VersionPanel({
             </p>
           ) : null}
 
+          {failureNetwork || (failure === "" && info.checkNetwork) ? (
+            <p className="text-muted-foreground text-xs">
+              {i18next.t("general:GitHub could not be reached, if this machine 
needs a proxy to reach the internet, set one and try again")}{" "}
+              <Link to="/settings" onClick={() => setOpen(false)} 
className="text-primary underline underline-offset-2">
+                {i18next.t("general:Set a proxy")}
+              </Link>
+            </p>
+          ) : null}
+
           {busy ? (
             <div className="space-y-2">
               <div className="flex items-center justify-between text-sm">
diff --git a/web/src/locales/en/data.json b/web/src/locales/en/data.json
index 2c7ff9f..a1d843c 100644
--- a/web/src/locales/en/data.json
+++ b/web/src/locales/en/data.json
@@ -354,6 +354,7 @@
     "Failed to save": "Failed to save",
     "Failed to sign out": "Failed to sign out",
     "Gateway Analytics": "Gateway Analytics",
+    "GitHub could not be reached, if this machine needs a proxy to reach the 
internet, set one and try again": "GitHub could not be reached, if this machine 
needs a proxy to reach the internet, set one and try again",
     "Go to writable demo site?": "Go to writable demo site?",
     "Half A Hour": "Half A Hour",
     "Home": "Home",
@@ -398,6 +399,7 @@
     "Search": "Search",
     "Select": "Select",
     "Self": "Self",
+    "Set a proxy": "Set a proxy",
     "Signed out successfully": "Signed out successfully",
     "Signing in...": "Signing in...",
     "Sites": "Sites",
@@ -716,7 +718,7 @@
     "Max records": "Max records",
     "Network and certificates": "Network and certificates",
     "Outbound SOCKS5 proxy": "Outbound SOCKS5 proxy",
-    "Outbound SOCKS5 proxy hint": "\"host:port\", used for outbound traffic 
such as ACME.",
+    "Outbound SOCKS5 proxy hint": "\"host:port\", used for outbound traffic 
such as update downloads and ACME.",
     "Page description": "Everything Casbin Gateway reads out of its 
configuration. Changes are stored in the database and applied at once.",
     "Pricing file": "Pricing file",
     "Pricing file hint": "Optional JSON file of token prices in US dollars per 
million, overriding the built-in list prices.",
diff --git a/web/src/locales/zh/data.json b/web/src/locales/zh/data.json
index 82d8c43..374cea4 100644
--- a/web/src/locales/zh/data.json
+++ b/web/src/locales/zh/data.json
@@ -354,6 +354,7 @@
     "Failed to save": "保存失败",
     "Failed to sign out": "退出登录失败",
     "Gateway Analytics": "网关统计",
+    "GitHub could not be reached, if this machine needs a proxy to reach the 
internet, set one and try again": "无法连接 GitHub。如果本机需要代理才能访问外网,请先配置代理后重试。",
     "Go to writable demo site?": "跳转至可写演示站点?",
     "Half A Hour": "半小时",
     "Home": "首页",
@@ -398,6 +399,7 @@
     "Search": "搜索",
     "Select": "请选择",
     "Self": "自己",
+    "Set a proxy": "去配置代理",
     "Signed out successfully": "已退出登录",
     "Signing in...": "正在登录...",
     "Sites": "站点",
@@ -716,7 +718,7 @@
     "Max records": "最大记录条数",
     "Network and certificates": "网络与证书",
     "Outbound SOCKS5 proxy": "出站 SOCKS5 代理",
-    "Outbound SOCKS5 proxy hint": "形如 \"host:port\",用于 ACME 等出站流量。",
+    "Outbound SOCKS5 proxy hint": "形如 \"host:port\",用于升级下载、ACME 等出站流量。",
     "Page description": "Casbin Gateway 的全部配置项。修改会保存到数据库并立即生效。",
     "Pricing file": "价格文件",
     "Pricing file hint": "可选的 JSON 文件,按每百万 Token 的美元价格覆盖内置价目表。",
diff --git a/web/src/types.ts b/web/src/types.ts
index 8059294..499acb6 100644
--- a/web/src/types.ts
+++ b/web/src/types.ts
@@ -668,6 +668,8 @@ export interface UpdateStatus {
   total: number;
   target: string;
   error: string;
+  /** The failure was reaching GitHub, which a proxy can fix. */
+  network: boolean;
 }
 
 export interface VersionInfo {
@@ -679,6 +681,8 @@ export interface VersionInfo {
   blocked: string;
   /** A failed lookup, which leaves the current version worth showing anyway. 
*/
   checkError: string;
+  /** The lookup failed because GitHub could not be reached. */
+  checkNetwork: boolean;
   update: UpdateStatus;
   releaseUrl: string;
   installCommand: string;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to