Skip to content

proposal: text/template, html/template: add ExecuteContext methods #31107

@empijei

Description

@empijei

This proposal is about providing a way to pass a context.Context to template execution.

Summary

Reasons:

  • Template execution cannot be gracefully interrupted, and it is a rather expensive and potentially long-running task.
  • Passing values to templates can only be achieved through the data interface{} parameter, and this makes the package not flexible enough for some purposes (expecially for html web nonces).

Advantages:

  • Provide an idiomatic way to cancel template execution.
  • Provade an idiomatic way to pass values that are not related to data being rendered, but to the context it is rendered in.

Detailed rationale

Cancellation

Templates might implement a rather complex logic, and they might import each other. It could be possible that given some specific conditions a template might run for a long time, even after the result output is not needed anymore.

Currently the only way to address this is to have a separate goroutine that closes the output io.Writer of the template.

This has three main downsides:

  • Requires a separate goroutine to run, which costs and must not be leaked
  • io.WriteCloser does not document that calling Close concurrently with Write is safe. This means that to cancel a template execution the programmer needs to wrap their io.WriteCloser in a safe one, and only then they can abort a template execution safely
  • If the programmer has only an io.Writer they need to implement a custom closing mechanism on it.

Contextual values

Sometimes data needs to be rendered differently based on the context it is rendered in.

In the following examples I'm referring to two html/template examples I struggled with:

  1. If I want to render data in a <form>, I also need to add an anti-CSRF token. This means that the template might need a value in some cases, and might not need it in others. If a toolkit or framework wants to provide protection against CSRF attacks, it must require the users to add the CSRF field to all their data structures being rendered. If the programmer misses one, that becomes a vulnerability.
  2. If an html template contains a <script> or <style> tag, they need to have a nonce in order to be executed when a strict Content Security Policy is enforced. This means that coders will have to manually pass the nonce value to all templates executing in their handlers. If a coder misses one, that breaks the service with a runtime error in the browser.

It would be nice to be able to protect an entire http.Handler in a http.ServeMux from CSRF and XSS and just add security tokens to the context. It would make it very hard for programmers to forget about security.

Example

Adding context values will make HTML templates usage from requiring this:

func handle(w http.ResponseWriter, r *http.Request) {
	data := struct {
		MyField      string
		MyOtherField string
		Nonce        string
		CSRFtok      string
	}{
		"Field",
		"OtherField",
	}
	// Retrieve nonce from r.Context()
	data.Nonce = nonce
	// Retrieve CSRFtok from r.Context()
	data.CSRFtok = csrftok
	err := tmpl.Execute(w, data)
	// Handle err
}

to this (which also handles cancellation):

func handle(w http.ResponseWriter, r *http.Request) {
	data := struct {
		MyField      string
		MyOtherField string
	}{
		"Field",
		"OtherField",
	}
	err := tmpl.ExecuteContext(r.Context(), w, data)
	// Handle err
}

Toolkits and frameworks could then provide ways to pre-parse and transform templates and add values to requests context.

Backward compatibility

This change would be backward compatible as it is about adding two func in each package:

  • (*Template).ExecuteTemplateContext(context.Context, io.Writer, string, interface{})
  • (*Template).ExecuteContext(context.Context, io.Writer, interface{})

That could add a context or ctx func to the default FuncMap. This would also be backward compatible because no-one is using ExecuteContext at the moment, and they could change their func names when migrating to this new API (only if they are using "context" as a name).
Alternatively a forbidden name (like ! or $_) could be used, but I'm not a fan of this option.

What do you think?

/cc @mvdan @esseks @mikesamuel @robpike @bradfitz @lweichselbaum

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions