Skip to content

Latest commit

 

History

History
165 lines (133 loc) · 4.2 KB

File metadata and controls

165 lines (133 loc) · 4.2 KB

Render to an io.Writer with FRender

FRender sends template output to an io.Writer. Use it to write directly to a file or connection, transform output, or stop rendering when a writer reaches a limit.

Basic use

engine := liquid.NewEngine()
template, err := engine.ParseTemplate([]byte(`<h1>{{ page.title }}</h1>`))
if err != nil {
    log.Fatal(err)
}

bindings := liquid.Bindings{
    "page": map[string]string{"title": "Introduction"},
}

var output bytes.Buffer
if err := template.FRender(&output, bindings); err != nil {
    log.Fatal(err)
}
fmt.Println(output.String())

Template.FRender returns a SourceError. Writer failures wrap their original error, so callers can inspect them with errors.Is.

Engine.ParseAndFRender combines parsing and rendering:

err := engine.ParseAndFRender(
    os.Stdout,
    []byte(`Hello, {{ name }}!`),
    liquid.Bindings{"name": "Ada"},
)

Write directly to a file

file, err := os.Create("output.html")
if err != nil {
    return err
}

renderErr := template.FRender(file, bindings)
closeErr := file.Close()
return errors.Join(renderErr, closeErr)

This avoids creating the complete rendered result as a []byte or string before writing it.

Limit output and check cancellation

The engine has no built-in output or execution limit. A writer can reject output after a byte limit and check a context at each write:

var ErrOutputLimit = errors.New("template output limit exceeded")

type BoundedWriter struct {
    Context  context.Context
    Writer   io.Writer
    MaxBytes int64
    written  int64
}

func (w *BoundedWriter) Write(p []byte) (int, error) {
    select {
    case <-w.Context.Done():
        return 0, w.Context.Err()
    default:
    }

    if int64(len(p)) > w.MaxBytes-w.written {
        return 0, ErrOutputLimit
    }

    n, err := w.Writer.Write(p)
    w.written += int64(n)
    if err == nil && n != len(p) {
        err = io.ErrShortWrite
    }
    return n, err
}

Use the writer with a timeout context:

func renderBounded(
    template *liquid.Template,
    bindings liquid.Bindings,
    timeout time.Duration,
    maxBytes int64,
) (string, error) {
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()

    var output bytes.Buffer
    writer := &BoundedWriter{
        Context:  ctx,
        Writer:   &output,
        MaxBytes: maxBytes,
    }

    if err := template.FRender(writer, bindings); err != nil {
        switch {
        case errors.Is(err, context.DeadlineExceeded):
            return "", fmt.Errorf("template render timed out: %w", err)
        case errors.Is(err, ErrOutputLimit):
            return "", fmt.Errorf(
                "template output exceeded %d bytes: %w",
                maxBytes,
                err,
            )
        default:
            return "", err
        }
    }
    return output.String(), nil
}

Cancellation is cooperative. The writer checks its context only when Liquid writes output. A template that performs a long computation without writing may not stop at the deadline. Run untrusted templates in a process or container with enforceable CPU, memory, and wall-clock limits.

Transform output

Writer wrappers can transform each output chunk:

type UpperWriter struct {
    Writer io.Writer
}

func (w UpperWriter) Write(p []byte) (int, error) {
    upper := bytes.ToUpper(p)
    n, err := w.Writer.Write(upper)
    if err == nil && n != len(upper) {
        err = io.ErrShortWrite
    }
    return n, err
}

Transformations must preserve io.Writer semantics. Return the number of input bytes consumed, propagate the wrapped writer's error, and return io.ErrShortWrite when a writer accepts fewer bytes without an error.

Choose a render method

Method Result Use it when
Template.Render []byte The caller needs bytes.
Template.RenderString string The caller needs a string.
Template.FRender Writes to io.Writer Output should stream through a destination or wrapper.
Engine.ParseAndFRender Parses, then writes The caller has source bytes and does not need to retain a compiled template.

For the full threat model, read the security policy.