mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
Add retry in resources.GetRemote for temporary HTTP errors
Fixes #11312
This commit is contained in:
parent
2c20fd557a
commit
a3d42a277d
3 changed files with 128 additions and 21 deletions
|
@ -36,7 +36,7 @@ func TestResourceChainBasic(t *testing.T) {
|
||||||
failIfHandler := func(h http.Handler) http.Handler {
|
failIfHandler := func(h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path == "/fail.jpg" {
|
if r.URL.Path == "/fail.jpg" {
|
||||||
http.Error(w, "{ msg: failed }", 500)
|
http.Error(w, "{ msg: failed }", 501)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.ServeHTTP(w, r)
|
h.ServeHTTP(w, r)
|
||||||
|
@ -116,8 +116,8 @@ FIT REMOTE: sunset_%[1]s.jpg|/sunset_%[1]s_hu59e56ffff1bc1d8d122b1403d34e039f_0_
|
||||||
REMOTE NOT FOUND: OK
|
REMOTE NOT FOUND: OK
|
||||||
LOCAL NOT FOUND: OK
|
LOCAL NOT FOUND: OK
|
||||||
PRINT PROTOCOL ERROR DETAILS: Err: error calling resources.GetRemote: Get "gopher://example.org": unsupported protocol scheme "gopher"||
|
PRINT PROTOCOL ERROR DETAILS: Err: error calling resources.GetRemote: Get "gopher://example.org": unsupported protocol scheme "gopher"||
|
||||||
FAILED REMOTE ERROR DETAILS CONTENT: |failed to fetch remote resource: Internal Server Error|Body: { msg: failed }
|
FAILED REMOTE ERROR DETAILS CONTENT: |failed to fetch remote resource: Not Implemented|Body: { msg: failed }
|
||||||
|StatusCode: 500|ContentLength: 16|ContentType: text/plain; charset=utf-8|
|
|StatusCode: 501|ContentLength: 16|ContentType: text/plain; charset=utf-8|
|
||||||
|
|
||||||
|
|
||||||
`, identity.HashString(ts.URL+"/sunset.jpg", map[string]any{})))
|
`, identity.HashString(ts.URL+"/sunset.jpg", map[string]any{})))
|
||||||
|
|
|
@ -14,12 +14,17 @@
|
||||||
package create_test
|
package create_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/hugolib"
|
"github.com/gohugoio/hugo/hugolib"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetResourceHead(t *testing.T) {
|
func TestGetRemoteHead(t *testing.T) {
|
||||||
|
|
||||||
files := `
|
files := `
|
||||||
-- config.toml --
|
-- config.toml --
|
||||||
|
@ -57,3 +62,63 @@ func TestGetResourceHead(t *testing.T) {
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetRemoteRetry(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
temporaryHTTPCodes := []int{408, 429, 500, 502, 503, 504}
|
||||||
|
numPages := 30
|
||||||
|
|
||||||
|
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if rand.Intn(4) == 0 {
|
||||||
|
w.WriteHeader(temporaryHTTPCodes[rand.Intn(len(temporaryHTTPCodes))])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Add("Content-Type", "text/plain")
|
||||||
|
w.Write([]byte("Response for " + r.URL.Path + "."))
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(handler))
|
||||||
|
t.Cleanup(func() { srv.Close() })
|
||||||
|
|
||||||
|
files := `
|
||||||
|
-- hugo.toml --
|
||||||
|
disableKinds = ["home", "taxonomy", "term"]
|
||||||
|
[security]
|
||||||
|
[security.http]
|
||||||
|
urls = ['.*']
|
||||||
|
mediaTypes = ['text/plain']
|
||||||
|
-- layouts/_default/single.html --
|
||||||
|
{{ $url := printf "%s%s" "URL" .RelPermalink}}
|
||||||
|
{{ $opts := dict }}
|
||||||
|
{{ with resources.GetRemote $url $opts }}
|
||||||
|
{{ with .Err }}
|
||||||
|
{{ errorf "Unable to get remote resource: %s" . }}
|
||||||
|
{{ else }}
|
||||||
|
Content: {{ .Content }}
|
||||||
|
{{ end }}
|
||||||
|
{{ else }}
|
||||||
|
{{ errorf "Unable to get remote resource: %s" $url }}
|
||||||
|
{{ end }}
|
||||||
|
`
|
||||||
|
|
||||||
|
for i := 0; i < numPages; i++ {
|
||||||
|
files += fmt.Sprintf("-- content/post/p%d.md --\n", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
files = strings.ReplaceAll(files, "URL", srv.URL)
|
||||||
|
|
||||||
|
b := hugolib.NewIntegrationTestBuilder(
|
||||||
|
hugolib.IntegrationTestConfig{
|
||||||
|
T: t,
|
||||||
|
TxtarString: files,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
b.Build()
|
||||||
|
|
||||||
|
for i := 0; i < numPages; i++ {
|
||||||
|
b.AssertFileContent(fmt.Sprintf("public/post/p%d/index.html", i), fmt.Sprintf("Content: Response for /post/p%d/.", i))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math/rand"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
|
@ -25,6 +26,7 @@ import (
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/common/hugio"
|
"github.com/gohugoio/hugo/common/hugio"
|
||||||
"github.com/gohugoio/hugo/common/maps"
|
"github.com/gohugoio/hugo/common/maps"
|
||||||
|
@ -83,6 +85,15 @@ func toHTTPError(err error, res *http.Response, readBody bool) *HTTPError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var temporaryHTTPStatusCodes = map[int]bool{
|
||||||
|
408: true,
|
||||||
|
429: true,
|
||||||
|
500: true,
|
||||||
|
502: true,
|
||||||
|
503: true,
|
||||||
|
504: true,
|
||||||
|
}
|
||||||
|
|
||||||
// FromRemote expects one or n-parts of a URL to a resource
|
// FromRemote expects one or n-parts of a URL to a resource
|
||||||
// If you provide multiple parts they will be joined together to the final URL.
|
// If you provide multiple parts they will be joined together to the final URL.
|
||||||
func (c *Client) FromRemote(uri string, optionsm map[string]any) (resource.Resource, error) {
|
func (c *Client) FromRemote(uri string, optionsm map[string]any) (resource.Resource, error) {
|
||||||
|
@ -108,30 +119,61 @@ func (c *Client) FromRemote(uri string, optionsm map[string]any) (resource.Resou
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := options.NewRequest(uri)
|
var (
|
||||||
if err != nil {
|
start time.Time
|
||||||
return nil, fmt.Errorf("failed to create request for resource %s: %w", uri, err)
|
nextSleep = time.Duration((rand.Intn(1000) + 100)) * time.Millisecond
|
||||||
}
|
nextSleepLimit = time.Duration(5) * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
res, err := c.httpClient.Do(req)
|
for {
|
||||||
if err != nil {
|
b, retry, err := func() ([]byte, bool, error) {
|
||||||
return nil, err
|
req, err := options.NewRequest(uri)
|
||||||
}
|
if err != nil {
|
||||||
defer res.Body.Close()
|
return nil, false, fmt.Errorf("failed to create request for resource %s: %w", uri, err)
|
||||||
|
}
|
||||||
|
|
||||||
httpResponse, err := httputil.DumpResponse(res, true)
|
res, err := c.httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, toHTTPError(err, res, !isHeadMethod)
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != http.StatusNotFound {
|
if res.StatusCode != http.StatusNotFound {
|
||||||
if res.StatusCode < 200 || res.StatusCode > 299 {
|
if res.StatusCode < 200 || res.StatusCode > 299 {
|
||||||
return nil, toHTTPError(fmt.Errorf("failed to fetch remote resource: %s", http.StatusText(res.StatusCode)), res, !isHeadMethod)
|
return nil, temporaryHTTPStatusCodes[res.StatusCode], toHTTPError(fmt.Errorf("failed to fetch remote resource: %s", http.StatusText(res.StatusCode)), res, !isHeadMethod)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := httputil.DumpResponse(res, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, toHTTPError(err, res, !isHeadMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, false, nil
|
||||||
|
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if retry {
|
||||||
|
if start.IsZero() {
|
||||||
|
start = time.Now()
|
||||||
|
} else if d := time.Since(start) + nextSleep; d >= c.rs.Cfg.Timeout() {
|
||||||
|
return nil, fmt.Errorf("timeout (configured to %s) fetching remote resource %s: last error: %w", c.rs.Cfg.Timeout(), uri, err)
|
||||||
|
}
|
||||||
|
time.Sleep(nextSleep)
|
||||||
|
if nextSleep < nextSleepLimit {
|
||||||
|
nextSleep *= 2
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return hugio.ToReadCloser(bytes.NewReader(b)), nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return hugio.ToReadCloser(bytes.NewReader(httpResponse)), nil
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
Loading…
Reference in a new issue