neurons firing from a keyboard

thoughts about devops, technology, and faster business from a random guy from dallas.

Render Golang templates with a timeout

Reading Time: Approximately 2 minutes.
View this post on GitHub.

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 goroutines 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!