Skip to content

Commit 69d3a34

Browse files
author
Jay Conrod
committed
cmd/go: add support for GOPROXY fallback on unexpected errors
URLs in GOPROXY may now be separated with commas (,) or pipes (|). If a request to a proxy fails with any error (including connection errors and timeouts) and the proxy URL is followed by a pipe, the go command will try the request with the next proxy in the list. If the proxy is followed by a comma, the go command will only try the next proxy if the error a 404 or 410 HTTP response. The go command will determine how to connect to the checksum database using the same logic. Before accessing the checksum database, the go command sends a request to <proxyURL>/sumdb/<sumdb-name>/supported. If a proxy responds with 404 or 410, or if any other error occurs and the proxy URL in GOPROXY is followed by a pipe, the go command will try the request with the next proxy. If all proxies respond with 404 or 410 or are configured to fall back on errors, the go command will connect to the checksum database directly. This CL does not change the default value or meaning of GOPROXY. Fixes #37367 Change-Id: If53152ec1c3282c67d4909818b666af58884fb2c Reviewed-on: https://go-review.googlesource.com/c/go/+/223257 Run-TryBot: Jay Conrod <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Bryan C. Mills <[email protected]>
1 parent 8cb865c commit 69d3a34

File tree

7 files changed

+174
-96
lines changed

7 files changed

+174
-96
lines changed

Diff for: doc/go1.15.html

+12
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ <h2 id="tools">Tools</h2>
4343

4444
<h3 id="go-command">Go command</h3>
4545

46+
<p><!-- golang.org/issue/37367 -->
47+
The <code>GOPROXY</code> environment variable now supports skipping proxies
48+
that return errors. Proxy URLs may now be separated with either commas
49+
(<code>,</code>) or pipe characters (<code>|</code>). If a proxy URL is
50+
followed by a comma, the <code>go</code> command will only try the next proxy
51+
in the list after a 404 or 410 HTTP response. If a proxy URL is followed by a
52+
pipe character, the <code>go</code> command will try the next proxy in the
53+
list after any error. Note that the default value of <code>GOPROXY</code>
54+
remains <code>https://proxy.golang.org,direct</code>, which does not fall
55+
back to <code>direct</code> in case of errors.
56+
</p>
57+
4658
<p>
4759
TODO
4860
</p>

Diff for: src/cmd/go/alldocs.go

+9-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: src/cmd/go/internal/modfetch/proxy.go

+74-35
Original file line numberDiff line numberDiff line change
@@ -101,27 +101,51 @@ cached module versions with GOPROXY=https://example.com/proxy.
101101

102102
var proxyOnce struct {
103103
sync.Once
104-
list []string
104+
list []proxySpec
105105
err error
106106
}
107107

108-
func proxyURLs() ([]string, error) {
108+
type proxySpec struct {
109+
// url is the proxy URL or one of "off", "direct", "noproxy".
110+
url string
111+
112+
// fallBackOnError is true if a request should be attempted on the next proxy
113+
// in the list after any error from this proxy. If fallBackOnError is false,
114+
// the request will only be attempted on the next proxy if the error is
115+
// equivalent to os.ErrNotFound, which is true for 404 and 410 responses.
116+
fallBackOnError bool
117+
}
118+
119+
func proxyList() ([]proxySpec, error) {
109120
proxyOnce.Do(func() {
110121
if cfg.GONOPROXY != "" && cfg.GOPROXY != "direct" {
111-
proxyOnce.list = append(proxyOnce.list, "noproxy")
122+
proxyOnce.list = append(proxyOnce.list, proxySpec{url: "noproxy"})
112123
}
113-
for _, proxyURL := range strings.Split(cfg.GOPROXY, ",") {
114-
proxyURL = strings.TrimSpace(proxyURL)
115-
if proxyURL == "" {
124+
125+
goproxy := cfg.GOPROXY
126+
for goproxy != "" {
127+
var url string
128+
fallBackOnError := false
129+
if i := strings.IndexAny(goproxy, ",|"); i >= 0 {
130+
url = goproxy[:i]
131+
fallBackOnError = goproxy[i] == '|'
132+
goproxy = goproxy[i+1:]
133+
} else {
134+
url = goproxy
135+
goproxy = ""
136+
}
137+
138+
url = strings.TrimSpace(url)
139+
if url == "" {
116140
continue
117141
}
118-
if proxyURL == "off" {
142+
if url == "off" {
119143
// "off" always fails hard, so can stop walking list.
120-
proxyOnce.list = append(proxyOnce.list, "off")
144+
proxyOnce.list = append(proxyOnce.list, proxySpec{url: "off"})
121145
break
122146
}
123-
if proxyURL == "direct" {
124-
proxyOnce.list = append(proxyOnce.list, "direct")
147+
if url == "direct" {
148+
proxyOnce.list = append(proxyOnce.list, proxySpec{url: "direct"})
125149
// For now, "direct" is the end of the line. We may decide to add some
126150
// sort of fallback behavior for them in the future, so ignore
127151
// subsequent entries for forward-compatibility.
@@ -131,63 +155,78 @@ func proxyURLs() ([]string, error) {
131155
// Single-word tokens are reserved for built-in behaviors, and anything
132156
// containing the string ":/" or matching an absolute file path must be a
133157
// complete URL. For all other paths, implicitly add "https://".
134-
if strings.ContainsAny(proxyURL, ".:/") && !strings.Contains(proxyURL, ":/") && !filepath.IsAbs(proxyURL) && !path.IsAbs(proxyURL) {
135-
proxyURL = "https://" + proxyURL
158+
if strings.ContainsAny(url, ".:/") && !strings.Contains(url, ":/") && !filepath.IsAbs(url) && !path.IsAbs(url) {
159+
url = "https://" + url
136160
}
137161

138162
// Check that newProxyRepo accepts the URL.
139163
// It won't do anything with the path.
140-
_, err := newProxyRepo(proxyURL, "golang.org/x/text")
141-
if err != nil {
164+
if _, err := newProxyRepo(url, "golang.org/x/text"); err != nil {
142165
proxyOnce.err = err
143166
return
144167
}
145-
proxyOnce.list = append(proxyOnce.list, proxyURL)
168+
169+
proxyOnce.list = append(proxyOnce.list, proxySpec{
170+
url: url,
171+
fallBackOnError: fallBackOnError,
172+
})
146173
}
147174
})
148175

149176
return proxyOnce.list, proxyOnce.err
150177
}
151178

152179
// TryProxies iterates f over each configured proxy (including "noproxy" and
153-
// "direct" if applicable) until f returns an error that is not
154-
// equivalent to os.ErrNotExist.
180+
// "direct" if applicable) until f returns no error or until f returns an
181+
// error that is not equivalent to os.ErrNotExist on a proxy configured
182+
// not to fall back on errors.
155183
//
156184
// TryProxies then returns that final error.
157185
//
158186
// If GOPROXY is set to "off", TryProxies invokes f once with the argument
159187
// "off".
160188
func TryProxies(f func(proxy string) error) error {
161-
proxies, err := proxyURLs()
189+
proxies, err := proxyList()
162190
if err != nil {
163191
return err
164192
}
165193
if len(proxies) == 0 {
166194
return f("off")
167195
}
168196

169-
var lastAttemptErr error
197+
// We try to report the most helpful error to the user. "direct" and "noproxy"
198+
// errors are best, followed by proxy errors other than ErrNotExist, followed
199+
// by ErrNotExist. Note that errProxyOff, errNoproxy, and errUseProxy are
200+
// equivalent to ErrNotExist.
201+
const (
202+
notExistRank = iota
203+
proxyRank
204+
directRank
205+
)
206+
var bestErr error
207+
bestErrRank := notExistRank
170208
for _, proxy := range proxies {
171-
err = f(proxy)
172-
if !errors.Is(err, os.ErrNotExist) {
173-
lastAttemptErr = err
174-
break
209+
err := f(proxy.url)
210+
if err == nil {
211+
return nil
212+
}
213+
isNotExistErr := errors.Is(err, os.ErrNotExist)
214+
215+
if (proxy.url == "direct" || proxy.url == "noproxy") && !isNotExistErr {
216+
bestErr = err
217+
bestErrRank = directRank
218+
} else if bestErrRank <= proxyRank && !isNotExistErr {
219+
bestErr = err
220+
bestErrRank = proxyRank
221+
} else if bestErrRank == notExistRank {
222+
bestErr = err
175223
}
176224

177-
// The error indicates that the module does not exist.
178-
// In general we prefer to report the last such error,
179-
// because it indicates the error that occurs after all other
180-
// options have been exhausted.
181-
//
182-
// However, for modules in the NOPROXY list, the most useful error occurs
183-
// first (with proxy set to "noproxy"), and the subsequent errors are all
184-
// errNoProxy (which is not particularly helpful). Do not overwrite a more
185-
// useful error with errNoproxy.
186-
if lastAttemptErr == nil || !errors.Is(err, errNoproxy) {
187-
lastAttemptErr = err
225+
if !proxy.fallBackOnError && !isNotExistErr {
226+
break
188227
}
189228
}
190-
return lastAttemptErr
229+
return bestErr
191230
}
192231

193232
type proxyRepo struct {

Diff for: src/cmd/go/internal/modfetch/sumdb.go

+42-40
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"cmd/go/internal/lockedfile"
2727
"cmd/go/internal/str"
2828
"cmd/go/internal/web"
29+
2930
"golang.org/x/mod/module"
3031
"golang.org/x/mod/sumdb"
3132
"golang.org/x/mod/sumdb/note"
@@ -146,49 +147,50 @@ func (c *dbClient) initBase() {
146147
}
147148

148149
// Try proxies in turn until we find out how to connect to this database.
149-
urls, err := proxyURLs()
150-
if err != nil {
151-
c.baseErr = err
152-
return
153-
}
154-
for _, proxyURL := range urls {
155-
if proxyURL == "noproxy" {
156-
continue
157-
}
158-
if proxyURL == "direct" || proxyURL == "off" {
159-
break
160-
}
161-
proxy, err := url.Parse(proxyURL)
162-
if err != nil {
163-
c.baseErr = err
164-
return
165-
}
166-
// Quoting https://golang.org/design/25530-sumdb#proxying-a-checksum-database:
167-
//
168-
// Before accessing any checksum database URL using a proxy,
169-
// the proxy client should first fetch <proxyURL>/sumdb/<sumdb-name>/supported.
170-
// If that request returns a successful (HTTP 200) response, then the proxy supports
171-
// proxying checksum database requests. In that case, the client should use
172-
// the proxied access method only, never falling back to a direct connection to the database.
173-
// If the /sumdb/<sumdb-name>/supported check fails with a “not found” (HTTP 404)
174-
// or “gone” (HTTP 410) response, the proxy is unwilling to proxy the checksum database,
175-
// and the client should connect directly to the database.
176-
// Any other response is treated as the database being unavailable.
177-
_, err = web.GetBytes(web.Join(proxy, "sumdb/"+c.name+"/supported"))
178-
if err == nil {
150+
//
151+
// Before accessing any checksum database URL using a proxy, the proxy
152+
// client should first fetch <proxyURL>/sumdb/<sumdb-name>/supported.
153+
//
154+
// If that request returns a successful (HTTP 200) response, then the proxy
155+
// supports proxying checksum database requests. In that case, the client
156+
// should use the proxied access method only, never falling back to a direct
157+
// connection to the database.
158+
//
159+
// If the /sumdb/<sumdb-name>/supported check fails with a “not found” (HTTP
160+
// 404) or “gone” (HTTP 410) response, or if the proxy is configured to fall
161+
// back on errors, the client will try the next proxy. If there are no
162+
// proxies left or if the proxy is "direct" or "off", the client should
163+
// connect directly to that database.
164+
//
165+
// Any other response is treated as the database being unavailable.
166+
//
167+
// See https://golang.org/design/25530-sumdb#proxying-a-checksum-database.
168+
err := TryProxies(func(proxy string) error {
169+
switch proxy {
170+
case "noproxy":
171+
return errUseProxy
172+
case "direct", "off":
173+
return errProxyOff
174+
default:
175+
proxyURL, err := url.Parse(proxy)
176+
if err != nil {
177+
return err
178+
}
179+
if _, err := web.GetBytes(web.Join(proxyURL, "sumdb/"+c.name+"/supported")); err != nil {
180+
return err
181+
}
179182
// Success! This proxy will help us.
180-
c.base = web.Join(proxy, "sumdb/"+c.name)
181-
return
182-
}
183-
// If the proxy serves a non-404/410, give up.
184-
if !errors.Is(err, os.ErrNotExist) {
185-
c.baseErr = err
186-
return
183+
c.base = web.Join(proxyURL, "sumdb/"+c.name)
184+
return nil
187185
}
186+
})
187+
if errors.Is(err, os.ErrNotExist) {
188+
// No proxies, or all proxies failed (with 404, 410, or were were allowed
189+
// to fall back), or we reached an explicit "direct" or "off".
190+
c.base = c.direct
191+
} else if err != nil {
192+
c.baseErr = err
188193
}
189-
190-
// No proxies, or all proxies said 404, or we reached an explicit "direct".
191-
c.base = c.direct
192194
}
193195

194196
// ReadConfig reads the key from c.key

Diff for: src/cmd/go/internal/modload/help.go

+9-9
Original file line numberDiff line numberDiff line change
@@ -363,15 +363,15 @@ variable (see 'go help env'). The default setting for GOPROXY is
363363
Go module mirror run by Google and fall back to a direct connection
364364
if the proxy reports that it does not have the module (HTTP error 404 or 410).
365365
See https://proxy.golang.org/privacy for the service's privacy policy.
366-
If GOPROXY is set to the string "direct", downloads use a direct connection
367-
to source control servers. Setting GOPROXY to "off" disallows downloading
368-
modules from any source. Otherwise, GOPROXY is expected to be a comma-separated
369-
list of the URLs of module proxies, in which case the go command will fetch
370-
modules from those proxies. For each request, the go command tries each proxy
371-
in sequence, only moving to the next if the current proxy returns a 404 or 410
372-
HTTP response. The string "direct" may appear in the proxy list,
373-
to cause a direct connection to be attempted at that point in the search.
374-
Any proxies listed after "direct" are never consulted.
366+
367+
If GOPROXY is set to the string "direct", downloads use a direct connection to
368+
source control servers. Setting GOPROXY to "off" disallows downloading modules
369+
from any source. Otherwise, GOPROXY is expected to be list of module proxy URLs
370+
separated by either comma (,) or pipe (|) characters, which control error
371+
fallback behavior. For each request, the go command tries each proxy in
372+
sequence. If there is an error, the go command will try the next proxy in the
373+
list if the error is a 404 or 410 HTTP response or if the current proxy is
374+
followed by a pipe character, indicating it is safe to fall back on any error.
375375
376376
The GOPRIVATE and GONOPROXY environment variables allow bypassing
377377
the proxy for selected modules. See 'go help module-private' for details.

Diff for: src/cmd/go/testdata/script/mod_proxy_list.txt

+11-3
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,25 @@ stderr '404 Not Found'
1010
env GOPROXY=$proxy/404,$proxy/410,$proxy
1111
go get rsc.io/[email protected]
1212

13-
# get should not walk past other 4xx errors.
13+
# get should not walk past other 4xx errors if proxies are separated with ','.
1414
env GOPROXY=$proxy/403,$proxy
1515
! go get rsc.io/[email protected]
1616
stderr 'reading.*/403/rsc.io/.*: 403 Forbidden'
1717

18-
# get should not walk past non-4xx errors.
18+
# get should not walk past non-4xx errors if proxies are separated with ','.
1919
env GOPROXY=$proxy/500,$proxy
2020
! go get rsc.io/[email protected]
2121
stderr 'reading.*/500/rsc.io/.*: 500 Internal Server Error'
2222

23-
# get should return the final 404/410 if that's all we have.
23+
# get should walk past other 4xx errors if proxies are separated with '|'.
24+
env GOPROXY=$proxy/403|https://0.0.0.0|$proxy
25+
go get rsc.io/[email protected]
26+
27+
# get should walk past non-4xx errors if proxies are separated with '|'.
28+
env GOPROXY=$proxy/500|https://0.0.0.0|$proxy
29+
go get rsc.io/[email protected]
30+
31+
# get should return the final error if that's all we have.
2432
env GOPROXY=$proxy/404,$proxy/410
2533
! go get rsc.io/[email protected]
2634
stderr 'reading.*/410/rsc.io/.*: 410 Gone'

Diff for: src/cmd/go/testdata/script/mod_sumdb_proxy.txt

+17
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,22 @@ stderr '503 Service Unavailable'
4646
rm $GOPATH/pkg/mod/cache/download/sumdb
4747
rm go.sum
4848

49+
# the error from the last attempted proxy should be returned.
50+
cp go.mod.orig go.mod
51+
env GOSUMDB=$sumdb
52+
env GOPROXY=$proxy/sumdb-404,$proxy/sumdb-503
53+
! go get -d rsc.io/[email protected]
54+
stderr '503 Service Unavailable'
55+
rm $GOPATH/pkg/mod/cache/download/sumdb
56+
rm go.sum
57+
58+
# if proxies are separated with '|', fallback is allowed on any error.
59+
cp go.mod.orig go.mod
60+
env GOSUMDB=$sumdb
61+
env GOPROXY=$proxy/sumdb-503|https://0.0.0.0|$proxy
62+
go get -d rsc.io/[email protected]
63+
rm $GOPATH/pkg/mod/cache/download/sumdb
64+
rm go.sum
65+
4966
-- go.mod.orig --
5067
module m

0 commit comments

Comments
 (0)