Repository navigation
resources.GetRemote: context canceled when fetching many remote images from the same host #14611
Description
Description
When using resources.GetRemote to fetch remote images with the per-request timeout option, requests fail with context canceled errors.
Environment
- Hugo version: v0.157.0+extended+withdeploy linux/amd64
- OS: Linux
Steps to Reproduce
Create a Hugo site that uses resources.GetRemote to fetch a remote image with the timeout parameter:
{{ with try (resources.GetRemote $url (dict "timeout" "20s")) }}
{{ with .Err }}
{{ warnf "Failed to fetch remote resource %q: %s" $url . }}
{{ else with .Value }}
{{ $resource = . }}
{{ end }}
{{ end }}Run hugo --gc --minify.
Actual Behavior
The remote image fetches fail with:
WARN Failed to fetch remote resource "https://example.com/image.webp": error calling GetRemote:
failed to read remote resource "https://example.com/image.webp": context canceled
The error is context canceled, not context deadline exceeded.
Root Cause Analysis
After reading the Hugo v0.157.0 source code (resources/resource_factories/create/remote.go and related files), I identified the following issue:
defer cancel() fires prematurely in getRes closure
When the per-request timeout option is passed to resources.GetRemote, the code in FromRemote creates a context with timeout inside the getRes closure:
getRes := func() (*http.Response, error) {
ctx := context.Background()
if perRequestTimeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, perRequestTimeout)
defer cancel() // <-- fires when getRes() returns, not when FromRemote returns
}
req, err := options.NewRequest(uri)
req = req.WithContext(ctx)
return c.httpClient.Do(req)
}
res, err := getRes()
// ... later:
body, err = io.ReadAll(res.Body) // <-- reads body AFTER context is already canceled!The defer cancel() executes when getRes() returns (after receiving HTTP response headers), but io.ReadAll(res.Body) runs in the outer FromRemote scope after getRes has returned. By that point, the context is already canceled, causing the body read to fail.
Suggested Fixes
Move the defer cancel() out of the getRes closure, or restructure the code so that io.ReadAll(res.Body) runs inside the closure before the context is canceled:
// Option A: Move cancel to FromRemote scope
var cancel context.CancelFunc
getRes := func() (*http.Response, error) {
ctx := context.Background()
if perRequestTimeout > 0 {
ctx, cancel = context.WithTimeout(ctx, perRequestTimeout)
}
// ...
}
res, err := getRes()
if cancel != nil {
defer cancel()
}Or:
// Option B: Read body inside the closure
getRes := func() ([]byte, *http.Response, error) {
// ... same as before ...
defer cancel()
resp, err := c.httpClient.Do(req)
if err != nil { return nil, nil, err }
body, err := io.ReadAll(resp.Body)
return body, resp, err
}(Note: In my original report, I hypothesized this had to do with connection pool exhaustion in http.DefaultTransport under heavy load without the timeout parameter. That was my mistake due to an error in my testing methodology. Hugo handles 90+ concurrent image fetches gracefully without the timeout dict. The only actual bug is this premature defer cancel().)
Activity
powerfullz commentedon Mar 10, 2026
(Note: I updated the main issue body above to remove my initial incorrect hypothesis about connection pool exhaustion. The ONLY bug is the premature
defer cancel()when using thetimeoutdict option. I made a testing methodology mistake in my initial report. Sorry for the initial noise!)resources: Fix context canceled on GetRemote with per-request timeout
bep commentedon Mar 10, 2026
Yea, I see that now. Thanks for the bug report, I will fix it and push a release in a day or two.
resources: Fix context canceled on GetRemote with per-request timeout
resources: Fix context canceled on GetRemote with per-request timeout
resources: Fix context canceled on GetRemote with per-request timeout
github-actions commentedon Apr 1, 2026
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
github-actions commentedon Apr 1, 2026
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
github-actions commentedon Apr 1, 2026
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.