The FRender method enables rendering Liquid templates directly to any io.Writer implementation, providing fine-grained control over output handling. This is particularly useful for performance optimization, resource limiting, and security constraints.
The simplest use of FRender writes template output to any io.Writer:
engine := liquid.NewEngine()
template, err := engine.ParseTemplate([]byte(`<h1>{{ page.title }}</h1>`))
if err != nil {
log.Fatal(err)
}
bindings := map[string]any{
"page": map[string]string{"title": "Introduction"},
}
var buf bytes.Buffer
err = template.FRender(&buf, bindings)
if err != nil {
log.Fatal(err)
}
fmt.Println(buf.String())
// Output: <h1>Introduction</h1>Avoid unnecessary memory allocation by rendering large templates directly to files:
engine := liquid.NewEngine()
template, err := engine.ParseTemplate(sourceBytes)
if err != nil {
log.Fatal(err)
}
file, err := os.Create("output.html")
if err != nil {
log.Fatal(err)
}
defer file.Close()
// Stream directly to file without intermediate buffers
err = template.FRender(file, bindings)
if err != nil {
log.Fatal(err)
}Prevent runaway template rendering by implementing cancellation via context:
// CancelWriter wraps an io.Writer with context cancellation support
type CancelWriter struct {
ctx context.Context
w io.Writer
}
func (cw *CancelWriter) Write(p []byte) (n int, err error) {
select {
case <-cw.ctx.Done():
return 0, cw.ctx.Err()
default:
return cw.w.Write(p)
}
}
func renderWithTimeout(template *liquid.Template, bindings liquid.Bindings, timeout time.Duration) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var buf bytes.Buffer
cw := &CancelWriter{ctx: ctx, w: &buf}
err := template.FRender(cw, bindings)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return "", fmt.Errorf("template rendering exceeded %v timeout", timeout)
}
return "", err
}
return buf.String(), nil
}
// Usage
engine := liquid.NewEngine()
template, _ := engine.ParseTemplate([]byte(`{% for i in (1..1000000) %}{{ i }}{% endfor %}`))
result, err := renderWithTimeout(template, liquid.Bindings{}, 100*time.Millisecond)
if err != nil {
log.Printf("Rendering stopped: %v", err)
}This is crucial when rendering untrusted templates that might contain deeply nested loops or expensive operations.
Protect against excessive output from untrusted templates:
// LimitWriter enforces a maximum output size
type LimitWriter struct {
w io.Writer
written int64
maxBytes int64
}
var ErrOutputLimitExceeded = errors.New("output size limit exceeded")
func NewLimitWriter(w io.Writer, maxBytes int64) *LimitWriter {
return &LimitWriter{w: w, maxBytes: maxBytes}
}
func (lw *LimitWriter) Write(p []byte) (n int, err error) {
if lw.written+int64(len(p)) > lw.maxBytes {
return 0, ErrOutputLimitExceeded
}
n, err = lw.w.Write(p)
lw.written += int64(n)
return n, err
}
func renderWithSizeLimit(template *liquid.Template, bindings liquid.Bindings, maxBytes int64) (string, error) {
var buf bytes.Buffer
lw := NewLimitWriter(&buf, maxBytes)
err := template.FRender(lw, bindings)
if err != nil {
if errors.Is(err, ErrOutputLimitExceeded) {
return "", fmt.Errorf("template output exceeded %d bytes", maxBytes)
}
return "", err
}
return buf.String(), nil
}
// Usage - limit untrusted template output to 1MB
result, err := renderWithSizeLimit(template, bindings, 1024*1024)
if err != nil {
log.Printf("Rendering failed: %v", err)
}Transform output on-the-fly without post-processing:
// UpperCaseWriter converts all output to uppercase
type UpperCaseWriter struct {
w io.Writer
}
func (uc *UpperCaseWriter) Write(p []byte) (n int, err error) {
upper := bytes.ToUpper(p)
return uc.w.Write(upper)
}
// MinifyWriter could strip whitespace, compress, etc.
type MinifyWriter struct {
w io.Writer
}
func (mw *MinifyWriter) Write(p []byte) (n int, err error) {
// Remove extra whitespace
compressed := regexp.MustCompile(`\s+`).ReplaceAll(p, []byte(" "))
_, err = mw.w.Write(compressed)
return len(p), err // Return original length for proper accounting
}
// Usage
var buf bytes.Buffer
upperWriter := &UpperCaseWriter{w: &buf}
template.FRender(upperWriter, bindings)func (t *Template) FRender(w io.Writer, vars Bindings) SourceErrorExecutes the template with the specified variable bindings and writes output to w.
Parameters:
w: Any type implementingio.Writerinterfacevars: Variable bindings (typicallymap[string]any)
Returns:
SourceError: Error with source location information, ornilon success
Error Handling:
FRender returns errors from:
- Template execution errors (undefined variables, filter errors, etc.)
- Writer errors (disk full, context cancellation, custom limits, etc.)
Both error types are returned as SourceError when possible, providing line number information for template-related issues.
func (e *Engine) ParseAndFRender(w io.Writer, source []byte, b Bindings) SourceErrorConvenience method that parses a template and immediately renders it to a writer.
Example:
engine := liquid.NewEngine()
var buf bytes.Buffer
err := engine.ParseAndFRender(&buf, []byte(`{{ greeting }}`), liquid.Bindings{
"greeting": "Hello, World!",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(buf.String())| Method | Return Type | Use Case |
|---|---|---|
Render(vars) |
([]byte, error) |
Small templates, need byte slice |
RenderString(vars) |
(string, error) |
Small templates, need string |
FRender(w, vars) |
error |
Large output, streaming, custom handling |
When to use FRender:
- Template output > 1MB (avoid memory allocation)
- Writing to files or network connections
- Need cancellation or resource limits
- Want custom output transformation
- Rendering untrusted templates
When to use Render/RenderString:
- Small templates with predictable output
- Need the result as a value for further processing
- Simpler code for straightforward use cases
FRender can significantly improve performance for large templates:
// Memory-inefficient for large output
data, _ := template.Render(bindings)
file.Write(data) // Entire output buffered in memory
// Memory-efficient streaming
file, _ := os.Create("output.html")
template.FRender(file, bindings) // Streams directly to diskFor a 100MB template output:
Render()approach: ~100MB memory usageFRender()approach: ~4KB memory usage (typical buffer size)
When rendering untrusted templates, always use FRender with protective wrappers:
type SafeWriter struct {
ctx context.Context
w io.Writer
written int64
maxBytes int64
}
func (sw *SafeWriter) Write(p []byte) (n int, err error) {
// Check context cancellation
select {
case <-sw.ctx.Done():
return 0, sw.ctx.Err()
default:
}
// Check size limit
if sw.written+int64(len(p)) > sw.maxBytes {
return 0, ErrOutputLimitExceeded
}
n, err = sw.w.Write(p)
sw.written += int64(n)
return n, err
}
func renderUntrusted(template *liquid.Template, bindings liquid.Bindings) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var buf bytes.Buffer
safeWriter := &SafeWriter{
ctx: ctx,
w: &buf,
maxBytes: 10 * 1024 * 1024, // 10MB limit
}
err := template.FRender(safeWriter, bindings)
return buf.String(), err
}This approach protects against:
- Infinite loops or deeply nested iterations
- Excessive memory consumption
- DoS attacks via template complexity