Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 842d8f1

Browse files
bepclaude
andcommitted
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 <[email protected]>
1 parent c47ec23 commit 842d8f1

2 files changed

Lines changed: 70 additions & 8 deletions

File tree

resources/resource_factories/create/create_integration_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,50 @@ mediaTypes = ['text/plain']
220220
// The per-request timeout of 200ms should fire well before the global 30s timeout.
221221
b.AssertFileContent("public/index.html", "Err:")
222222
}
223+
224+
// Issue 14611.
225+
func TestGetRemotePerRequestTimeoutBodyRead(t *testing.T) {
226+
t.Parallel()
227+
228+
// Server sends headers immediately but streams body with a delay.
229+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
230+
w.Header().Add("Content-Type", "text/plain")
231+
w.WriteHeader(200)
232+
if f, ok := w.(http.Flusher); ok {
233+
f.Flush()
234+
}
235+
time.Sleep(300 * time.Millisecond)
236+
w.Write([]byte("Hello from remote."))
237+
}))
238+
t.Cleanup(func() { srv.Close() })
239+
240+
files := `
241+
-- hugo.toml --
242+
timeout = "30s"
243+
[security]
244+
[security.http]
245+
urls = ['.*']
246+
mediaTypes = ['text/plain']
247+
-- layouts/home.html --
248+
{{ $url := "URL" }}
249+
{{ $opts := dict "timeout" "5s" }}
250+
{{ with try (resources.GetRemote $url $opts) }}
251+
{{ with .Err }}
252+
Err: {{ . }}
253+
{{ else with .Value }}
254+
Content: {{ .Content }}
255+
{{ end }}
256+
{{ end }}
257+
`
258+
files = strings.ReplaceAll(files, "URL", srv.URL)
259+
260+
b := hugolib.NewIntegrationTestBuilder(
261+
hugolib.IntegrationTestConfig{
262+
T: t,
263+
TxtarString: files,
264+
},
265+
)
266+
b.Build()
267+
268+
b.AssertFileContent("public/index.html", "Content: Hello from remote.")
269+
}

resources/resource_factories/create/remote.go

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ var temporaryHTTPStatusCodes = map[int]bool{
110110
504: true,
111111
}
112112

113-
func (c *Client) configurePollingIfEnabled(uri, optionsKey string, getRes func() (*http.Response, error)) {
113+
func (c *Client) configurePollingIfEnabled(uri, optionsKey string, getRes func() (*http.Response, context.CancelFunc, error)) {
114114
if c.remoteResourceChecker == nil {
115115
return
116116
}
@@ -137,7 +137,10 @@ func (c *Client) configurePollingIfEnabled(uri, optionsKey string, getRes func()
137137
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)
138138
}()
139139
// TODO(bep) figure out a ways to remove unused tasks.
140-
res, err := getRes()
140+
res, cancel, err := getRes()
141+
if cancel != nil {
142+
defer cancel()
143+
}
141144
if err != nil {
142145
return pollingConfig.Config.High, err
143146
}
@@ -210,26 +213,38 @@ func (c *Client) FromRemote(uri string, optionsm map[string]any) (resource.Resou
210213
return nil, err
211214
}
212215

213-
getRes := func() (*http.Response, error) {
216+
getRes := func() (*http.Response, context.CancelFunc, error) {
214217
ctx := context.Background()
218+
var cancel context.CancelFunc
215219
if perRequestTimeout > 0 {
216-
var cancel context.CancelFunc
217220
ctx, cancel = context.WithTimeout(ctx, perRequestTimeout)
218-
defer cancel()
219221
}
220222
ctx = c.resourceIDDispatcher.Set(ctx, filecacheKey)
221223

222224
req, err := options.NewRequest(uri)
223225
if err != nil {
224-
return nil, fmt.Errorf("failed to create request for resource %s: %w", uri, err)
226+
if cancel != nil {
227+
cancel()
228+
}
229+
return nil, nil, fmt.Errorf("failed to create request for resource %s: %w", uri, err)
225230
}
226231

227232
req = req.WithContext(ctx)
228233

229-
return c.httpClient.Do(req)
234+
resp, err := c.httpClient.Do(req)
235+
if err != nil {
236+
if cancel != nil {
237+
cancel()
238+
}
239+
return nil, nil, err
240+
}
241+
return resp, cancel, nil
230242
}
231243

232-
res, err := getRes()
244+
res, cancel, err := getRes()
245+
if cancel != nil {
246+
defer cancel()
247+
}
233248
if err != nil {
234249
return nil, err
235250
}

0 commit comments

Comments
 (0)