From 3bbeb5688c4452a336af07be1e8cec44dac7d6ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Sat, 4 Mar 2023 18:08:29 +0100 Subject: [PATCH] Fix "context canceled" with partial Make sure the context used for timeouts isn't created based on the incoming context, as we have cases where this can cancel the context prematurely. Fixes #10789 --- lazy/init.go | 7 ++++--- lazy/init_test.go | 6 ------ resources/transform.go | 4 ++-- tpl/partials/integration_test.go | 28 ++++++++++++++++++++++++++++ tpl/partials/partials.go | 9 ++++++--- 5 files changed, 40 insertions(+), 14 deletions(-) diff --git a/lazy/init.go b/lazy/init.go index 4de2a83f7..bfb9c4e07 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/lazy/init_test.go b/lazy/init_test.go index 499ea2cce..efc329d79 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/resources/transform.go b/resources/transform.go index 4ab51485e..fe438e366 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/integration_test.go b/tpl/partials/integration_test.go index fcebe6c05..3dbaf2ce4 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") + +} diff --git a/tpl/partials/partials.go b/tpl/partials/partials.go index d9a826aa4..26ce0f5c6 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) }