From e28d6e2b8df6098ea27eeb16883cc175ea18d28f Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Tue, 12 May 2026 16:28:30 -0700 Subject: [PATCH] fix(variable): treat unexported struct fields as nil instead of panicking --- pongo2_issues_test.go | 23 +++++++++++++++++++++++ variable.go | 14 ++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/pongo2_issues_test.go b/pongo2_issues_test.go index b914288..8fb3ad5 100644 --- a/pongo2_issues_test.go +++ b/pongo2_issues_test.go @@ -2471,3 +2471,26 @@ func TestBugAndOrPrecedence(t *testing.T) { }) } } + +type issue204Item struct{ index string } + +func (i issue204Item) String() string { return "Item-" + i.index } + +// Issue #204: accessing an unexported struct field from a template must not panic. +func TestIssue204UnexportedStructField(t *testing.T) { + items := []interface{}{issue204Item{"0"}, issue204Item{"1"}, issue204Item{"2"}} + + for _, tplStr := range []string{ + `{% for item in items %}{% if item.index %}x{% endif %}{% endfor %}`, + `{% for item in items %}{{ item["index"] }}{% endfor %}`, + `{% for item in items %}{{ item.index }}{% endfor %}`, + } { + tpl, err := pongo2.FromString(tplStr) + if err != nil { + t.Fatalf("parse error for %q: %v", tplStr, err) + } + if _, err := tpl.Execute(pongo2.Context{"items": items}); err != nil { + t.Fatalf("execute error for %q: %v", tplStr, err) + } + } +} diff --git a/variable.go b/variable.go index ed83be2..0966552 100644 --- a/variable.go +++ b/variable.go @@ -381,7 +381,12 @@ func (vr *variableResolver) resolveIntIndex(current reflect.Value, part *variabl func (vr *variableResolver) resolveIdentifier(current reflect.Value, part *variablePart) (reflect.Value, bool, error) { switch current.Kind() { case reflect.Struct: - return current.FieldByName(part.s), false, nil + field := current.FieldByName(part.s) + if field.IsValid() && !field.CanInterface() { + // Unexported fields are not accessible from templates (Django renders them empty). + return reflect.Value{}, true, nil + } + return field, false, nil case reflect.Map: return current.MapIndex(reflect.ValueOf(part.s)), false, nil default: @@ -417,7 +422,12 @@ func (vr *variableResolver) resolveSubscript( } return reflect.Value{}, true, nil case reflect.Struct: - return current.FieldByName(sv.String()), false, nil + field := current.FieldByName(sv.String()) + if field.IsValid() && !field.CanInterface() { + // Unexported fields are not accessible from templates (Django renders them empty). + return reflect.Value{}, true, nil + } + return field, false, nil case reflect.Map: if sv.IsNil() { return reflect.Value{}, true, nil