mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 15:28:54 +00:00
tpl/templates: Reject Defer inside partialCached
A partial's rendered output (placeholder included) is cached by partialCached across rebuilds, but BuildState.DeferredExecutions is reset every stage. On a fast-render rebuild the cached string replays the placeholder while doDefer is not called this build, leaving executeDeferredTemplates to panic with "deferred execution with id ... not found". Mark the ctx inside IncludeCached's body execution and have Defer return a clear error if it sees the flag. Catches transitive cases (partialCached -> partial -> Defer) via ctx propagation. Defer in baseof.html and in a plain partial is unaffected. Fixes #13492 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -234,6 +234,8 @@ func (ns *Namespace) IncludeCached(ctx context.Context, name string, context any
|
|||||||
depsManagerShared = identity.NewManager()
|
depsManagerShared = identity.NewManager()
|
||||||
ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, depsManagerShared.(identity.DependencyManagerScopedProvider))
|
ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, depsManagerShared.(identity.DependencyManagerScopedProvider))
|
||||||
}
|
}
|
||||||
|
// Mark the ctx so templates.Defer can reject being called from a cached body.
|
||||||
|
ctx = tpl.Context.IsInPartialCached.Set(ctx, true)
|
||||||
r := ns.doInclude(ctx, keyString, ti, context)
|
r := ns.doInclude(ctx, keyString, ti, context)
|
||||||
if ns.deps.Conf.Watching() {
|
if ns.deps.Conf.Watching() {
|
||||||
r.mangager = depsManagerShared
|
r.mangager = depsManagerShared
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ const (
|
|||||||
contextKeyIsInGoldmark
|
contextKeyIsInGoldmark
|
||||||
cntextKeyCurrentTemplateInfo
|
cntextKeyCurrentTemplateInfo
|
||||||
contextKeyPartialDecoratorIDStack
|
contextKeyPartialDecoratorIDStack
|
||||||
|
contextKeyIsInPartialCached
|
||||||
)
|
)
|
||||||
|
|
||||||
// Context manages values passed in the context to templates.
|
// Context manages values passed in the context to templates.
|
||||||
@@ -66,6 +67,7 @@ var Context = struct {
|
|||||||
IsInGoldmark contexthelpers.ContextDispatcher[bool]
|
IsInGoldmark contexthelpers.ContextDispatcher[bool]
|
||||||
CurrentTemplate contexthelpers.ContextDispatcher[*CurrentTemplateInfo]
|
CurrentTemplate contexthelpers.ContextDispatcher[*CurrentTemplateInfo]
|
||||||
PartialDecoratorIDStack contexthelpers.ContextDispatcher[*collections.Stack[*StringBool]]
|
PartialDecoratorIDStack contexthelpers.ContextDispatcher[*collections.Stack[*StringBool]]
|
||||||
|
IsInPartialCached contexthelpers.ContextDispatcher[bool]
|
||||||
}{
|
}{
|
||||||
DependencyManagerScopedProvider: contexthelpers.NewContextDispatcher[identity.DependencyManagerScopedProvider](contextKeyDependencyManagerScopedProvider),
|
DependencyManagerScopedProvider: contexthelpers.NewContextDispatcher[identity.DependencyManagerScopedProvider](contextKeyDependencyManagerScopedProvider),
|
||||||
DependencyScope: contexthelpers.NewContextDispatcher[int](contextKeyDependencyScope),
|
DependencyScope: contexthelpers.NewContextDispatcher[int](contextKeyDependencyScope),
|
||||||
@@ -73,6 +75,7 @@ var Context = struct {
|
|||||||
IsInGoldmark: contexthelpers.NewContextDispatcher[bool](contextKeyIsInGoldmark),
|
IsInGoldmark: contexthelpers.NewContextDispatcher[bool](contextKeyIsInGoldmark),
|
||||||
CurrentTemplate: contexthelpers.NewContextDispatcher[*CurrentTemplateInfo](cntextKeyCurrentTemplateInfo),
|
CurrentTemplate: contexthelpers.NewContextDispatcher[*CurrentTemplateInfo](cntextKeyCurrentTemplateInfo),
|
||||||
PartialDecoratorIDStack: contexthelpers.NewContextDispatcher[*collections.Stack[*StringBool]](contextKeyPartialDecoratorIDStack),
|
PartialDecoratorIDStack: contexthelpers.NewContextDispatcher[*collections.Stack[*StringBool]](contextKeyPartialDecoratorIDStack),
|
||||||
|
IsInPartialCached: contexthelpers.NewContextDispatcher[bool](contextKeyIsInPartialCached),
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -322,3 +322,48 @@ End.
|
|||||||
|
|
||||||
b.AssertFileContent("public/index.html", "Home.", "Defer 1", "Defer 2", "Defer 3", "End.")
|
b.AssertFileContent("public/index.html", "Home.", "Defer 1", "Defer 2", "Defer 3", "End.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// See issue 13492.
|
||||||
|
func TestDeferInsidePartialCachedShouldFail(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
files := `
|
||||||
|
-- hugo.toml --
|
||||||
|
-- layouts/home.html --
|
||||||
|
Home.
|
||||||
|
{{ partialCached "css.html" . }}
|
||||||
|
-- layouts/partials/css.html --
|
||||||
|
{{ with (templates.Defer (dict "key" "global")) }}
|
||||||
|
Defer
|
||||||
|
{{ end }}
|
||||||
|
`
|
||||||
|
|
||||||
|
b, err := hugolib.TestE(t, files)
|
||||||
|
|
||||||
|
b.Assert(err, qt.Not(qt.IsNil))
|
||||||
|
b.Assert(err.Error(), qt.Contains, "templates.Defer cannot be used inside a partialCached partial")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifies that the partialCached ctx flag propagates through a regular partial call.
|
||||||
|
// See issue 13492.
|
||||||
|
func TestDeferTransitivelyInsidePartialCachedShouldFail(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
files := `
|
||||||
|
-- hugo.toml --
|
||||||
|
-- layouts/home.html --
|
||||||
|
Home.
|
||||||
|
{{ partialCached "outer.html" . }}
|
||||||
|
-- layouts/partials/outer.html --
|
||||||
|
{{ partial "inner.html" . }}
|
||||||
|
-- layouts/partials/inner.html --
|
||||||
|
{{ with (templates.Defer (dict "key" "global")) }}
|
||||||
|
Defer
|
||||||
|
{{ end }}
|
||||||
|
`
|
||||||
|
|
||||||
|
b, err := hugolib.TestE(t, files)
|
||||||
|
|
||||||
|
b.Assert(err, qt.Not(qt.IsNil))
|
||||||
|
b.Assert(err.Error(), qt.Contains, "templates.Defer cannot be used inside a partialCached partial")
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,11 +53,18 @@ func (ns *Namespace) Exists(name string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Defer defers the execution of a template block.
|
// Defer defers the execution of a template block.
|
||||||
func (ns *Namespace) Defer(args ...any) (bool, error) {
|
func (ns *Namespace) Defer(ctx context.Context, args ...any) (bool, error) {
|
||||||
// Prevent defer from being used in content adapters,
|
// Prevent defer from being used in content adapters,
|
||||||
// that just doesn't work.
|
// that just doesn't work.
|
||||||
ns.deps.Site.CheckReady()
|
ns.deps.Site.CheckReady()
|
||||||
|
|
||||||
|
// Reject use inside a partialCached body: the cached output retains the
|
||||||
|
// deferred placeholder across rebuilds while the executions map is reset,
|
||||||
|
// which used to cause a panic in executeDeferredTemplates. See issue #13492.
|
||||||
|
if tpl.Context.IsInPartialCached.Get(ctx) {
|
||||||
|
return false, fmt.Errorf("templates.Defer cannot be used inside a partialCached partial; use partial instead, or move templates.Defer to the calling template")
|
||||||
|
}
|
||||||
|
|
||||||
if len(args) != 0 {
|
if len(args) != 0 {
|
||||||
return false, fmt.Errorf("Defer does not take any arguments")
|
return false, fmt.Errorf("Defer does not take any arguments")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user