mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 07:18:54 +00:00
resources: Fix context canceled on GetRemote with per-request timeout
The per-request timeout context was cancelled via defer in the getRes closure, before io.ReadAll(res.Body) in the outer scope could read the response body. Fix this by returning the cancel function from getRes so each caller manages its own cancel lifecycle. Fixes #14611 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -220,3 +220,50 @@ mediaTypes = ['text/plain']
|
||||
// The per-request timeout of 200ms should fire well before the global 30s timeout.
|
||||
b.AssertFileContent("public/index.html", "Err:")
|
||||
}
|
||||
|
||||
// Issue 14611.
|
||||
func TestGetRemotePerRequestTimeoutBodyRead(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Server sends headers immediately but streams body with a delay.
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
w.WriteHeader(200)
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
w.Write([]byte("Hello from remote."))
|
||||
}))
|
||||
t.Cleanup(func() { srv.Close() })
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
timeout = "30s"
|
||||
[security]
|
||||
[security.http]
|
||||
urls = ['.*']
|
||||
mediaTypes = ['text/plain']
|
||||
-- layouts/home.html --
|
||||
{{ $url := "URL" }}
|
||||
{{ $opts := dict "timeout" "5s" }}
|
||||
{{ with try (resources.GetRemote $url $opts) }}
|
||||
{{ with .Err }}
|
||||
Err: {{ . }}
|
||||
{{ else with .Value }}
|
||||
Content: {{ .Content }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
`
|
||||
files = strings.ReplaceAll(files, "URL", srv.URL)
|
||||
|
||||
b := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
},
|
||||
)
|
||||
b.Build()
|
||||
|
||||
b.AssertFileContent("public/index.html", "Content: Hello from remote.")
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ var temporaryHTTPStatusCodes = map[int]bool{
|
||||
504: true,
|
||||
}
|
||||
|
||||
func (c *Client) configurePollingIfEnabled(uri, optionsKey string, getRes func() (*http.Response, error)) {
|
||||
func (c *Client) configurePollingIfEnabled(uri, optionsKey string, getRes func() (*http.Response, context.CancelFunc, error)) {
|
||||
if c.remoteResourceChecker == nil {
|
||||
return
|
||||
}
|
||||
@@ -137,7 +137,10 @@ func (c *Client) configurePollingIfEnabled(uri, optionsKey string, getRes func()
|
||||
c.rs.Logger.Debugf("Polled remote resource for changes in %13s. Interval: %4s (low: %4s high: %4s) resource: %q ", duration, interval, pollingConfig.Config.Low, pollingConfig.Config.High, uri)
|
||||
}()
|
||||
// TODO(bep) figure out a ways to remove unused tasks.
|
||||
res, err := getRes()
|
||||
res, cancel, err := getRes()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
if err != nil {
|
||||
return pollingConfig.Config.High, err
|
||||
}
|
||||
@@ -210,26 +213,38 @@ func (c *Client) FromRemote(uri string, optionsm map[string]any) (resource.Resou
|
||||
return nil, err
|
||||
}
|
||||
|
||||
getRes := func() (*http.Response, error) {
|
||||
getRes := func() (*http.Response, context.CancelFunc, error) {
|
||||
ctx := context.Background()
|
||||
var cancel context.CancelFunc
|
||||
if perRequestTimeout > 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, perRequestTimeout)
|
||||
defer cancel()
|
||||
}
|
||||
ctx = c.resourceIDDispatcher.Set(ctx, filecacheKey)
|
||||
|
||||
req, err := options.NewRequest(uri)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request for resource %s: %w", uri, err)
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
return nil, nil, fmt.Errorf("failed to create request for resource %s: %w", uri, err)
|
||||
}
|
||||
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
return c.httpClient.Do(req)
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
return resp, cancel, nil
|
||||
}
|
||||
|
||||
res, err := getRes()
|
||||
res, cancel, err := getRes()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user