Skip to content

resources.GetRemote: context canceled when fetching many remote images from the same host #14611

Closed

Description

@powerfullz

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

  1. self-assigned this
    on Mar 10, 2026
  2. added this to the v0.158.0 milestone on Mar 10, 2026
  3. powerfullz commented on Mar 10, 2026

    @powerfullz
    Author

    (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 the timeout dict option. I made a testing methodology mistake in my initial report. Sorry for the initial noise!)

  4. bep commented on Mar 10, 2026

    @bep
    Member

    The ONLY bug is the premature defer cancel() when using the timeout dict option.

    Yea, I see that now. Thanks for the bug report, I will fix it and push a release in a day or two.

  5. added 2 commits that reference this issue on Mar 10, 2026
    ea4dbfc
    eb3934a
  6. added a commit that references this issue on Mar 10, 2026
    842d8f1
  7. github-actions commented on Apr 1, 2026

    @github-actions

    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.

  8. github-actions commented on Apr 1, 2026

    @github-actions

    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.

  9. github-actions commented on Apr 1, 2026

    @github-actions

    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.

  10. locked as resolved and limited conversation to collaborators on Apr 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions