Repository: google/go-safeweb
Pull request #62 (2020-08-10 13:24:13)

Add a plugin to automatically nonce templates.


Patch:
diff --git a/docs/high-level-design.md b/docs/high-level-design.md
index 48f1413..e1a3338 100644
--- a/docs/high-level-design.md
+++ b/docs/high-level-design.md
@@ -74,7 +74,7 @@ address, following issues (not an exhaustive list):
 - [**CSP (content security policy)**](https://csp.withgoogle.com/docs/index.html) -
   e.g. by automatically adding script nonces to HTML responses, adding relevant
   security headers
-- **Transport Security** - e.g. by enforcing HSTS support
+- **Transport Security** - e.g. by [enforcing HSTS support](plugins/hsts.md)
 - **IFraming** - e.g. by setting relevant HTTP headers to restrict framing or
   providing server-side support for origin selection
 - **Auth (access control)** - e.g. by providing infrastructure for plugging in
diff --git a/docs/plugins.md b/docs/plugins.md
new file mode 100644
index 0000000..16a77d1
--- /dev/null
+++ b/docs/plugins.md
@@ -0,0 +1,10 @@
+# Plugins
+
+The framework supports plugins. They provide a flexible way to address security 
+issues by setting non-overridable security headers or interrupting and
+responding to incoming requests before they reach the handler.
+
+## Provided plugins
+
+- [HSTS](plugins/hsts.md): Automatically redirects HTTP traffic to HTTPS and
+sets the `Strict-Transport-Security` header.
diff --git a/docs/plugins/hsts.md b/docs/plugins/hsts.md
new file mode 100644
index 0000000..b1e558f
--- /dev/null
+++ b/docs/plugins/hsts.md
@@ -0,0 +1,34 @@
+_Authors: grenfeldt@google.com_
+
+# HSTS Plugin
+
+HSTS<sup>1</sup> (HTTP Strict Transport Security) informs browsers that a
+website should only be accessed using HTTPS and not HTTP. This plugin enforces
+HSTS by redirecting all HTTP traffic to HTTPS and by setting the
+`Strict-Transport-Security` header on all outgoing HTTPS traffic.
+
+1) HSTS: [RFC](https://tools.ietf.org/html/rfc6797),
+[MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security),
+[Wikipedia](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security)
+
+## Usage
+
+To construct the plugin with safe default settings, use: `hsts.NewPlugin()`.
+
+## Options
+
+**Option (Default value)**: Description.
+
+- **MaxAge (`2 years`)**: The amount of time that the browser should remember to
+only access this site using HTTPS. The default value is 2 years as recommended
+by https://hstspreload.org/.
+- **IncludeSubDomains (`enabled`)**: When this is enabled, the browser knows
+that all subdomains should also only be accessed via HTTPS.
+- **Preload (`disabled`)**: If enabled, the domain is eligible to be added as
+part of the browser HSTS preload list. This is disabled by default to prevent
+adding domains to the preload list unintentionally. For more info, see:
+https://hstspreload.org/.
+- **BehindProxy (`disabled`)**: If this server is placed behind a proxy that
+terminates HTTPS traffic then only HTTP traffic will be received by this server.
+This option disables redirection of HTTP to HTTPS and always sends the
+`Strict-Transport-Security` header, even over HTTP.
diff --git a/go.mod b/go.mod
index 2cb5022..d686ce7 100644
--- a/go.mod
+++ b/go.mod
@@ -5,5 +5,5 @@ go 1.14
 require (
 	github.com/google/go-cmp v0.5.0
 	github.com/google/safehtml v0.0.2
+	golang.org/x/net v0.0.0-20200707034311-ab3426394381
 )
-
diff --git a/go.sum b/go.sum
index af176cb..8ebae39 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,19 @@
 github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
 github.com/google/safehtml v0.0.2 h1:ZOt2VXg4x24bW0m2jtzAOkhoXV0iM8vNKc0paByCZqM=
 github.com/google/safehtml v0.0.2/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/safehttp/incoming_request.go b/safehttp/incoming_request.go
index eeabb5e..cb8385a 100644
--- a/safehttp/incoming_request.go
+++ b/safehttp/incoming_request.go
@@ -18,18 +18,18 @@ import (
 	"crypto/tls"
 	"fmt"
 	"net/http"
-	"net/url"
 	"strings"
 	"sync"
 )
 
 // IncomingRequest TODO
 type IncomingRequest struct {
-	req       *http.Request
-	Header    Header
-	parseOnce sync.Once
-	TLS       *tls.ConnectionState
-	URL       *url.URL
+	req                *http.Request
+	Header             Header
+	postParseOnce      sync.Once
+	multipartParseOnce sync.Once
+	TLS                *tls.ConnectionState
+	URL                *URL
 }
 
 // NewIncomingRequest creates an IncomingRequest
@@ -39,10 +39,25 @@ func NewIncomingRequest(req *http.Request) *IncomingRequest {
 		req:    req,
 		Header: newHeader(req.Header),
 		TLS:    req.TLS,
-		URL:    req.URL,
+		URL:    &URL{url: req.URL},
 	}
 }
 
+// Host returns the host the request is targeted to.
+// TODO(@mihalimara22): Remove this after the safehttp.URL type has been
+// implemented.
+func (r *IncomingRequest) Host() string {
+	return r.req.Host
+}
+
+// Path returns the relative path of the URL in decoded format (e.g. %47%6f%2f
+// becomes /Go/).
+// TODO(@mihalimara22): Remove this after the safehttp.URL type has been     //
+// implemented.
+func (r *IncomingRequest) Path() string {
+	return r.req.URL.Path
+}
+
 // PostForm parses the form parameters provided in the body of a POST, PATCH or
 // PUT request that does not have Content-Type: multipart/form-data. It returns
 // the parsed form parameters as a Form object, if no error occurred. If a parsing
@@ -51,7 +66,7 @@ func NewIncomingRequest(req *http.Request) *IncomingRequest {
 // always be used for forms in POST requests.
 func (r *IncomingRequest) PostForm() (*Form, error) {
 	var err error
-	r.parseOnce.Do(func() {
+	r.postParseOnce.Do(func() {
 		if r.req.Method != "POST" && r.req.Method != "PATCH" && r.req.Method != "PUT" {
 			err = fmt.Errorf("got request method %s, want POST/PATCH/PUT", r.req.Method)
 			return
@@ -80,11 +95,11 @@ func (r *IncomingRequest) PostForm() (*Form, error) {
 // used when the user expects a POST request with the Content-Type: multipart/form-data header.
 func (r *IncomingRequest) MultipartForm(maxMemory int64) (*MultipartForm, error) {
 	var err error
-	r.parseOnce.Do(func() {
-		// Ensures no more than 32 MB are stored in memory when a form file is
-		// passed as part of the request. If this is bigger than 32 MB, the rest
-		// will be stored on disk.
-		const defaultMaxMemory = 32 << 20
+	// Ensures no more than 32 MB are stored in memory when a form file is
+	// passed as part of the request. If this is bigger than 32 MB, the rest
+	// will be stored on disk.
+	const defaultMaxMemory = 32 << 20
+	r.multipartParseOnce.Do(func() {
 		if r.req.Method != "POST" && r.req.Method != "PATCH" && r.req.Method != "PUT" {
 			err = fmt.Errorf("got request method %s, want POST/PATCH/PUT", r.req.Method)
 			return
diff --git a/safehttp/interceptor.go b/safehttp/interceptor.go
new file mode 100644
index 0000000..c82a08c
--- /dev/null
+++ b/safehttp/interceptor.go
@@ -0,0 +1,25 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package safehttp
+
+// Interceptor can be installed on a ServeMux in order to apply its
+// functionality on an IncomingRequest before it is sent to its corresponding
+// handler.
+type Interceptor interface {
+	// Before runs before the IncomingRequest is sent to the handler. If a
+	// response is written to the ResponseWriter, then the remaining
+	// interceptors and the handler won't execute.
+	Before(w ResponseWriter, r *IncomingRequest) Result
+}
diff --git a/safehttp/mux.go b/safehttp/mux.go
index 76ddc98..35b7ffb 100644
--- a/safehttp/mux.go
+++ b/safehttp/mux.go
@@ -74,6 +74,9 @@ type ServeMux struct {
 
 	// Maps patterns to handlers supporting multiple HTTP methods.
 	handlers map[string]methodHandler
+
+	// Maps interceptor key to interceptor.
+	interceps map[string]Interceptor
 }
 
 // NewServeMux allocates and returns a new ServeMux
@@ -84,10 +87,11 @@ func NewServeMux(d Dispatcher, domains ...string) *ServeMux {
 		dm[host] = true
 	}
 	return &ServeMux{
-		mux:      http.NewServeMux(),
-		domains:  dm,
-		disp:     d,
-		handlers: map[string]methodHandler{},
+		mux:       http.NewServeMux(),
+		domains:   dm,
+		disp:      d,
+		handlers:  map[string]methodHandler{},
+		interceps: map[string]Interceptor{},
 	}
 }
 
@@ -97,9 +101,10 @@ func (m *ServeMux) Handle(pattern string, method string, h Handler) {
 	mh, ok := m.handlers[pattern]
 	if !ok {
 		mh := methodHandler{
-			handlers: map[string]Handler{method: h},
-			domains:  m.domains,
-			disp:     m.disp,
+			handlers:     map[string]Handler{method: h},
+			domains:      m.domains,
+			disp:         m.disp,
+			muxInterceps: m.interceps,
 		}
 
 		m.handlers[pattern] = mh
@@ -113,23 +118,41 @@ func (m *ServeMux) Handle(pattern string, method string, h Handler) {
 	mh.handlers[method] = h
 }
 
+// Install installs an Interceptor. Interceptor keys need to be unique. If an
+// Interceptor with the same key has already been installed, Install panics.
+//
+// TODO(@empijei, @grenfeldt, @kele, @mihalimara22): Right now you could install
+// the same interceptor twice with different keys, we need to figure out how
+// exactly we want to avoid that and how we define key uniqueness.
+func (m *ServeMux) Install(key string, i Interceptor) {
+	if _, exists := m.interceps[key]; exists {
+		panic("interceptor with same key already installed")
+	}
+	m.interceps[key] = i
+}
+
 // ServeHTTP dispatches the request to the handler whose method matches the
 // incoming request and whose pattern most closely matches the request URL.
 func (m *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	m.mux.ServeHTTP(w, r)
 }
 
-// methodHandler is collection of handlers based on the request method.
+// methodHandler is a collection of handlers based on the request method.
 type methodHandler struct {
 	// Maps an HTTP method to its handler
-	handlers map[string]Handler
-	domains  map[string]bool
-	disp     Dispatcher
+	handlers     map[string]Handler
+	domains      map[string]bool
+	disp         Dispatcher
+	muxInterceps map[string]Interceptor
 }
 
 // ServeHTTP dispatches the request to the handler associated with
-// the incoming request's method.
+// the incoming request's method after calling the Before function of all
+// ServeMux interceptors the handler is registered on.
 func (m methodHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	rw := NewResponseWriter(m.disp, w, m.muxInterceps)
+	ir := NewIncomingRequest(r)
+
 	if !m.domains[r.Host] {
 		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
 		return
@@ -141,5 +164,11 @@ func (m methodHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	h.ServeHTTP(NewResponseWriter(m.disp, w), NewIncomingRequest(r))
+	for _, intercep := range m.muxInterceps {
+		if res := intercep.Before(rw, ir); res.written {
+			return
+		}
+	}
+
+	h.ServeHTTP(rw, ir)
 }
diff --git a/safehttp/plugins/hsts/hsts.go b/safehttp/plugins/hsts/hsts.go
index dd2ceac..e2bfc55 100644
--- a/safehttp/plugins/hsts/hsts.go
+++ b/safehttp/plugins/hsts/hsts.go
@@ -28,7 +28,7 @@ import (
 type Plugin struct {
 	// MaxAge is the duration that the browser should remember
 	// that a site is only to be accessed using HTTPS. MaxAge
-	// must be positive.
+	// must be positive. It will be rounded to seconds before use.
 	MaxAge time.Duration
 
 	// DisableIncludeSubDomains disables the includeSubDomains directive.
diff --git a/safehttp/plugins/htmlinject/htmlinject.go b/safehttp/plugins/htmlinject/htmlinject.go
new file mode 100644
index 0000000..ec54533
--- /dev/null
+++ b/safehttp/plugins/htmlinject/htmlinject.go
@@ -0,0 +1,194 @@
+// Package htmlinject provides utilities to pre-process HTML templates and inject additional parts into them before parsing.
+package htmlinject
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"strings"
+
+	"golang.org/x/net/html"
+)
+
+// Rule is a directive to instruct Transform on how to rewrite the given template.
+type Rule struct {
+	// Name is used for debug purposes in case rewriting fails.
+	Name string
+	// OnTag is the tag to be used to trigger the rule.
+	OnTag string
+	// WithAttributes is a filter applied on tags to decide whether to run the Rule:
+	// only tags with the given attributes key:value will be matched.
+	WithAttributes map[string]string
+	// AddAttributes is a list of strings to add to the HTML as attributes.
+	// All the given strings will be appended verbatim after the matched tag so they
+	// should be prefixed with a space.
+	AddAttributes []string
+	// AddNodes is a list of nodes to append immediately after the opening tag that matched.
+	// This means that for elements that have a matching closing tag the added node will be
+	// a child node, for self-closing tags it will be a sibling.
+	AddNodes []string
+}
+
+func (r Rule) String() string { return r.Name }
+
+// Config is a slice of Rules that are somehow related to each other.
+type Config []Rule
+
+// CSPNoncesDefault is the default config for CSP Nonces. The rewritten template
+// expects the CSPNonce Func to be available in the template to provide nonces.
+var CSPNoncesDefault = CSPNonces(`nonce="{{CSPNonce}}"`)
+
+// CSPNonces constructs a Config to add CSP nonces to a template. The given nonce
+// attribute will be automatically prefixed with the required empty space.
+func CSPNonces(nonceAttr string) Config {
+	nonceAttr = " " + nonceAttr
+	return Config{
+		Rule{
+			Name:          "Nonces for scripts",
+			OnTag:         "script",
+			AddAttributes: []string{nonceAttr},
+		},
+		Rule{
+			Name:           "Nonces for link as=script rel=preload",
+			OnTag:          "link",
+			WithAttributes: map[string]string{"rel": "preload", "as": "script"},
+			AddAttributes:  []string{nonceAttr},
+		},
+	}
+}
+
+// XSRFTokensDefault is the default config to add hidden inputs to forms to provide
+// an anti-XSRF token. The rewritten template expects the XSRFToken Func to be available
+// in the template to provide tokens and sets the name for the sent value to be "xsrf-token".
+var XSRFTokensDefault = XSRFTokens(`<input type="hidden" name="xsrf-token" value="{{XSRFToken}}">`)
+
+// XSRFTokens constructs a Config to add the given string as a child node to forms.
+func XSRFTokens(inputTag string) Config {
+	return Config{Rule{
+		Name:     "XSRFTokens on forms",
+		OnTag:    "form",
+		AddNodes: []string{inputTag}}}
+}
+
+// Transform rewrites the given template according to the given configs.
+// If the passed io.Rewriter has a `Size() int64` method it will be used to pre-allocate buffers.
+func Transform(src io.Reader, cfg ...Config) (string, error) {
+	rw := rewriter{
+		rules:     map[string][]Rule{},
+		tokenizer: html.NewTokenizer(src),
+		out:       &strings.Builder{},
+	}
+	if sizer, ok := src.(interface{ Size() int64 }); ok {
+		rw.out.Grow(int(sizer.Size()))
+	}
+	for _, c := range cfg {
+		for _, r := range c {
+			rw.rules[r.OnTag] = append(rw.rules[r.OnTag], r)
+		}
+	}
+	if err := rw.rewrite(); err != nil {
+		return "", err
+	}
+	return rw.out.String(), nil
+}
+
+type rewriter struct {
+	// tag -> rules for that tag
+	rules     map[string][]Rule
+	tokenizer *html.Tokenizer
+	out       *strings.Builder
+}
+
+// emitRaw copies the current raw token to the output.
+func (r rewriter) emitRaw() error {
+	_, err := r.out.Write(r.tokenizer.Raw())
+	return err
+}
+
+// rewrite runs the rewriter.
+func (r rewriter) rewrite() error {
+	for {
+		switch tkn := r.tokenizer.Next(); tkn {
+		case html.ErrorToken:
+			if err := r.tokenizer.Err(); !errors.Is(err, io.EOF) {
+				return err
+			}
+			// We got EOF, let's just emit the last token and exit.
+			return r.emitRaw()
+		case html.StartTagToken, html.SelfClosingTagToken:
+			if err := r.processTag(); err != nil {
+				return err
+			}
+		default:
+			if err := r.emitRaw(); err != nil {
+				return err
+			}
+		}
+	}
+}
+
+func (r rewriter) processTag() error {
+	// Copy raw tokens to better formats
+	var (
+		tagname    string
+		attributes = map[string]string{}
+		raw        = make([]byte, len(r.tokenizer.Raw()))
+	)
+	{
+		copy(raw, r.tokenizer.Raw())
+		tag, hasAttr := r.tokenizer.TagName()
+		tagname = string(tag)
+		for hasAttr {
+			key, val, more := r.tokenizer.TagAttr()
+			hasAttr = more
+			attributes[string(key)] = string(val)
+		}
+	}
+
+	// Filter rules by attributes
+	var triggeredRules []Rule
+	{
+		for _, r := range r.rules[tagname] {
+			match := true
+			for k, v := range r.WithAttributes {
+				if attributes[k] != v {
+					match = false
+					break
+				}
+			}
+			if match {
+				triggeredRules = append(triggeredRules, r)
+			}
+		}
+	}
+
+	// Emit the rewritten HTML
+	{
+		attrPos := len(tagname) + 1
+		// Write the "<" symbol and the tag name, e.g. "<script"
+		if _, err := r.out.Write(raw[:attrPos]); err != nil {
+			return fmt.Errorf("copying beginning of tag: %w", err)
+		}
+		// Write the attributes we have to add
+		for _, rule := range triggeredRules {
+			for _, attr := range rule.AddAttributes {
+				if _, err := r.out.WriteString(attr); err != nil {
+					return fmt.Errorf("executing rule %q: %w", rule.Name, err)
+				}
+			}
+		}
+		// Write the rest of the opening tag, e.g. ` src="foo.js">`
+		if _, err := r.out.Write(raw[attrPos:]); err != nil {
+			return fmt.Errorf("copying end of tag: %w", err)
+		}
+		// Write the nodes we have to add
+		for _, rule := range triggeredRules {
+			for _, node := range rule.AddNodes {
+				if _, err := r.out.WriteString(node); err != nil {
+					return fmt.Errorf("executing rule %q: %w", rule.Name, err)
+				}
+			}
+		}
+	}
+	return nil
+}
diff --git a/safehttp/plugins/xsrf/xsrf.go b/safehttp/plugins/xsrf/xsrf.go
new file mode 100644
index 0000000..7fc34dd
--- /dev/null
+++ b/safehttp/plugins/xsrf/xsrf.go
@@ -0,0 +1,74 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package xsrf
+
+import (
+	"github.com/google/go-safeweb/safehttp"
+	"golang.org/x/net/xsrftoken"
+)
+
+const (
+	// TokenKey is the form key used when sending the token as part of POST
+	// request.
+	TokenKey = "xsrf-token"
+)
+
+// UserIdentifier provides the web application users' identifiers,
+// needed in generating the XSRF token.
+type UserIdentifier interface {
+	// UserID returns the user's identifier based on the
+	// safehttp.IncomingRequest received.
+	UserID(*safehttp.IncomingRequest) (string, error)
+}
+
+// Interceptor implements XSRF protection. It requires an application key and a
+// storage service. The appKey uniquely identifies each registered service and
+// should have high entropy. The storage service supports retrieving ID's of the
+// application's users. Both the appKey and user ID are used in the XSRF
+// token generation algorithm.
+type Interceptor struct {
+	AppKey     string
+	Identifier UserIdentifier
+}
+
+// Before should be executed before directing the safehttp.IncomingRequest to
+// the handler to ensure it is not part of the Cross Site Request
+// Forgery. It checks for the presence of an xsrf-token in the request body and
+// validates it based on the userID associated with the request.
+func (p *Interceptor) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+	userID, err := p.Identifier.UserID(r)
+	if err != nil {
+		return w.ClientError(safehttp.StatusUnauthorized)
+	}
+	f, err := r.PostForm()
+	if err != nil {
+		mf, err := r.MultipartForm(32 << 20)
+		if err != nil {
+			return w.ClientError(safehttp.StatusBadRequest)
+		}
+		f = &mf.Form
+	}
+
+	tok := f.String(TokenKey, "")
+	if f.Err() != nil || tok == "" {
+		return w.ClientError(safehttp.StatusUnauthorized)
+	}
+
+	if ok := xsrftoken.Valid(tok, p.AppKey, userID, r.Host()+r.Path()); !ok {
+		return w.ClientError(safehttp.StatusForbidden)
+	}
+
+	return safehttp.Result{}
+}
diff --git a/safehttp/prototype.go b/safehttp/prototype.go
index 5729f94..f174bc1 100644
--- a/safehttp/prototype.go
+++ b/safehttp/prototype.go
@@ -31,7 +31,7 @@ func NewMachinery(h HandleFunc, d Dispatcher) *Machinery {
 
 // HandleRequest TODO
 func (m *Machinery) HandleRequest(w http.ResponseWriter, req *http.Request) {
-	rw := NewResponseWriter(m.d, w)
+	rw := NewResponseWriter(m.d, w, nil)
 	ir := NewIncomingRequest(req)
 	m.h(rw, ir)
 }
diff --git a/safehttp/response_writer.go b/safehttp/response_writer.go
index 1467141..2cc6d03 100644
--- a/safehttp/response_writer.go
+++ b/safehttp/response_writer.go
@@ -26,25 +26,42 @@ type ResponseWriter struct {
 	// Having this field unexported is essential for
 	// security. Otherwise one can easily overwrite
 	// the struct bypassing all our safety guarantees.
-	header Header
+	header       Header
+	muxInterceps map[string]Interceptor
 }
 
-// NewResponseWriter creates a ResponseWriter from
-// a safehttp.Dispatcher and a http.ResponseWriter.
-func NewResponseWriter(d Dispatcher, rw http.ResponseWriter) ResponseWriter {
+// NewResponseWriter creates a ResponseWriter from a safehttp.Dispatcher, an
+// http.ResponseWriter and a list of interceptors associated with a ServeMux.
+func NewResponseWriter(d Dispatcher, rw http.ResponseWriter, muxInterceps map[string]Interceptor) ResponseWriter {
 	header := newHeader(rw.Header())
-	return ResponseWriter{d: d, rw: rw, header: header}
+	return ResponseWriter{
+		d:            d,
+		rw:           rw,
+		header:       header,
+		muxInterceps: muxInterceps,
+	}
+}
+
+// Interceptor returns the interceptor associated with the given key.
+func (w *ResponseWriter) Interceptor(key string) Interceptor {
+	mp, ok := w.muxInterceps[key]
+	if !ok {
+		return nil
+	}
+	return mp
 }
 
 // Result TODO
-type Result struct{}
+type Result struct {
+	written bool
+}
 
 // Write TODO
 func (w *ResponseWriter) Write(resp Response) Result {
 	if err := w.d.Write(w.rw, resp); err != nil {
 		panic("error")
 	}
-	return Result{}
+	return Result{written: true}
 }
 
 // WriteTemplate TODO
@@ -52,7 +69,7 @@ func (w *ResponseWriter) WriteTemplate(t Template, data interface{}) Result {
 	if err := w.d.ExecuteTemplate(w.rw, t, data); err != nil {
 		panic("error")
 	}
-	return Result{}
+	return Result{written: true}
 }
 
 // ClientError TODO
@@ -62,7 +79,7 @@ func (w *ResponseWriter) ClientError(code StatusCode) Result {
 		panic("wrong method called")
 	}
 	http.Error(w.rw, http.StatusText(int(code)), int(code))
-	return Result{}
+	return Result{written: true}
 }
 
 // ServerError TODO
@@ -71,16 +88,16 @@ func (w *ResponseWriter) ServerError(code StatusCode) Result {
 		// TODO(@mattiasgrenfeldt, @mihalimara22, @kele, @empijei): Decide how it should
 		// be communicated to the user of the framework that they've called the wrong
 		// method.
-		return Result{}
+		return Result{written: true}
 	}
 	http.Error(w.rw, http.StatusText(int(code)), int(code))
-	return Result{}
+	return Result{written: true}
 }
 
 // Redirect responds with a redirect to a given url, using code as the status code.
 func (w *ResponseWriter) Redirect(r *IncomingRequest, url string, code StatusCode) Result {
 	http.Redirect(w.rw, r.req, url, int(code))
-	return Result{}
+	return Result{written: true}
 }
 
 // Header returns the collection of headers that will be set
diff --git a/safehttp/status.go b/safehttp/status.go
index b65ac7f..72ba7a2 100644
--- a/safehttp/status.go
+++ b/safehttp/status.go
@@ -21,5 +21,8 @@ type StatusCode int
 const (
 	StatusOK                  StatusCode = 200 // RFC 7231, 6.3.1
 	StatusMovedPermanently    StatusCode = 301 // RFC 7231, 6.4.2
+	StatusBadRequest          StatusCode = 400 // RFC 7231, 6.5.1
+	StatusUnauthorized        StatusCode = 401 // RFC 7231, 3.1
+	StatusForbidden           StatusCode = 403 // RFC 7231, 6.5.3
 	StatusInternalServerError StatusCode = 500 // RFC 7231, 6.6.1
 )
diff --git a/safehttp/url.go b/safehttp/url.go
new file mode 100644
index 0000000..356be06
--- /dev/null
+++ b/safehttp/url.go
@@ -0,0 +1,73 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package safehttp
+
+import "net/url"
+
+// URL represents a parsed URL (technically, a URI reference).
+type URL struct {
+	url *url.URL
+}
+
+// Query parses the query string in the URL and returns a form
+// containing its values. The returned error describes the first
+// decoding error encountered, if any.
+func (u URL) Query() (Form, error) {
+	v, err := url.ParseQuery(u.url.RawQuery)
+	if err != nil {
+		return Form{}, err
+	}
+	return Form{values: map[string][]string(v)}, nil
+}
+
+// String reassembles the URL into a valid URL string.
+//
+// The method uses the net/url.EscapedPath method to obtain the path.
+// See the net/url.EscapedPath method for more details.
+func (u URL) String() string {
+	// The escaping is perfomed by u.url.String()
+	return u.url.String()
+}
+
+// Host returns the host or the host:port of the URL.
+func (u URL) Host() string {
+	return u.url.Host
+}
+
+// Hostname returns the host of the URL, stripping any valid
+// port number if present.
+//
+// If the result is enclosed in square brackets, as literal IPv6
+// addresses are, the square brackets are removed from the result.
+func (u URL) Hostname() string {
+	return u.url.Hostname()
+}
+
+// Port returns the port part of the URL. If the
+// host doesn't contain a valid port number, Port returns an
+// empty string.
+func (u URL) Port() string {
+	return u.url.Port()
+}
+
+// Path returns the path of the URL.
+//
+// Note that the path is stored in decoded form: /%47%6f%2f
+// becomes /Go/. A consequence is that it is impossible to tell
+// which slashes in the path were slashes in the raw URL and which
+// were %2f.
+func (u URL) Path() string {
+	return u.url.Path
+}


Test patch:
diff --git a/internal/requesttesting/test_harness.go b/internal/requesttesting/test_harness.go
index cc0c93b..143020c 100644
--- a/internal/requesttesting/test_harness.go
+++ b/internal/requesttesting/test_harness.go
@@ -22,6 +22,7 @@ import (
 	"io"
 	"net"
 	"net/http"
+	"runtime"
 	"sync"
 )
 
@@ -128,6 +129,18 @@ func MakeRequest(ctx context.Context, req []byte, callback func(*http.Request))
 	if n == 4096 {
 		return nil, errors.New("response larger than or equal to 4096 bytes")
 	}
-
+	listener.clientEndpoint.Close()
+
+	// Don't remove the line below! This line is to stop server.Shutdown from
+	// getting stuck polling for idle connections to close. When we close the
+	// connection on the line above, it takes a little while for the other
+	// goroutines to shut everything down. When server.Shutdown is called it
+	// checks if there are any idle, non-closed connections left and waits for
+	// them to close. It performs this check every 0.5 s. But our connection
+	// above takes a lot less time than 0.5 s to close. Therefore, if we just
+	// give up execution for a moment to let the other goroutines close
+	// everything before calling server.Shutdown, we don't get punished by
+	// having to wait 0.5s every time we try to shut down.
+	runtime.Gosched()
 	return resp[:n], server.Shutdown(ctx)
 }
diff --git a/safehttp/form_test.go b/safehttp/form_test.go
index 052e208..a8c546d 100644
--- a/safehttp/form_test.go
+++ b/safehttp/form_test.go
@@ -15,15 +15,13 @@
 package safehttp_test
 
 import (
-	"errors"
-	"fmt"
 	"math"
 	"net/http"
 	"net/http/httptest"
-	"reflect"
 	"strconv"
 	"strings"
 	"testing"
+	"text/template"
 
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-safeweb/safehttp"
@@ -77,17 +75,20 @@ func TestFormValidInt(t *testing.T) {
 				}
 				form = &mf.Form
 			}
+
 			got := form.Int64("pizza", 0)
 			if err := form.Err(); err != nil {
 				t.Errorf(`form.Error: got %v, want nil`, err)
 			}
 			if diff := cmp.Diff(test.want, got); diff != "" {
-				t.Errorf("form.Int64: got %v, want %v, diff (-want +got): \n%s", got, test.want, diff)
+				t.Errorf("form.Int64:  mismatch (-want +got): \n%s", diff)
 			}
 			return safehttp.Result{}
 		}, &testDispatcher{})
+
 		rec := newResponseRecorder(&strings.Builder{})
 		m.HandleRequest(rec, test.req)
+
 		if want := 200; rec.status != want {
 			t.Errorf("response status: got %v, want %v", rec.status, want)
 		}
@@ -98,7 +99,6 @@ func TestFormInvalidInt(t *testing.T) {
 	tests := []struct {
 		name string
 		reqs []*http.Request
-		err  error
 		want int64
 	}{
 		{
@@ -120,7 +120,6 @@ func TestFormInvalidInt(t *testing.T) {
 					return multipartReq
 				}(),
 			},
-			err:  errors.New(`strconv.ParseInt: parsing "9223372036854775810": value out of range`),
 			want: 0,
 		},
 		{
@@ -143,7 +142,6 @@ func TestFormInvalidInt(t *testing.T) {
 					return multipartReq
 				}(),
 			},
-			err:  errors.New(`strconv.ParseInt: parsing "diavola": invalid syntax`),
 			want: 0,
 		},
 	}
@@ -165,17 +163,21 @@ func TestFormInvalidInt(t *testing.T) {
 					}
 					form = &mf.Form
 				}
+
 				got := form.Int64("pizza", 0)
 				if diff := cmp.Diff(test.want, got); diff != "" {
-					t.Errorf("form.Int64: got %v, want %v, diff (-want +got): \n%s", got, test.want, diff)
+					t.Errorf("form.Int64:  mismatch (-want +got): \n%s", diff)
 				}
 				if form.Err() == nil {
-					t.Errorf("form.Err: got nil, want %v", test.err)
+					t.Errorf("form.Err: got nil, want error")
 				}
+
 				return safehttp.Result{}
 			}, &testDispatcher{})
+
 			rec := newResponseRecorder(&strings.Builder{})
 			m.HandleRequest(rec, req)
+
 			if want := 200; rec.status != want {
 				t.Errorf("response status: got %v, want %v", rec.status, want)
 			}
@@ -231,17 +233,20 @@ func TestFormValidUint(t *testing.T) {
 				}
 				form = &mf.Form
 			}
+
 			got := form.Uint64("pizza", 0)
 			if err := form.Err(); err != nil {
 				t.Errorf(`form.Error: got %v, want nil`, err)
 			}
 			if diff := cmp.Diff(test.want, got); diff != "" {
-				t.Errorf("form.Uint64: got %v, want %v, diff (-want +got): \n%s", got, test.want, diff)
+				t.Errorf("form.Uint64:  mismatch (-want +got): \n%s", diff)
 			}
 			return safehttp.Result{}
 		}, &testDispatcher{})
+
 		rec := newResponseRecorder(&strings.Builder{})
 		m.HandleRequest(rec, test.req)
+
 		if want := 200; rec.status != want {
 			t.Errorf("response status: got %v, want %v", rec.status, want)
 		}
@@ -252,7 +257,6 @@ func TestFormInvalidUint(t *testing.T) {
 	tests := []struct {
 		name string
 		reqs []*http.Request
-		err  error
 		want uint64
 	}{
 		{
@@ -274,7 +278,6 @@ func TestFormInvalidUint(t *testing.T) {
 					return multipartReq
 				}(),
 			},
-			err:  errors.New(`strconv.ParseUint: parsing "-1": invalid syntax`),
 			want: 0,
 		},
 		{
@@ -296,7 +299,6 @@ func TestFormInvalidUint(t *testing.T) {
 					return multipartReq
 				}(),
 			},
-			err:  errors.New(`strconv.ParseUint: parsing "18446744073709551630": value out of range`),
 			want: 0,
 		},
 	}
@@ -317,17 +319,20 @@ func TestFormInvalidUint(t *testing.T) {
 					}
 					form = &mf.Form
 				}
+
 				got := form.Uint64("pizza", 0)
 				if diff := cmp.Diff(test.want, got); diff != "" {
-					t.Errorf("form.Uint64: got %v, want %v, diff (-want +got): \n%s", got, test.want, diff)
+					t.Errorf("form.Uint64:  mismatch (-want +got): \n%s", diff)
 				}
 				if form.Err() == nil {
-					t.Errorf("form.Err: got nil, want %v", test.err)
+					t.Errorf("form.Err: got nil, want error")
 				}
 				return safehttp.Result{}
 			}, &testDispatcher{})
+
 			rec := newResponseRecorder(&strings.Builder{})
 			m.HandleRequest(rec, req)
+
 			if want := 200; rec.status != want {
 				t.Errorf("response status: got %v, want %v", rec.status, want)
 			}
@@ -395,18 +400,20 @@ func TestFormValidString(t *testing.T) {
 					}
 					form = &mf.Form
 				}
-				want := test.want[idx]
+
 				got := form.String("pizza", "")
-				if diff := cmp.Diff(want, got); diff != "" {
-					t.Errorf("form.String: got %v, want %v, diff (-want +got): \n%s", got, want, diff)
+				if diff := cmp.Diff(test.want[idx], got); diff != "" {
+					t.Errorf("form.String: diff (-want +got): \n%s", diff)
 				}
 				if err := form.Err(); err != nil {
 					t.Errorf("form.Err: got %v, want nil", err)
 				}
 				return safehttp.Result{}
 			}, &testDispatcher{})
+
 			rec := newResponseRecorder(&strings.Builder{})
 			m.HandleRequest(rec, req)
+
 			if want := 200; rec.status != want {
 				t.Errorf("response status: got %v, want %v", rec.status, want)
 			}
@@ -475,18 +482,20 @@ func TestFormValidFloat64(t *testing.T) {
 					}
 					form = &mf.Form
 				}
-				want := test.want[idx]
+
 				got := form.Float64("pizza", 0.0)
-				if diff := cmp.Diff(want, got); diff != "" {
-					t.Errorf("form.Float64: got %v, want %v, diff (-want +got): \n%s", got, want, diff)
+				if diff := cmp.Diff(test.want[idx], got); diff != "" {
+					t.Errorf("form.Float64: diff (-want +got): \n%s", diff)
 				}
 				if err := form.Err(); err != nil {
 					t.Errorf("form.Err: got %v, want nil", err)
 				}
 				return safehttp.Result{}
 			}, &testDispatcher{})
+
 			rec := newResponseRecorder(&strings.Builder{})
 			m.HandleRequest(rec, req)
+
 			if want := 200; rec.status != want {
 				t.Errorf("response status: got %v, want %v", rec.status, want)
 			}
@@ -498,7 +507,6 @@ func TestFormInvalidFloat64(t *testing.T) {
 	tests := []struct {
 		name string
 		reqs []*http.Request
-		err  error
 		want float64
 	}{
 		{
@@ -520,7 +528,6 @@ func TestFormInvalidFloat64(t *testing.T) {
 					return multipartReq
 				}(),
 			},
-			err:  errors.New(`strconv.ParseFloat: parsing "diavola": invalid syntax`),
 			want: 0.0,
 		},
 		{
@@ -542,7 +549,6 @@ func TestFormInvalidFloat64(t *testing.T) {
 					return multipartReq
 				}(),
 			},
-			err:  errors.New(`strconv.ParseFloat: parsing "1.797693134862315708145274237317043567981e309": value out of range`),
 			want: 0.0,
 		},
 	}
@@ -563,17 +569,21 @@ func TestFormInvalidFloat64(t *testing.T) {
 					}
 					form = &mf.Form
 				}
+
 				got := form.Float64("pizza", 0.0)
 				if diff := cmp.Diff(test.want, got); diff != "" {
-					t.Errorf("form.Float64: got %v, want %v, diff (-want +got): \n%s", got, test.want, diff)
+					t.Errorf("form.Float64:  mismatch (-want +got): \n%s", diff)
 				}
 				if form.Err() == nil {
-					t.Errorf("form.Err: got nil, want %v", test.err)
+					t.Errorf("form.Err: got nil, want error")
 				}
+
 				return safehttp.Result{}
 			}, &testDispatcher{})
+
 			rec := newResponseRecorder(&strings.Builder{})
 			m.HandleRequest(rec, req)
+
 			if want := 200; rec.status != want {
 				t.Errorf("response status: got %v, want %v", rec.status, want)
 			}
@@ -650,18 +660,21 @@ func TestFormValidBool(t *testing.T) {
 					}
 					form = &mf.Form
 				}
-				want := test.want[idx]
+
 				got := form.Bool("pizza", false)
-				if diff := cmp.Diff(want, got); diff != "" {
-					t.Errorf("form.Bool: got %v, want %v, diff (-want +got): \n%s", got, want, diff)
+				if diff := cmp.Diff(test.want[idx], got); diff != "" {
+					t.Errorf("form.Bool: diff (-want +got): \n%s", diff)
 				}
 				if err := form.Err(); err != nil {
 					t.Errorf("form.Err: got %v, want nil", err)
 				}
+
 				return safehttp.Result{}
 			}, &testDispatcher{})
+
 			rec := newResponseRecorder(&strings.Builder{})
 			m.HandleRequest(rec, req)
+
 			if want := 200; rec.status != want {
 				t.Errorf("response status: got %v, want %v", rec.status, want)
 			}
@@ -674,7 +687,6 @@ func TestFormInvalidBool(t *testing.T) {
 		name string
 		reqs []*http.Request
 		want bool
-		err  error
 	}{
 		{
 			name: "Invalid booleans in POST non-multipart request",
@@ -689,7 +701,6 @@ func TestFormInvalidBool(t *testing.T) {
 				return reqs
 			}(),
 			want: false,
-			err:  errors.New(`values of form parameter "pizza" not a boolean`),
 		},
 		{
 			name: "Invalid booleans in POST multipart request",
@@ -710,7 +721,6 @@ func TestFormInvalidBool(t *testing.T) {
 				return reqs
 			}(),
 			want: false,
-			err:  errors.New(`values of form parameter "pizza" not a boolean`),
 		},
 	}
 
@@ -731,146 +741,20 @@ func TestFormInvalidBool(t *testing.T) {
 					}
 					form = &mf.Form
 				}
+
 				got := form.Bool("pizza", false)
 				if diff := cmp.Diff(test.want, got); diff != "" {
-					t.Errorf("form.Bool: got %v, want %v, diff (-want +got): \n%s", got, test.want, diff)
+					t.Errorf("form.Bool:  mismatch (-want +got): \n%s", diff)
 				}
 				if form.Err() == nil {
-					t.Errorf("form.Err: got nil, want %v", test.err)
+					t.Errorf("form.Err: got nil, want error")
 				}
 				return safehttp.Result{}
 			}, &testDispatcher{})
-			rec := newResponseRecorder(&strings.Builder{})
-			m.HandleRequest(rec, req)
-			if want := 200; rec.status != want {
-				t.Errorf("response status: got %v, want %v", rec.status, want)
-			}
-		}
-	}
-}
 
-func TestFormValidSlice(t *testing.T) {
-	// TODO(@mihalimara22): Test other boolean syntax like "t", "f", "0" and "1" here.
-	validSlices := []interface{}{
-		[]int64{-8, 9, -100}, []uint64{8, 9, 10}, []string{"margeritta", "diavola", "calzone"}, []float64{1.3, 8.9, -4.1}, []bool{true, false, true},
-	}
-	// Parsing behaviour of native types that are supported is not included as
-	// it was tested in previous tests.
-	tests := []struct {
-		name string
-		reqs []*http.Request
-	}{
-		{
-			name: "Valid slices in POST non-multipart request",
-			reqs: func() []*http.Request {
-				reqs := []*http.Request{
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza=-8&pizza=9&pizza=-100")),
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza=8&pizza=9&pizza=10")),
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza=margeritta&pizza=diavola&pizza=calzone")),
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza=1.3&pizza=8.9&pizza=-4.1")),
-					httptest.NewRequest("POST", "/", strings.NewReader("pizza=true&pizza=false&pizza=true")),
-				}
-				for _, req := range reqs {
-					req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
-				}
-				return reqs
-			}(),
-		},
-		{
-			name: "Valid slice in POST multipart request",
-			reqs: func() []*http.Request {
-				m := &validSlices
-				multipartReqBody := ""
-				var reqs []*http.Request
-				for _, vals := range *m {
-					s := reflect.ValueOf(vals)
-					for i := 0; i < s.Len(); i++ {
-						multipartReqBody += "--123\r\n" +
-							"Content-Disposition: form-data; name=\"pizza\"\r\n" +
-							"\r\n"
-						multipartReqBody += fmt.Sprintf("%v\r\n", s.Index(i))
-					}
-					multipartReqBody += "--123--\r\n"
-					multipartReq := httptest.NewRequest("POST", "/", strings.NewReader(multipartReqBody))
-					multipartReq.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
-					reqs = append(reqs, multipartReq)
-					multipartReqBody = ""
-				}
-				return reqs
-			}(),
-		},
-	}
-
-	for _, test := range tests {
-		for idx, req := range test.reqs {
-			m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
-				var form *safehttp.Form
-				if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
-					var err error
-					form, err = ir.PostForm()
-					if err != nil {
-						t.Fatalf(`ir.PostForm: got %v, want nil`, err)
-					}
-				} else {
-					mf, err := ir.MultipartForm(32 << 20)
-					if err != nil {
-						t.Fatalf(`ir.MultipartForm: got %v, want nil`, err)
-					}
-					form = &mf.Form
-				}
-				switch want := validSlices[idx].(type) {
-				case []int64:
-					var got []int64
-					form.Slice("pizza", &got)
-					if err := form.Err(); err != nil {
-						t.Errorf(`form.Error: got %v, want nil`, err)
-					}
-					if diff := cmp.Diff(want, got); diff != "" {
-						t.Errorf("form.Slice: got %v, want %v, diff (-want +got): \n%s", got, want, diff)
-					}
-				case []string:
-					var got []string
-					form.Slice("pizza", &got)
-					if err := form.Err(); err != nil {
-						t.Errorf(`form.Error: got %v, want nil`, err)
-					}
-					if diff := cmp.Diff(want, got); diff != "" {
-						t.Errorf("form.Slice: got %v, want %v, diff (-want +got): \n%s", got, want, diff)
-					}
-				case []uint64:
-					var got []uint64
-					form.Slice("pizza", &got)
-					if err := form.Err(); err != nil {
-						t.Errorf(`form.Error: got %v, want nil`, err)
-					}
-					if diff := cmp.Diff(want, got); diff != "" {
-						t.Errorf("form.Slice: got %v, want %v, diff (-want +got): \n%s", got, want, diff)
-					}
-				case []float64:
-					var got []float64
-					form.Slice("pizza", &got)
-					if err := form.Err(); err != nil {
-						t.Errorf(`form.Error: got %v, want nil`, err)
-					}
-					if diff := cmp.Diff(want, got); diff != "" {
-						t.Errorf("form.Slice: got %v, want %v, diff (-want +got): \n%s", got, want, diff)
-					}
-				case []bool:
-					var got []bool
-					form.Slice("pizza", &got)
-					if err := form.Err(); err != nil {
-						t.Errorf(`form.Error: got %v, want nil`, err)
-					}
-					if diff := cmp.Diff(want, got); diff != "" {
-						t.Errorf("form.Slice: got %v, want %v, diff (-want +got): \n%s", got, want, diff)
-					}
-				default:
-					t.Fatalf("type not supported: %T", want)
-				}
-				return safehttp.Result{}
-			}, &testDispatcher{})
 			rec := newResponseRecorder(&strings.Builder{})
 			m.HandleRequest(rec, req)
+
 			if want := 200; rec.status != want {
 				t.Errorf("response status: got %v, want %v", rec.status, want)
 			}
@@ -884,7 +768,6 @@ func TestFormInvalidSlice(t *testing.T) {
 	tests := []struct {
 		name string
 		reqs []*http.Request
-		err  error
 		got  interface{}
 		want interface{}
 	}{
@@ -911,15 +794,6 @@ func TestFormInvalidSlice(t *testing.T) {
 					return multipartReq
 				}(),
 			},
-			err: errors.New(`values of form parameter "pizza" not a boolean`),
-			got: func() interface{} {
-				var got []bool
-				return got
-			}(),
-			want: func() interface{} {
-				var want []bool
-				return want
-			}(),
 		},
 		{
 			name: "Unsupported slice type",
@@ -944,15 +818,6 @@ func TestFormInvalidSlice(t *testing.T) {
 					return multipartReq
 				}(),
 			},
-			err: errors.New(`type not supported in Slice call: *[]int8`),
-			got: func() interface{} {
-				var got []int8
-				return got
-			}(),
-			want: func() interface{} {
-				var want []int8
-				return want
-			}(),
 		},
 	}
 
@@ -973,28 +838,21 @@ func TestFormInvalidSlice(t *testing.T) {
 					}
 					form = &mf.Form
 				}
-				switch got := test.got.(type) {
-				case []int8:
-					form.Slice("pizza", &got)
-					if diff := cmp.Diff(test.want, got); diff != "" {
-						t.Errorf("form.Slice: got %v, want %v", got, test.want)
-					}
-					if form.Err() == nil {
-						t.Errorf("form.Err: got nil, want %v", test.err)
-					}
-				case []bool:
-					form.Slice("pizza", &got)
-					if diff := cmp.Diff(test.want, got); diff != "" {
-						t.Errorf("form.Slice: got %v, want %v", got, test.want)
-					}
-					if form.Err() == nil {
-						t.Errorf("form.Err: got nil, want %v", test.err)
-					}
+
+				form.Slice("pizza", test.got)
+				if diff := cmp.Diff(test.want, test.got); diff != "" {
+					t.Errorf("form.Slice: diff (-want +got): \n%v", diff)
+				}
+				if form.Err() == nil {
+					t.Errorf("form.Err: got nil, want error")
 				}
+
 				return safehttp.Result{}
 			}, &testDispatcher{})
+
 			rec := newResponseRecorder(&strings.Builder{})
 			m.HandleRequest(rec, req)
+
 			if want := 200; rec.status != want {
 				t.Errorf("response status: got %v, want %v", rec.status, want)
 			}
@@ -1003,19 +861,21 @@ func TestFormInvalidSlice(t *testing.T) {
 }
 
 func TestFormErrorHandling(t *testing.T) {
-	test := struct {
+	tests := []struct {
 		name string
-		reqs []*http.Request
-		errs []error
+		req  *http.Request
 	}{
-		name: "Erros occuring in requests",
-		reqs: []*http.Request{
-			func() *http.Request {
+		{
+			name: "Post form errors",
+			req: func() *http.Request {
 				postReq := httptest.NewRequest("POST", "/", strings.NewReader("pizzaInt=diavola&pizzaBool=true&pizzaUint=-13"))
 				postReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 				return postReq
 			}(),
-			func() *http.Request {
+		},
+		{
+			name: "Multipart form errors",
+			req: func() *http.Request {
 				multipartReqBody := "--123\r\n" +
 					"Content-Disposition: form-data; name=\"pizzaInt\"\r\n" +
 					"\r\n" +
@@ -1034,10 +894,9 @@ func TestFormErrorHandling(t *testing.T) {
 				return multipartReq
 			}(),
 		},
-		errs: []error{errors.New(`strconv.ParseInt: parsing "diavola": invalid syntax`), errors.New(`strconv.ParseUint: parsing "-13": invalid syntax`)},
 	}
 
-	for _, req := range test.reqs {
+	for _, tc := range tests {
 		m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
 			var form *safehttp.Form
 			if !strings.HasPrefix(ir.Header.Get("Content-Type"), "multipart/form-data") {
@@ -1053,37 +912,222 @@ func TestFormErrorHandling(t *testing.T) {
 				}
 				form = &mf.Form
 			}
-			var wantInt int64 = 0
-			gotInt := form.Int64("pizzaInt", 0)
-			if diff := cmp.Diff(wantInt, gotInt); diff != "" {
-				t.Errorf("form.Int64: got %v, want %v, diff (-want +got): \n%s", gotInt, wantInt, diff)
+
+			gotInt64 := form.Int64("pizzaInt", 0)
+			if diff := cmp.Diff(int64(0), gotInt64); diff != "" {
+				t.Errorf("form.Int64: diff (-want +got): \n%s", diff)
 			}
-			if form.Err() == nil {
-				t.Errorf("form.Err: got nil, want %v", test.errs[0])
+			prevErr := form.Err()
+			if prevErr == nil {
+				t.Errorf("form.Err: got nil, want error")
 			}
-			wantBool := true
+
 			gotBool := form.Bool("pizzaBool", false)
-			if diff := cmp.Diff(wantBool, gotBool); diff != "" {
-				t.Errorf("form.Bool: got %v, want %v, diff (-want +got): \n%s", gotBool, wantBool, diff)
+			if diff := cmp.Diff(true, gotBool); diff != "" {
+				t.Errorf("form.Bool: diff (-want +got): \n%s", diff)
 			}
 			// We expect the same error here becase calling form.Bool succeeds
-			if form.Err() == nil {
-				t.Errorf("form.Err: got nil, want %v", test.errs[0])
+			if err := form.Err(); prevErr.Error() != err.Error() {
+				t.Errorf("form.Err: want %v, got %v", prevErr, err)
 			}
-			var wantUint uint64 = 0
-			gotUint := form.Uint64("pizzaUint", 0)
-			if diff := cmp.Diff(wantUint, gotUint); diff != "" {
-				t.Errorf("form.Uint64: got %v, want %v, diff (-want +got): \n%s", gotUint, wantUint, diff)
+
+			gotUint64 := form.Uint64("pizzaUint", 0)
+			if diff := cmp.Diff(uint64(0), gotUint64); diff != "" {
+				t.Errorf("form.Uint64: diff (-want +got): \n%s", diff)
 			}
 			if form.Err() == nil {
-				t.Errorf("form.Err: got nil, want %v", test.errs[1])
+				t.Errorf("form.Err: got nil, want error")
 			}
+
 			return safehttp.Result{}
 		}, &testDispatcher{})
+
 		rec := newResponseRecorder(&strings.Builder{})
-		m.HandleRequest(rec, req)
+		m.HandleRequest(rec, tc.req)
+
 		if want := 200; rec.status != want {
 			t.Errorf("response status: got %v, want %v", rec.status, want)
 		}
 	}
 }
+
+func TestFormValidSlicePost(t *testing.T) {
+	tests := []struct {
+		req         *http.Request
+		placeholder interface{}
+		want        interface{}
+	}{
+		{
+			req: func() *http.Request {
+				req := httptest.NewRequest("POST", "/", strings.NewReader("pizza=-8&pizza=9&pizza=-100"))
+				req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+				return req
+			}(),
+			placeholder: &[]int64{},
+			want:        &[]int64{-8, 9, -100},
+		},
+		{
+			req: func() *http.Request {
+				req := httptest.NewRequest("POST", "/", strings.NewReader("pizza=8&pizza=9&pizza=10"))
+				req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+				return req
+			}(),
+			placeholder: &[]uint64{},
+			want:        &[]uint64{8, 9, 10},
+		},
+		{
+			req: func() *http.Request {
+				req := httptest.NewRequest("POST", "/", strings.NewReader("pizza=margeritta&pizza=diavola&pizza=calzone"))
+				req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+				return req
+			}(),
+			placeholder: &[]string{},
+			want:        &[]string{"margeritta", "diavola", "calzone"},
+		},
+		{
+			req: func() *http.Request {
+				req := httptest.NewRequest("POST", "/", strings.NewReader("pizza=1.3&pizza=8.9&pizza=-4.1"))
+				req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+				return req
+			}(),
+			placeholder: &[]float64{},
+			want:        &[]float64{1.3, 8.9, -4.1},
+		},
+		{
+			req: func() *http.Request {
+				req := httptest.NewRequest("POST", "/", strings.NewReader("pizza=true&pizza=false&pizza=T"))
+				req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+				return req
+			}(),
+			placeholder: &[]bool{},
+			want:        &[]bool{true, false, true},
+		},
+	}
+
+	for _, tc := range tests {
+		m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+			form, err := ir.PostForm()
+			if err != nil {
+				t.Fatalf(`ir.PostForm: got err %v, want nil`, err)
+			}
+
+			form.Slice("pizza", tc.placeholder)
+			if err := form.Err(); err != nil {
+				t.Errorf(`form.Err: got err %v, want nil`, err)
+			}
+
+			if diff := cmp.Diff(tc.want, tc.placeholder); diff != "" {
+				t.Errorf("form.Slice: diff (-want +got): \n%v", diff)
+			}
+
+			return safehttp.Result{}
+		}, &testDispatcher{})
+
+		rec := newResponseRecorder(&strings.Builder{})
+		m.HandleRequest(rec, tc.req)
+
+		if respStatus, want := rec.status, 200; respStatus != want {
+			t.Errorf("response status: got %v, want %v", respStatus, want)
+		}
+	}
+
+}
+
+func TestFormValidSliceMultipart(t *testing.T) {
+	multipartBody := template.Must(template.New("multipart").Parse(
+		"{{ range . -}}" +
+			"--123\r\n" +
+			"Content-Disposition: form-data; name=\"pizza\"\r\n" +
+			"\r\n" +
+			"{{ . }}\r\n" +
+			"{{ end }}" +
+			"--123--\r\n"))
+
+	tests := []struct {
+		req         *http.Request
+		placeholder interface{}
+		want        interface{}
+	}{
+		{
+			req: func() *http.Request {
+				b := &strings.Builder{}
+				multipartBody.Execute(b, []string{"-8", "9", "-100"})
+				req := httptest.NewRequest("POST", "/", strings.NewReader(b.String()))
+				req.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
+				return req
+			}(),
+			placeholder: &[]int64{},
+			want:        &[]int64{-8, 9, -100},
+		},
+		{
+			req: func() *http.Request {
+				b := &strings.Builder{}
+				multipartBody.Execute(b, []string{"8", "9", "10"})
+				req := httptest.NewRequest("POST", "/", strings.NewReader(b.String()))
+				req.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
+				return req
+			}(),
+			placeholder: &[]uint64{},
+			want:        &[]uint64{8, 9, 10},
+		},
+		{
+			req: func() *http.Request {
+				b := &strings.Builder{}
+				multipartBody.Execute(b, []string{"margeritta", "diavola", "calzone"})
+				req := httptest.NewRequest("POST", "/", strings.NewReader(b.String()))
+				req.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
+				return req
+			}(),
+			placeholder: &[]string{},
+			want:        &[]string{"margeritta", "diavola", "calzone"},
+		},
+		{
+			req: func() *http.Request {
+				b := &strings.Builder{}
+				multipartBody.Execute(b, []string{"1.3", "8.9", "-4.1"})
+				req := httptest.NewRequest("POST", "/", strings.NewReader(b.String()))
+				req.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
+				return req
+			}(),
+			placeholder: &[]float64{},
+			want:        &[]float64{1.3, 8.9, -4.1},
+		},
+		{
+			req: func() *http.Request {
+				b := &strings.Builder{}
+				multipartBody.Execute(b, []string{"true", "false", "true"})
+				req := httptest.NewRequest("POST", "/", strings.NewReader(b.String()))
+				req.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
+				return req
+			}(),
+			placeholder: &[]bool{},
+			want:        &[]bool{true, false, true},
+		},
+	}
+
+	for _, tc := range tests {
+		m := safehttp.NewMachinery(func(rw safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
+			form, err := ir.MultipartForm(32 << 20)
+			if err != nil {
+				t.Fatalf(`ir.MultipartForm: got err %v, want nil`, err)
+			}
+
+			form.Slice("pizza", tc.placeholder)
+			if err := form.Err(); err != nil {
+				t.Errorf(`form.Err: got err %v, want nil`, err)
+			}
+
+			if diff := cmp.Diff(tc.want, tc.placeholder); diff != "" {
+				t.Errorf("form.Slice: diff (-want +got): \n%v", diff)
+			}
+			return safehttp.Result{}
+		}, &testDispatcher{})
+
+		rec := newResponseRecorder(&strings.Builder{})
+		m.HandleRequest(rec, tc.req)
+
+		if respStatus, want := rec.status, 200; respStatus != want {
+			t.Errorf("response status: got %v, want %v", respStatus, want)
+		}
+	}
+}
diff --git a/safehttp/mux_test.go b/safehttp/mux_test.go
index d80dffc..541e226 100644
--- a/safehttp/mux_test.go
+++ b/safehttp/mux_test.go
@@ -12,63 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package safehttp
+package safehttp_test
 
 import (
-	"io"
 	"net/http"
 	"net/http/httptest"
 	"strings"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-safeweb/safehttp"
 	"github.com/google/safehtml"
-	"github.com/google/safehtml/template"
 )
 
-type testDispatcher struct{}
-
-func (testDispatcher) Write(rw http.ResponseWriter, resp Response) error {
-	switch x := resp.(type) {
-	case safehtml.HTML:
-		_, err := rw.Write([]byte(x.String()))
-		return err
-	default:
-		panic("not a safe response type")
-	}
-}
-
-func (testDispatcher) ExecuteTemplate(rw http.ResponseWriter, t Template, data interface{}) error {
-	switch x := t.(type) {
-	case *template.Template:
-		return x.Execute(rw, data)
-	default:
-		panic("not a safe response type")
-	}
-}
-
-type responseRecorder struct {
-	header http.Header
-	writer io.Writer
-	status int
-}
-
-func newResponseRecorder(w io.Writer) *responseRecorder {
-	return &responseRecorder{header: http.Header{}, writer: w, status: http.StatusOK}
-}
-
-func (r *responseRecorder) Header() http.Header {
-	return r.header
-}
-
-func (r *responseRecorder) WriteHeader(statusCode int) {
-	r.status = statusCode
-}
-
-func (r *responseRecorder) Write(data []byte) (int, error) {
-	return r.writer.Write(data)
-}
-
 func TestMuxOneHandlerOneRequest(t *testing.T) {
 	var test = []struct {
 		name       string
@@ -108,12 +64,12 @@ func TestMuxOneHandlerOneRequest(t *testing.T) {
 
 	for _, tt := range test {
 		t.Run(tt.name, func(t *testing.T) {
-			mux := NewServeMux(testDispatcher{}, "foo.com")
+			mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
 
-			h := HandlerFunc(func(w ResponseWriter, r *IncomingRequest) Result {
+			h := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 				return w.Write(safehtml.HTMLEscaped("<h1>Hello World!</h1>"))
 			})
-			mux.Handle("/", MethodGet, h)
+			mux.Handle("/", safehttp.MethodGet, h)
 
 			b := &strings.Builder{}
 			rw := newResponseRecorder(b)
@@ -139,7 +95,7 @@ func TestMuxServeTwoHandlers(t *testing.T) {
 	var tests = []struct {
 		name        string
 		req         *http.Request
-		hf          HandlerFunc
+		hf          safehttp.Handler
 		wantStatus  int
 		wantHeaders map[string][]string
 		wantBody    string
@@ -147,7 +103,7 @@ func TestMuxServeTwoHandlers(t *testing.T) {
 		{
 			name: "GET Handler",
 			req:  httptest.NewRequest("GET", "http://foo.com/bar", nil),
-			hf: HandlerFunc(func(w ResponseWriter, r *IncomingRequest) Result {
+			hf: safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 				return w.Write(safehtml.HTMLEscaped("<h1>Hello World! GET</h1>"))
 			}),
 			wantStatus:  200,
@@ -157,7 +113,7 @@ func TestMuxServeTwoHandlers(t *testing.T) {
 		{
 			name: "POST Handler",
 			req:  httptest.NewRequest("POST", "http://foo.com/bar", nil),
-			hf: HandlerFunc(func(w ResponseWriter, r *IncomingRequest) Result {
+			hf: safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 				return w.Write(safehtml.HTMLEscaped("<h1>Hello World! POST</h1>"))
 			}),
 			wantStatus:  200,
@@ -167,9 +123,9 @@ func TestMuxServeTwoHandlers(t *testing.T) {
 	}
 
 	d := testDispatcher{}
-	mux := NewServeMux(d, "foo.com")
-	mux.Handle("/bar", MethodGet, tests[0].hf)
-	mux.Handle("/bar", MethodPost, tests[1].hf)
+	mux := safehttp.NewServeMux(d, "foo.com")
+	mux.Handle("/bar", safehttp.MethodGet, tests[0].hf)
+	mux.Handle("/bar", safehttp.MethodPost, tests[1].hf)
 
 	for _, test := range tests {
 		b := &strings.Builder{}
@@ -191,12 +147,12 @@ func TestMuxServeTwoHandlers(t *testing.T) {
 
 func TestMuxHandleSameMethodTwice(t *testing.T) {
 	d := testDispatcher{}
-	mux := NewServeMux(d, "foo.com")
+	mux := safehttp.NewServeMux(d, "foo.com")
 
-	registeredHandler := HandlerFunc(func(w ResponseWriter, r *IncomingRequest) Result {
+	registeredHandler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
 		return w.Write(safehtml.HTMLEscaped("<h1>Hello World!</h1>"))
 	})
-	mux.Handle("/bar", MethodGet, registeredHandler)
+	mux.Handle("/bar", safehttp.MethodGet, registeredHandler)
 
 	defer func() {
 		if r := recover(); r == nil {
@@ -204,5 +160,153 @@ func TestMuxHandleSameMethodTwice(t *testing.T) {
 		}
 	}()
 
-	mux.Handle("/bar", MethodGet, registeredHandler)
+	mux.Handle("/bar", safehttp.MethodGet, registeredHandler)
+}
+
+type setHeaderInterceptor struct {
+	name  string
+	value string
+}
+
+func (p setHeaderInterceptor) Before(w safehttp.ResponseWriter, _ *safehttp.IncomingRequest) safehttp.Result {
+	if err := w.Header().Set(p.name, p.value); err != nil {
+		return w.ServerError(safehttp.StatusInternalServerError)
+	}
+	return safehttp.Result{}
+}
+
+type internalErrorInterceptor struct{}
+
+func (internalErrorInterceptor) Before(w safehttp.ResponseWriter, _ *safehttp.IncomingRequest) safehttp.Result {
+	return w.ServerError(safehttp.StatusInternalServerError)
+}
+
+type claimHeaderInterceptor struct {
+	headerToClaim string
+	setValue      func([]string)
+}
+
+func (p *claimHeaderInterceptor) Before(w safehttp.ResponseWriter, _ *safehttp.IncomingRequest) safehttp.Result {
+	f, err := w.Header().Claim(p.headerToClaim)
+	if err != nil {
+		return w.ServerError(safehttp.StatusInternalServerError)
+	}
+	p.setValue = f
+	return safehttp.Result{}
+}
+
+func (p *claimHeaderInterceptor) SetHeader(value string) {
+	p.setValue([]string{value})
+}
+
+func TestMuxInterceptors(t *testing.T) {
+	tests := []struct {
+		name        string
+		mux         *safehttp.ServeMux
+		wantStatus  int
+		wantHeaders map[string][]string
+		wantBody    string
+	}{
+		{
+			name: "Install ServeMux Interceptor before handler registration",
+			mux: func() *safehttp.ServeMux {
+				mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
+				mux.Install("test", setHeaderInterceptor{
+					name:  "Foo",
+					value: "bar",
+				})
+
+				registeredHandler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+					return w.Write(safehtml.HTMLEscaped("<h1>Hello World!</h1>"))
+				})
+				mux.Handle("/bar", safehttp.MethodGet, registeredHandler)
+				return mux
+			}(),
+			wantStatus:  200,
+			wantHeaders: map[string][]string{"Foo": {"bar"}},
+			wantBody:    "&lt;h1&gt;Hello World!&lt;/h1&gt;",
+		},
+		{
+			name: "Install ServeMux interceptor after handler registration",
+			mux: func() *safehttp.ServeMux {
+				mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
+
+				registeredHandler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+					return w.Write(safehtml.HTMLEscaped("<h1>Hello World!</h1>"))
+				})
+				mux.Handle("/bar", safehttp.MethodGet, registeredHandler)
+
+				mux.Install("test", setHeaderInterceptor{
+					name:  "Foo",
+					value: "bar",
+				})
+				return mux
+			}(),
+			wantStatus:  200,
+			wantHeaders: map[string][]string{"Foo": {"bar"}},
+			wantBody:    "&lt;h1&gt;Hello World!&lt;/h1&gt;",
+		},
+		{
+			name: "Install Interrupting Interceptor",
+			mux: func() *safehttp.ServeMux {
+				mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
+				mux.Install("test", internalErrorInterceptor{})
+
+				registeredHandler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+					return w.Write(safehtml.HTMLEscaped("<h1>Hello World!</h1>"))
+				})
+				mux.Handle("/bar", safehttp.MethodGet, registeredHandler)
+
+				return mux
+			}(),
+			wantStatus: 500,
+			wantHeaders: map[string][]string{
+				"Content-Type":           {"text/plain; charset=utf-8"},
+				"X-Content-Type-Options": {"nosniff"},
+			},
+			wantBody: "Internal Server Error\n",
+		},
+		{
+			name: "Handler Communication With ServeMux Interceptor",
+			mux: func() *safehttp.ServeMux {
+				mux := safehttp.NewServeMux(testDispatcher{}, "foo.com")
+				mux.Install("claim", &claimHeaderInterceptor{headerToClaim: "Foo"})
+
+				registeredHandler := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+					p := w.Interceptor("claim").(*claimHeaderInterceptor)
+					p.SetHeader("bar")
+					return w.Write(safehtml.HTMLEscaped("<h1>Hello World!</h1>"))
+				})
+				mux.Handle("/bar", safehttp.MethodGet, registeredHandler)
+
+				return mux
+			}(),
+			wantStatus:  200,
+			wantHeaders: map[string][]string{"Foo": {"bar"}},
+			wantBody:    "&lt;h1&gt;Hello World!&lt;/h1&gt;",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			b := &strings.Builder{}
+			rw := newResponseRecorder(b)
+
+			req := httptest.NewRequest("GET", "http://foo.com/bar", nil)
+
+			tt.mux.ServeHTTP(rw, req)
+
+			if rw.status != tt.wantStatus {
+				t.Errorf("rw.status: got %v want %v", rw.status, tt.wantStatus)
+			}
+
+			if diff := cmp.Diff(tt.wantHeaders, map[string][]string(rw.header)); diff != "" {
+				t.Errorf("rw.header mismatch (-want +got):\n%s", diff)
+			}
+
+			if got := b.String(); got != tt.wantBody {
+				t.Errorf("response body: got %q want %q", got, tt.wantBody)
+			}
+		})
+	}
 }
diff --git a/safehttp/plugins/htmlinject/htmlinject_test.go b/safehttp/plugins/htmlinject/htmlinject_test.go
new file mode 100644
index 0000000..9d67964
--- /dev/null
+++ b/safehttp/plugins/htmlinject/htmlinject_test.go
@@ -0,0 +1,224 @@
+package htmlinject
+
+import (
+	"html/template"
+	"os"
+	"strings"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func ExampleTransform() {
+	const in = `
+<html>
+<head>
+<link rel=preload as="script" src="gopher.js">
+</head>
+<body>
+{{.Content}}
+<script type="application/javascript">alert("script")</script>
+<form>
+First name:<br>
+<input type="text" name="firstname"><br>
+Last name:<br>
+<input type="text" name="lastname">
+</form>
+</body>
+</html>
+`
+	got, err := Transform(strings.NewReader(in), CSPNoncesDefault, XSRFTokensDefault)
+	if err != nil {
+		// handle error
+		panic(err)
+	}
+	template.Must(template.New("example transform").Funcs(map[string]interface{}{
+		"XSRFToken": func() string { return "XSRFToken-secret" },
+		"CSPNonce":  func() string { return "CSPNonce-secret" },
+	}).Parse(got)).Execute(os.Stdout, map[string]string{"Content": "This is some content"})
+	// Output:
+	// <html>
+	// <head>
+	// <link nonce="CSPNonce-secret" rel=preload as="script" src="gopher.js">
+	// </head>
+	// <body>
+	// This is some content
+	// <script nonce="CSPNonce-secret" type="application/javascript">alert("script")</script>
+	// <form><input type="hidden" name="xsrf-token" value="XSRFToken-secret">
+	// First name:<br>
+	// <input type="text" name="firstname"><br>
+	// Last name:<br>
+	// <input type="text" name="lastname">
+	// </form>
+	// </body>
+	// </html>
+}
+
+func BenchmarkTransform(b *testing.B) {
+	b.ReportAllocs()
+	var (
+		config = []Config{CSPNoncesDefault, XSRFTokensDefault}
+		in     = `
+<html>
+<head>
+<link rel=preload as="script" src="gopher.js">
+</head>
+<body>
+<script type="application/javascript">alert("script")</script>
+<form>
+  First name:<br>
+  <input type="text" name="firstname"><br>
+  Last name:<br>
+  <input type="text" name="lastname">
+</form>
+</body>
+</html>
+`
+		want = `
+<html>
+<head>
+<link nonce="{{CSPNonce}}" rel=preload as="script" src="gopher.js">
+</head>
+<body>
+<script nonce="{{CSPNonce}}" type="application/javascript">alert("script")</script>
+<form><input type="hidden" name="xsrf-token" value="{{XSRFToken}}">
+  First name:<br>
+  <input type="text" name="firstname"><br>
+  Last name:<br>
+  <input type="text" name="lastname">
+</form>
+</body>
+</html>
+`
+	)
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		got, err := Transform(strings.NewReader(in), config...)
+		if err != nil {
+			b.Fatalf("Transform: got err %q, didn't want one", err)
+		}
+		if got != want {
+			b.Errorf("got %q, want %q", got, want)
+		}
+	}
+}
+func TestTransform(t *testing.T) {
+	var tests = []struct {
+		name     string
+		config   []Config
+		in, want string
+	}{
+		{
+			name: "nothing to change",
+			in: `
+<html>
+<header><title>This is title</title></header>
+<body>
+Hello world
+</body>
+</html>
+`,
+			want: `
+<html>
+<header><title>This is title</title></header>
+<body>
+Hello world
+</body>
+</html>
+`,
+		},
+		{
+			name:   "add CSP nonces",
+			config: []Config{CSPNoncesDefault},
+			in: `
+<html>
+<head>
+<link rel=preload as="script" src="gopher.js">
+</head>
+<body>
+<script type="application/javascript">alert("script")</script>
+</body>
+</html>
+`,
+			want: `
+<html>
+<head>
+<link nonce="{{CSPNonce}}" rel=preload as="script" src="gopher.js">
+</head>
+<body>
+<script nonce="{{CSPNonce}}" type="application/javascript">alert("script")</script>
+</body>
+</html>
+`,
+		},
+		{
+			name:   "add XSRF protection",
+			config: []Config{XSRFTokensDefault},
+			in: `
+<form>
+  First name:<br>
+  <input type="text" name="firstname"><br>
+  Last name:<br>
+  <input type="text" name="lastname">
+</form>
+`,
+			want: `
+<form><input type="hidden" name="xsrf-token" value="{{XSRFToken}}">
+  First name:<br>
+  <input type="text" name="firstname"><br>
+  Last name:<br>
+  <input type="text" name="lastname">
+</form>
+`,
+		},
+		{
+			name:   "all configs",
+			config: []Config{CSPNoncesDefault, XSRFTokensDefault},
+			in: `
+<html>
+<head>
+<link rel="stylesheet" href="styles.css">
+<link rel=preload as="script" src="gopher.js">
+</head>
+<body>
+<script type="application/javascript">alert("script")</script>
+<form>
+  First name:<br>
+  <input type="text" name="firstname"><br>
+  Last name:<br>
+  <input type="text" name="lastname">
+</form>
+</body>
+</html>
+`,
+			want: `
+<html>
+<head>
+<link rel="stylesheet" href="styles.css">
+<link nonce="{{CSPNonce}}" rel=preload as="script" src="gopher.js">
+</head>
+<body>
+<script nonce="{{CSPNonce}}" type="application/javascript">alert("script")</script>
+<form><input type="hidden" name="xsrf-token" value="{{XSRFToken}}">
+  First name:<br>
+  <input type="text" name="firstname"><br>
+  Last name:<br>
+  <input type="text" name="lastname">
+</form>
+</body>
+</html>
+`,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := Transform(strings.NewReader(tt.in), tt.config...)
+			if err != nil {
+				t.Fatalf("Transform: got err %q, didn't want one", err)
+			}
+			if diff := cmp.Diff(tt.want, got); diff != "" {
+				t.Errorf("-want +got %s", diff)
+			}
+		})
+	}
+}
diff --git a/safehttp/plugins/xsrf/xsrf_test.go b/safehttp/plugins/xsrf/xsrf_test.go
new file mode 100644
index 0000000..65ae013
--- /dev/null
+++ b/safehttp/plugins/xsrf/xsrf_test.go
@@ -0,0 +1,249 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package xsrf_test
+
+import (
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/plugins/xsrf"
+	"github.com/google/go-safeweb/safehttp/safehttptest"
+	"golang.org/x/net/xsrftoken"
+	"strings"
+	"testing"
+)
+
+type userIdentifier struct{}
+
+func (userIdentifier) UserID(r *safehttp.IncomingRequest) (string, error) {
+	return "1234", nil
+}
+
+func TestXSRFTokenPost(t *testing.T) {
+	tests := []struct {
+		name       string
+		host       string
+		path       string
+		userID     string
+		wantStatus int
+		wantHeader map[string][]string
+		wantBody   string
+	}{
+		{
+			name:       "Valid token",
+			userID:     "1234",
+			host:       "foo.com",
+			path:       "/pizza",
+			wantStatus: 200,
+			wantHeader: map[string][]string{},
+			wantBody:   "",
+		},
+		{
+			name:       "Invalid host in token generation",
+			userID:     "1234",
+			host:       "bar.com",
+			path:       "/pizza",
+			wantStatus: 403,
+			wantHeader: map[string][]string{
+				"Content-Type":           {"text/plain; charset=utf-8"},
+				"X-Content-Type-Options": {"nosniff"},
+			},
+			wantBody: "Forbidden\n",
+		},
+		{
+			name:       "Invalid path in token generation",
+			userID:     "1234",
+			host:       "foo.com",
+			path:       "spaghetti",
+			wantStatus: 403,
+			wantHeader: map[string][]string{
+				"Content-Type":           {"text/plain; charset=utf-8"},
+				"X-Content-Type-Options": {"nosniff"},
+			},
+			wantBody: "Forbidden\n",
+		},
+		{
+			name:       "Invalid userID in token generation",
+			userID:     "5678",
+			host:       "foo.com",
+			path:       "/pizza",
+			wantStatus: 403,
+			wantHeader: map[string][]string{
+				"Content-Type":           {"text/plain; charset=utf-8"},
+				"X-Content-Type-Options": {"nosniff"},
+			},
+			wantBody: "Forbidden\n",
+		},
+	}
+	for _, test := range tests {
+		i := xsrf.Interceptor{AppKey: "xsrf", Identifier: userIdentifier{}}
+		tok := xsrftoken.Generate("xsrf", test.userID, test.host+test.path)
+		req := safehttptest.NewRequest("POST", "http://foo.com/pizza", strings.NewReader(xsrf.TokenKey+"="+tok))
+		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+		rec := safehttptest.NewResponseRecorder()
+		i.Before(rec.ResponseWriter, req)
+
+		if rec.Status() != test.wantStatus {
+			t.Errorf("response status: got %v, want %v", rec.Status(), test.wantStatus)
+		}
+		if diff := cmp.Diff(test.wantHeader, map[string][]string(rec.Header())); diff != "" {
+			t.Errorf("rec.Header() mismatch (-want +got):\n%s", diff)
+		}
+		if got := rec.Body(); got != test.wantBody {
+			t.Errorf("response body: got %q want %q", got, test.wantBody)
+		}
+	}
+}
+
+func TestXSRFTokenMultipart(t *testing.T) {
+	tests := []struct {
+		name       string
+		host       string
+		path       string
+		userID     string
+		wantStatus int
+		wantHeader map[string][]string
+		wantBody   string
+	}{
+		{
+			name:       "Valid token",
+			userID:     "1234",
+			host:       "foo.com",
+			path:       "/pizza",
+			wantStatus: 200,
+			wantHeader: map[string][]string{},
+			wantBody:   "",
+		},
+		{
+			name:       "Invalid host in token generation",
+			userID:     "1234",
+			host:       "bar.com",
+			path:       "/pizza",
+			wantStatus: 403,
+			wantHeader: map[string][]string{
+				"Content-Type":           {"text/plain; charset=utf-8"},
+				"X-Content-Type-Options": {"nosniff"},
+			},
+			wantBody: "Forbidden\n",
+		},
+		{
+			name:       "Invalid path in token generation",
+			userID:     "1234",
+			host:       "foo.com",
+			path:       "spaghetti",
+			wantStatus: 403,
+			wantHeader: map[string][]string{
+				"Content-Type":           {"text/plain; charset=utf-8"},
+				"X-Content-Type-Options": {"nosniff"},
+			},
+			wantBody: "Forbidden\n",
+		},
+		{
+			name:       "Invalid userID in token generation",
+			userID:     "5678",
+			host:       "foo.com",
+			path:       "/pizza",
+			wantStatus: 403,
+			wantHeader: map[string][]string{
+				"Content-Type":           {"text/plain; charset=utf-8"},
+				"X-Content-Type-Options": {"nosniff"},
+			},
+			wantBody: "Forbidden\n",
+		},
+	}
+	for _, test := range tests {
+		i := xsrf.Interceptor{AppKey: "xsrf", Identifier: userIdentifier{}}
+		tok := xsrftoken.Generate("xsrf", test.userID, test.host+test.path)
+		b := "--123\r\n" +
+			"Content-Disposition: form-data; name=\"xsrf-token\"\r\n" +
+			"\r\n" +
+			tok + "\r\n" +
+			"--123--\r\n"
+		req := safehttptest.NewRequest("POST", "http://foo.com/pizza", strings.NewReader(b))
+		req.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
+
+		rec := safehttptest.NewResponseRecorder()
+		i.Before(rec.ResponseWriter, req)
+
+		if rec.Status() != test.wantStatus {
+			t.Errorf("response status: got %v, want %v", rec.Status(), test.wantStatus)
+		}
+		if diff := cmp.Diff(test.wantHeader, map[string][]string(rec.Header())); diff != "" {
+			t.Errorf("rw.header mismatch (-want +got):\n%s", diff)
+		}
+		if got := rec.Body(); got != test.wantBody {
+			t.Errorf("response body: got %q want %q", got, test.wantBody)
+		}
+	}
+}
+
+func TestXSRFMissingToken(t *testing.T) {
+	tests := []struct {
+		name       string
+		req        *safehttp.IncomingRequest
+		wantStatus int
+		wantHeader map[string][]string
+		wantBody   string
+	}{
+		{
+			name: "Missing token in POST request",
+			req: func() *safehttp.IncomingRequest {
+				req := safehttptest.NewRequest("POST", "/", strings.NewReader("foo=bar"))
+				req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+				return req
+			}(),
+			wantStatus: 401,
+			wantHeader: map[string][]string{
+				"Content-Type":           {"text/plain; charset=utf-8"},
+				"X-Content-Type-Options": {"nosniff"},
+			},
+			wantBody: "Unauthorized\n",
+		},
+		{
+			name: "Missing token in multipart request",
+			req: func() *safehttp.IncomingRequest {
+				b := "--123\r\n" +
+					"Content-Disposition: form-data; name=\"foo\"\r\n" +
+					"\r\n" +
+					"bar\r\n" +
+					"--123--\r\n"
+				req := safehttptest.NewRequest("POST", "/", strings.NewReader(b))
+				req.Header.Set("Content-Type", `multipart/form-data; boundary="123"`)
+				return req
+			}(),
+			wantStatus: 401,
+			wantHeader: map[string][]string{
+				"Content-Type":           {"text/plain; charset=utf-8"},
+				"X-Content-Type-Options": {"nosniff"},
+			},
+			wantBody: "Unauthorized\n",
+		},
+	}
+	for _, test := range tests {
+		i := xsrf.Interceptor{AppKey: "xsrf", Identifier: userIdentifier{}}
+		rec := safehttptest.NewResponseRecorder()
+		i.Before(rec.ResponseWriter, test.req)
+
+		if rec.Status() != test.wantStatus {
+			t.Errorf("response status: got %v, want %v", rec.Status(), test.wantStatus)
+		}
+		if diff := cmp.Diff(test.wantHeader, map[string][]string(rec.Header())); diff != "" {
+			t.Errorf("rw.header mismatch (-want +got):\n%s", diff)
+		}
+		if got := rec.Body(); got != test.wantBody {
+			t.Errorf("response body: got %q want %q", got, test.wantBody)
+		}
+	}
+}
diff --git a/safehttp/recorder_dispatcher_test.go b/safehttp/recorder_dispatcher_test.go
index 76822d0..d17106c 100644
--- a/safehttp/recorder_dispatcher_test.go
+++ b/safehttp/recorder_dispatcher_test.go
@@ -56,6 +56,7 @@ type responseRecorder struct {
 
 func newResponseRecorder(w io.Writer) *responseRecorder {
 	return &responseRecorder{
+		header: http.Header{},
 		writer: w,
 		status: http.StatusOK,
 	}
diff --git a/safehttp/safehttptest/recorder.go b/safehttp/safehttptest/recorder.go
index 5222848..f3df891 100644
--- a/safehttp/safehttptest/recorder.go
+++ b/safehttp/safehttptest/recorder.go
@@ -61,7 +61,7 @@ func NewResponseRecorder() *ResponseRecorder {
 	return &ResponseRecorder{
 		rw:             rw,
 		b:              &b,
-		ResponseWriter: safehttp.NewResponseWriter(testDispatcher{}, rw),
+		ResponseWriter: safehttp.NewResponseWriter(testDispatcher{}, rw, nil),
 	}
 }
 
@@ -73,7 +73,7 @@ func NewResponseRecorderFromDispatcher(d safehttp.Dispatcher) *ResponseRecorder
 	return &ResponseRecorder{
 		rw:             rw,
 		b:              &b,
-		ResponseWriter: safehttp.NewResponseWriter(d, rw),
+		ResponseWriter: safehttp.NewResponseWriter(d, rw, nil),
 	}
 }
 
diff --git a/safehttp/url_test.go b/safehttp/url_test.go
new file mode 100644
index 0000000..fc4a97b
--- /dev/null
+++ b/safehttp/url_test.go
@@ -0,0 +1,205 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package safehttp
+
+import (
+	"net/url"
+	"testing"
+)
+
+func TestURLToString(t *testing.T) {
+	want := "http://www.example.com/asdf?fruit=apple"
+
+	netURL, err := url.Parse(want)
+	if err != nil {
+		t.Fatalf(`url.Parse("http://www.example.com/asdf?fruit=apple") got: %v want: nil`, err)
+	}
+
+	u := URL{url: netURL}
+	if got := u.String(); got != want {
+		t.Errorf("u.String() got: %v want: %v", got, want)
+	}
+}
+
+func TestURLHost(t *testing.T) {
+	var test = []struct {
+		name string
+		url  string
+		want string
+	}{
+		{
+			name: "Just host",
+			url:  "http://www.example.com/asdf?fruit=apple",
+			want: "www.example.com",
+		},
+		{
+			name: "Host and port",
+			url:  "http://www.example.com:1337/asdf?fruit=apple",
+			want: "www.example.com:1337",
+		},
+	}
+
+	for _, tt := range test {
+		t.Run(tt.name, func(t *testing.T) {
+			netURL, err := url.Parse(tt.url)
+			if err != nil {
+				t.Fatalf("url.Parse(tt.url) got: %v want: nil", err)
+			}
+
+			u := URL{url: netURL}
+			if got := u.Host(); got != tt.want {
+				t.Errorf("u.Host() got: %v want: %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestURLHostname(t *testing.T) {
+	var test = []struct {
+		name string
+		url  string
+		want string
+	}{
+		{
+			name: "Just host",
+			url:  "http://www.example.com/asdf?fruit=apple",
+			want: "www.example.com",
+		},
+		{
+			name: "Host and port",
+			url:  "http://www.example.com:1337/asdf?fruit=apple",
+			want: "www.example.com",
+		},
+		{
+			name: "Ipv6 and port",
+			url:  "http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:17000/asdf?fruit=apple",
+			want: "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+		},
+	}
+
+	for _, tt := range test {
+		t.Run(tt.name, func(t *testing.T) {
+			netURL, err := url.Parse(tt.url)
+			if err != nil {
+				t.Fatalf("url.Parse(tt.url) got: %v want: nil", err)
+			}
+
+			u := URL{url: netURL}
+			if got := u.Hostname(); got != tt.want {
+				t.Errorf("u.Hostname() got: %v want: %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestURLPort(t *testing.T) {
+	var test = []struct {
+		name string
+		url  string
+		want string
+	}{
+		{
+			name: "Just host",
+			url:  "http://www.example.com/asdf?fruit=apple",
+			want: "",
+		},
+		{
+			name: "Host and port",
+			url:  "http://www.example.com:1337/asdf?fruit=apple",
+			want: "1337",
+		},
+		{
+			name: "HTTPS",
+			url:  "https://www.example.com/asdf?fruit=apple",
+			want: "",
+		},
+	}
+
+	for _, tt := range test {
+		t.Run(tt.name, func(t *testing.T) {
+			netURL, err := url.Parse(tt.url)
+			if err != nil {
+				t.Fatalf("url.Parse(tt.url) got: %v want: nil", err)
+			}
+
+			u := URL{url: netURL}
+			if got := u.Port(); got != tt.want {
+				t.Errorf("u.Port() got: %v want: %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestURLPath(t *testing.T) {
+	netURL, err := url.Parse("http://www.example.com/asdf?fruit=apple")
+	if err != nil {
+		t.Fatalf(`url.Parse("http://www.example.com/asdf?fruit=apple") got: %v want: nil`, err)
+	}
+
+	u := URL{url: netURL}
+	if got, want := u.Path(), "/asdf"; got != want {
+		t.Errorf("u.Path() got: %v want: %v", got, want)
+	}
+}
+
+func TestURLQuery(t *testing.T) {
+	var test = []struct {
+		name string
+		url  string
+		want string
+	}{
+		{
+			name: "Normal",
+			url:  "http://www.example.com/asdf?fruit=apple",
+			want: "apple",
+		},
+		{
+			name: "Empty",
+			url:  "http://www.example.com/asdf",
+			want: "",
+		},
+	}
+
+	for _, tt := range test {
+		t.Run(tt.name, func(t *testing.T) {
+			netURL, err := url.Parse(tt.url)
+			if err != nil {
+				t.Fatalf("url.Parse(tt.url) got: %v want: nil", err)
+			}
+
+			u := URL{url: netURL}
+			f, err := u.Query()
+			if err != nil {
+				t.Errorf("u.Query() got: %v want: nil", err)
+			}
+
+			if got := f.String("fruit", ""); got != tt.want {
+				t.Errorf(`f.String("fruit", "") got: %q want: %q`, got, tt.want)
+			}
+		})
+	}
+}
+
+func TestURLInvalidQuery(t *testing.T) {
+	netURL, err := url.Parse("http://www.example.com/asdf?%xx=abc")
+	if err != nil {
+		t.Fatalf(`url.Parse("http://www.example.com/asdf?%%xx=abc") got: %v want: nil`, err)
+	}
+
+	u := URL{url: netURL}
+	if _, err = u.Query(); err == nil {
+		t.Error("u.Query() got: nil want: error")
+	}
+}