Repository: gohugoio/hugo (language: go)

Issue:
 Error calling partial: context canceled



Discussion / hints:
OK, I see what's happening here:

https://github.com/onweru/compose/blob/master/layouts/partials/functions/getScript.html

* The above returns a lazily evaluated resource
* Which means that the partial timeout context is cancelled before the template in execute template gets cancelled.
* And in this case, the ExecuteAsTemplate also includes a partial ... and boom.

I'll try to create a test for this, but the fix would be to make sure that the execution of ExecuteAsTemplate don't use an old (cancelled context).


Fix patch:
diff --git a/lazy/init.go b/lazy/init.go
index 4de2a83f74f..bfb9c4e0791 100644
--- a/lazy/init.go
+++ b/lazy/init.go
@@ -180,14 +180,15 @@ func (ini *Init) checkDone() {
 }
 
 func (ini *Init) withTimeout(ctx context.Context, timeout time.Duration, f func(ctx context.Context) (any, error)) (any, error) {
-	ctx, cancel := context.WithTimeout(ctx, timeout)
+	// Create a new context with a timeout not connected to the incoming context.
+	waitCtx, cancel := context.WithTimeout(context.Background(), timeout)
 	defer cancel()
 	c := make(chan verr, 1)
 
 	go func() {
 		v, err := f(ctx)
 		select {
-		case <-ctx.Done():
+		case <-waitCtx.Done():
 			return
 		default:
 			c <- verr{v: v, err: err}
@@ -195,7 +196,7 @@ func (ini *Init) withTimeout(ctx context.Context, timeout time.Duration, f func(
 	}()
 
 	select {
-	case <-ctx.Done():
+	case <-waitCtx.Done():
 		return nil, errors.New("timed out initializing value. You may have a circular loop in a shortcode, or your site may have resources that take longer to build than the `timeout` limit in your Hugo config file.")
 	case ve := <-c:
 		return ve.v, ve.err
diff --git a/resources/transform.go b/resources/transform.go
index 4ab51485e1b..fe438e36614 100644
--- a/resources/transform.go
+++ b/resources/transform.go
@@ -164,12 +164,12 @@ type resourceAdapter struct {
 	*resourceAdapterInner
 }
 
-func (r *resourceAdapter) Content(context.Context) (any, error) {
+func (r *resourceAdapter) Content(ctx context.Context) (any, error) {
 	r.init(false, true)
 	if r.transformationsErr != nil {
 		return nil, r.transformationsErr
 	}
-	return r.target.Content(context.Background())
+	return r.target.Content(ctx)
 }
 
 func (r *resourceAdapter) Err() resource.ResourceError {
diff --git a/tpl/partials/partials.go b/tpl/partials/partials.go
index d9a826aa41b..26ce0f5c66d 100644
--- a/tpl/partials/partials.go
+++ b/tpl/partials/partials.go
@@ -129,7 +129,10 @@ func (ns *Namespace) Include(ctx context.Context, name string, contextList ...an
 }
 
 func (ns *Namespace) includWithTimeout(ctx context.Context, name string, dataList ...any) includeResult {
-	ctx, cancel := context.WithTimeout(ctx, ns.deps.Timeout)
+	// There are situation where the ctx we pass on to the partial lives longer than
+	// the partial itself. For example, when the partial returns the result from reosurces.ExecuteAsTemplate.
+	// Because of that, create a completely new context here.
+	timeoutCtx, cancel := context.WithTimeout(context.Background(), ns.deps.Timeout)
 	defer cancel()
 
 	res := make(chan includeResult, 1)
@@ -141,8 +144,8 @@ func (ns *Namespace) includWithTimeout(ctx context.Context, name string, dataLis
 	select {
 	case r := <-res:
 		return r
-	case <-ctx.Done():
-		err := ctx.Err()
+	case <-timeoutCtx.Done():
+		err := timeoutCtx.Err()
 		if err == context.DeadlineExceeded {
 			err = fmt.Errorf("partial %q timed out after %s. This is most likely due to infinite recursion. If this is just a slow template, you can try to increase the 'timeout' config setting.", name, ns.deps.Timeout)
 		}


Test patch:
diff --git a/lazy/init_test.go b/lazy/init_test.go
index 499ea2cce7b..efc329d797e 100644
--- a/lazy/init_test.go
+++ b/lazy/init_test.go
@@ -126,12 +126,6 @@ func TestInitAddWithTimeoutTimeout(t *testing.T) {
 
 	init := New().AddWithTimeout(100*time.Millisecond, func(ctx context.Context) (any, error) {
 		time.Sleep(500 * time.Millisecond)
-		select {
-		case <-ctx.Done():
-			return nil, nil
-		default:
-		}
-		t.Fatal("slept")
 		return nil, nil
 	})
 
diff --git a/tpl/partials/integration_test.go b/tpl/partials/integration_test.go
index fcebe6c05d8..3dbaf2ce41e 100644
--- a/tpl/partials/integration_test.go
+++ b/tpl/partials/integration_test.go
@@ -324,3 +324,31 @@ timeout = '200ms'
 	b.Assert(err.Error(), qt.Contains, "timed out")
 
 }
+
+// See Issue #10789
+func TestReturnExecuteFromTemplateInPartial(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- config.toml --
+baseURL = 'http://example.com/'
+-- layouts/index.html --
+{{ $r :=  partial "foo" }}
+FOO:{{ $r.Content }}
+-- layouts/partials/foo.html --
+{{ $r := §§{{ partial "bar" }}§§ | resources.FromString "bar.html" | resources.ExecuteAsTemplate "bar.html" . }}
+{{ return $r }}
+-- layouts/partials/bar.html --
+BAR
+  `
+
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+		},
+	).Build()
+
+	b.AssertFileContent("public/index.html", "OO:BAR")
+
+}