Render Golang templates with a timeout
Situation
You’re writing a Go program that renders arbitrary Go templates that users can write. Since they are arbitrary, you want to prevent users from accidentally DDoSing your program by using long-running template functions. Something like this:
import (
"os"
"template"
)
// Perhaps this is exposed through an interface that a
// third-party API implements, for example.
func LongRunningFunction(s string) {
time.Sleep(100000000) // This takes forever
return s
}
func main() {
tmpl := `Hello, {{ .LongRunningFunction . }}!`
t, err := template.New("my-template").Parse(tmpl)
if err != nil {
panic(err)
}
t.Execute(os.Stdout, "Carlos")
}
When t.Execute
runs, it will wait ten million seconds before displaying
“Hello, Carlos” to your terminal.
Obviously, this is not ideal.
Solution That You Want To Exist But Doesn’t
Perhaps I could use a context.Context
to have Go send a SIGINT
upon
exceeding a deadline, you’re probably thinking.
Unfortunately, the Go authors disagree with you.
Actual Solution
Fortunately, goroutines
make implmenting timeouts insanely easy. Let’s
explore:
import (
"fmt"
"os"
"template"
"time"
)
// Perhaps this is exposed through an interface that a
// third-party API implements, for example.
func LongRunningFunction(s string) {
time.Sleep(100000000) // This takes forever
return s
}
func main() {
timeoutSeconds := 5
res := make(chan bool)
go func() {
tmpl := `Hello, {{ .LongRunningFunction . }}!`
t, err := template.New("my-template").Parse(tmpl)
if err != nil {
panic(err)
}
t.Execute(os.Stdout, "Carlos")
res <- true
}
select {
case ok := <-res {
return
}
case <-time.After(time.Duration(timeoutSeconds) * time.Second) {
panic("timeout exceeded")
}
}
goroutines
allow you to create functions that execute asynchronously and
communicate through channels. You can think of channels like pipes; you
send data into them, and data is consumed from them elsewhere.
You can learn more about the relationship between goroutine
s and
channels
here.
Furthermore, the select
statement is kind-of like a switch
-case
for
channels. It awaits on multiple channels and selects the channel that sends data
first. You can learn more about the select statement
here.
Putting it all together, instead of rendering our template directly
from main
, we render it asynchronously inside of a goroutine
and have it
send a flag to the res
channel when its work is complete. At the same time, we
open another channel with time.After
that sends a time after the provided
duration; five seconds in our case. Because we are using a select
statement to
wait on both and our goroutine
won’t finish for SIXTEEN WEEKS,
the latter case wins and we panic
.
Happy programming!