Repository: jensneuse/graphql-go-tools
Pull request #236 (2020-12-17 07:07:56)

go 1.12 compat, planner fix, websocket fix


Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 77865e3f..4259730b 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -6,7 +6,7 @@ jobs:
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
-        go: [ '1.13', '1.14', '1.15' ]
+        go: [ '1.12', '1.15' ]
         os: [ 'ubuntu-latest']
     steps:
       - name: Set up Go ${{ matrix.go }}
diff --git a/cmd/root.go b/cmd/root.go
index e05c6790..9b3ed4bf 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -6,7 +6,7 @@ import (
 
 	"github.com/spf13/cobra"
 
-	homedir "github.com/mitchellh/go-homedir"
+	"github.com/mitchellh/go-homedir"
 	"github.com/spf13/viper"
 )
 
diff --git a/go.mod b/go.mod
index caa6ab44..9d0da5a6 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,6 @@ require (
 	github.com/cespare/xxhash v1.1.0
 	github.com/dave/jennifer v1.4.0
 	github.com/davecgh/go-spew v1.1.1
-	github.com/dgraph-io/ristretto v0.0.4-0.20201104062757-d8d537137c97 // indirect
 	github.com/eclipse/paho.mqtt.golang v1.2.0
 	github.com/evanphx/json-patch/v5 v5.1.0
 	github.com/fsnotify/fsnotify v1.4.9 // indirect
@@ -39,9 +38,11 @@ require (
 	go.uber.org/atomic v1.5.1
 	go.uber.org/multierr v1.4.0 // indirect
 	golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect
+	golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e
 	golang.org/x/sys v0.0.0-20201101102859-da207088b7d1 // indirect
 	golang.org/x/text v0.3.2 // indirect
 	golang.org/x/tools v0.0.0-20200414032229-332987a829c3 // indirect
+	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
 	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
 	gopkg.in/yaml.v2 v2.2.8 // indirect
 )
diff --git a/go.sum b/go.sum
index d7cc64c5..75848e73 100644
--- a/go.sum
+++ b/go.sum
@@ -22,16 +22,8 @@ github.com/dave/jennifer v1.4.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhr
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dgraph-io/ristretto v0.0.3 h1:jh22xisGBjrEVnRZ1DVTpBVQm0Xndu8sMl0CWDzSIBI=
-github.com/dgraph-io/ristretto v0.0.3/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
-github.com/dgraph-io/ristretto v0.0.4-0.20201104062757-d8d537137c97 h1:VZSlLdMj2VmSmQlSQ9cy0uH1w7X9DWEV9+lxDNn++iE=
-github.com/dgraph-io/ristretto v0.0.4-0.20201104062757-d8d537137c97/go.mod h1:bDI4cDaalvYSji3vBVDKrn9ouDZrwN974u8ZO/AhYXs=
-github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
-github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
-github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
 github.com/eclipse/paho.mqtt.golang v1.2.0 h1:1F8mhG9+aO5/xpdtFkW4SxOJB67ukuDC3t2y2qayIX0=
 github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts=
-github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses=
 github.com/evanphx/json-patch/v5 v5.1.0 h1:B0aXl1o/1cP8NbviYiBMkcHBtUjIJ1/Ccg6b+SwCLQg=
 github.com/evanphx/json-patch/v5 v5.1.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
@@ -232,10 +224,6 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qq
 golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775 h1:TC0v2RSO1u2kn1ZugjrFXkRZAEaqMN/RW+OTZkBzmLE=
-golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200918174421-af09f7315aff h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8=
-golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201101102859-da207088b7d1 h1:a/mKvvZr9Jcc8oKfcmgzyp7OwF73JPWsQLvH1z2Kxck=
 golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -260,6 +248,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 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=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
diff --git a/pkg/ast/ast_field_definition.go b/pkg/ast/ast_field_definition.go
index b6a79d96..b9c96e83 100644
--- a/pkg/ast/ast_field_definition.go
+++ b/pkg/ast/ast_field_definition.go
@@ -133,6 +133,6 @@ func (d *Document) FieldDefinitionType(ref int) int {
 
 func (d *Document) FieldDefinitionTypeNode(ref int) Node {
 	typeName := d.ResolveTypeNameBytes(d.FieldDefinitions[ref].Type)
-	node,_ :=  d.Index.FirstNodeByNameBytes(typeName)
+	node, _ := d.Index.FirstNodeByNameBytes(typeName)
 	return node
 }
diff --git a/pkg/ast/ast_input_object_type_definition.go b/pkg/ast/ast_input_object_type_definition.go
index d67e5bce..70acc1d9 100644
--- a/pkg/ast/ast_input_object_type_definition.go
+++ b/pkg/ast/ast_input_object_type_definition.go
@@ -69,7 +69,7 @@ func (d *Document) InputObjectTypeDefinitionInputValueDefinitionDefaultValueFloa
 }
 
 func (d *Document) InputObjectTypeDefinitionInputValueDefinitionDefaultValue(inputObjectTypeDefinitionName, inputValueDefinitionName string) Value {
-	inputObjectTypeDefinition,exists := d.Index.FirstNodeByNameStr(inputObjectTypeDefinitionName)
+	inputObjectTypeDefinition, exists := d.Index.FirstNodeByNameStr(inputObjectTypeDefinitionName)
 	if !exists {
 		return Value{}
 	}
diff --git a/pkg/ast/ast_node.go b/pkg/ast/ast_node.go
index 56c0f4eb..161559d8 100644
--- a/pkg/ast/ast_node.go
+++ b/pkg/ast/ast_node.go
@@ -414,7 +414,7 @@ func (d *Document) NodeFragmentIsAllowedOnObjectTypeDefinition(fragmentNode, obj
 func (d *Document) UnionNodeIntersectsInterfaceNode(unionNode, interfaceNode Node) bool {
 	for _, i := range d.UnionTypeDefinitions[unionNode.Ref].UnionMemberTypes.Refs {
 		memberName := d.ResolveTypeNameBytes(i)
-		node,exists := d.Index.FirstNodeByNameBytes(memberName)
+		node, exists := d.Index.FirstNodeByNameBytes(memberName)
 		if !exists {
 			continue
 		}
diff --git a/pkg/ast/ast_operation_definition.go b/pkg/ast/ast_operation_definition.go
index 44bbfea0..85552e6b 100644
--- a/pkg/ast/ast_operation_definition.go
+++ b/pkg/ast/ast_operation_definition.go
@@ -29,7 +29,7 @@ type OperationDefinition struct {
 }
 
 func (d *Document) OperationDefinitionHasVariableDefinition(ref int, variableName string) bool {
-	for _,i := range d.OperationDefinitions[ref].VariableDefinitions.Refs {
+	for _, i := range d.OperationDefinitions[ref].VariableDefinitions.Refs {
 		value := d.VariableDefinitions[i].VariableValue.Ref
 		if variableName == d.VariableValueNameString(value) {
 			return true
@@ -71,7 +71,7 @@ func (d *Document) AddVariableDefinitionToOperationDefinition(operationDefinitio
 		append(d.OperationDefinitions[operationDefinitionRef].VariableDefinitions.Refs, ref)
 }
 
-func (d *Document) AddImportedVariableDefinitionToOperationDefinition(operationDefinition,variableDefinition int) {
+func (d *Document) AddImportedVariableDefinitionToOperationDefinition(operationDefinition, variableDefinition int) {
 	if !d.OperationDefinitions[operationDefinition].HasVariableDefinitions {
 		d.OperationDefinitions[operationDefinition].HasVariableDefinitions = true
 		d.OperationDefinitions[operationDefinition].VariableDefinitions.Refs = d.Refs[d.NextRefIndex()][:0]
@@ -80,6 +80,29 @@ func (d *Document) AddImportedVariableDefinitionToOperationDefinition(operationD
 		append(d.OperationDefinitions[operationDefinition].VariableDefinitions.Refs, variableDefinition)
 }
 
+func (d *Document) OperationNameExists(operationName string) bool {
+
+	for i := range d.RootNodes {
+		if d.RootNodes[i].Kind != NodeKindOperationDefinition {
+			continue
+		}
+		if d.OperationDefinitionNameString(i) == operationName {
+			return true
+		}
+	}
+
+	return false
+}
+
+func (d *Document) NumOfOperationDefinitions () (n int) {
+	for i := range d.RootNodes {
+		if d.RootNodes[i].Kind == NodeKindOperationDefinition {
+			n++
+		}
+	}
+	return
+}
+
 const (
 	alphabet = `abcdefghijklmnopqrstuvwxyz`
 )
diff --git a/pkg/ast/ast_val_variable_value.go b/pkg/ast/ast_val_variable_value.go
index fc8a9fa5..4b2477a3 100644
--- a/pkg/ast/ast_val_variable_value.go
+++ b/pkg/ast/ast_val_variable_value.go
@@ -54,4 +54,4 @@ func (d *Document) AddVariableValueArgument(argName, variableName []byte) (varia
 	d.Arguments = append(d.Arguments, arg)
 	argRef = len(d.Arguments) - 1
 	return
-}
\ No newline at end of file
+}
diff --git a/pkg/ast/index.go b/pkg/ast/index.go
index 3ceab363..1910b546 100644
--- a/pkg/ast/index.go
+++ b/pkg/ast/index.go
@@ -54,7 +54,6 @@ func (i *Index) AddNodeBytes(name []byte, node Node) {
 	i.nodes[hash] = append(i.nodes[hash], node)
 }
 
-
 func (i *Index) NodesByNameStr(name string) ([]Node, bool) {
 	hash := xxhash.Sum64String(name)
 	node, exists := i.nodes[hash]
diff --git a/pkg/astparser/parser.go b/pkg/astparser/parser.go
index 50e92eb6..50a3e18b 100644
--- a/pkg/astparser/parser.go
+++ b/pkg/astparser/parser.go
@@ -849,7 +849,7 @@ func (p *Parser) parseObjectTypeDefinition(description *ast.Description) {
 
 func (p *Parser) indexNode(key ast.ByteSliceReference, value ast.Node) {
 	name := p.document.Input.ByteSlice(key)
-	p.document.Index.AddNodeBytes(name,value)
+	p.document.Index.AddNodeBytes(name, value)
 }
 
 func (p *Parser) parseRootDescription() {
@@ -1827,7 +1827,7 @@ func (p *Parser) parseObjectTypeExtension(extend position.Position) {
 	p.document.RootNodes = append(p.document.RootNodes, node)
 
 	if p.shouldIndex {
-		p.indexNode(objectTypeDefinition.Name,node)
+		p.indexNode(objectTypeDefinition.Name, node)
 	}
 }
 
diff --git a/pkg/astvalidation/astvalidation.go b/pkg/astvalidation/astvalidation.go
index b3eed011..20a6b75b 100644
--- a/pkg/astvalidation/astvalidation.go
+++ b/pkg/astvalidation/astvalidation.go
@@ -1307,7 +1307,7 @@ func (v *variablesAreInputTypesVisitor) EnterDocument(operation, definition *ast
 func (v *variablesAreInputTypesVisitor) EnterVariableDefinition(ref int) {
 
 	typeName := v.operation.ResolveTypeNameBytes(v.operation.VariableDefinitions[ref].Type)
-	typeDefinitionNode,_ := v.definition.Index.FirstNodeByNameBytes(typeName)
+	typeDefinitionNode, _ := v.definition.Index.FirstNodeByNameBytes(typeName)
 	switch typeDefinitionNode.Kind {
 	case ast.NodeKindInputObjectTypeDefinition, ast.NodeKindScalarTypeDefinition, ast.NodeKindEnumTypeDefinition:
 		return
diff --git a/pkg/codegen/manual.go b/pkg/codegen/manual.go
index 75ead4c8..eb8a5436 100644
--- a/pkg/codegen/manual.go
+++ b/pkg/codegen/manual.go
@@ -1,6 +1,6 @@
 package codegen
 
-import ast "github.com/jensneuse/graphql-go-tools/pkg/ast"
+import "github.com/jensneuse/graphql-go-tools/pkg/ast"
 
 type DataSourceConfig struct {
 	NonNullString                 string
diff --git a/pkg/engine/datasource/httpclient/compatibility_1.12.go b/pkg/engine/datasource/httpclient/compatibility_1.12.go
new file mode 100644
index 00000000..959979b1
--- /dev/null
+++ b/pkg/engine/datasource/httpclient/compatibility_1.12.go
@@ -0,0 +1,138 @@
+package httpclient
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	urlpkg "net/url"
+	"strings"
+
+	"golang.org/x/net/http/httpguts"
+)
+
+/*
+This package is a copy of the NewRequestWithContext function, which was introduced with go 1.13.
+When support for go 1.12 is dropped, this file can be safely removed an the usage of `NewRequestWithContext`
+should be then changed to `http.NewRequestWithContext`.
+*/
+
+func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*http.Request, error) {
+	if method == "" {
+		// We document that "" means "GET" for Request.Method, and people have
+		// relied on that from NewRequest, so keep that working.
+		// We still enforce validMethod for non-empty methods.
+		method = "GET"
+	}
+	if !validMethod(method) {
+		return nil, fmt.Errorf("net/http: invalid method %q", method)
+	}
+	if ctx == nil {
+		return nil, errors.New("net/http: nil Context")
+	}
+	u, err := urlpkg.Parse(url)
+	if err != nil {
+		return nil, err
+	}
+	rc, ok := body.(io.ReadCloser)
+	if !ok && body != nil {
+		rc = ioutil.NopCloser(body)
+	}
+	// The host's colon:port should be normalized. See Issue 14836.
+	u.Host = removeEmptyPort(u.Host)
+	req := &http.Request{
+		Method:     method,
+		URL:        u,
+		Proto:      "HTTP/1.1",
+		ProtoMajor: 1,
+		ProtoMinor: 1,
+		Header:     make(http.Header),
+		Body:       rc,
+		Host:       u.Host,
+	}
+
+	req = req.WithContext(ctx)
+
+	if body != nil {
+		switch v := body.(type) {
+		case *bytes.Buffer:
+			req.ContentLength = int64(v.Len())
+			buf := v.Bytes()
+			req.GetBody = func() (io.ReadCloser, error) {
+				r := bytes.NewReader(buf)
+				return ioutil.NopCloser(r), nil
+			}
+		case *bytes.Reader:
+			req.ContentLength = int64(v.Len())
+			snapshot := *v
+			req.GetBody = func() (io.ReadCloser, error) {
+				r := snapshot
+				return ioutil.NopCloser(&r), nil
+			}
+		case *strings.Reader:
+			req.ContentLength = int64(v.Len())
+			snapshot := *v
+			req.GetBody = func() (io.ReadCloser, error) {
+				r := snapshot
+				return ioutil.NopCloser(&r), nil
+			}
+		default:
+			// This is where we'd set it to -1 (at least
+			// if body != NoBody) to mean unknown, but
+			// that broke people during the Go 1.8 testing
+			// period. People depend on it being 0 I
+			// guess. Maybe retry later. See Issue 18117.
+		}
+		// For client requests, Request.ContentLength of 0
+		// means either actually 0, or unknown. The only way
+		// to explicitly say that the ContentLength is zero is
+		// to set the Body to nil. But turns out too much code
+		// depends on NewRequest returning a non-nil Body,
+		// so we use a well-known ReadCloser variable instead
+		// and have the http package also treat that sentinel
+		// variable to mean explicitly zero.
+		if req.GetBody != nil && req.ContentLength == 0 {
+			req.Body = http.NoBody
+			req.GetBody = func() (io.ReadCloser, error) { return http.NoBody, nil }
+		}
+	}
+
+	return req, nil
+}
+
+func validMethod(method string) bool {
+	/*
+	     Method         = "OPTIONS"                ; Section 9.2
+	                    | "GET"                    ; Section 9.3
+	                    | "HEAD"                   ; Section 9.4
+	                    | "POST"                   ; Section 9.5
+	                    | "PUT"                    ; Section 9.6
+	                    | "DELETE"                 ; Section 9.7
+	                    | "TRACE"                  ; Section 9.8
+	                    | "CONNECT"                ; Section 9.9
+	                    | extension-method
+	   extension-method = token
+	     token          = 1*<any CHAR except CTLs or separators>
+	*/
+	return len(method) > 0 && strings.IndexFunc(method, isNotToken) == -1
+}
+
+// removeEmptyPort strips the empty port in ":port" to ""
+// as mandated by RFC 3986 Section 6.2.3.
+func removeEmptyPort(host string) string {
+	if hasPort(host) {
+		return strings.TrimSuffix(host, ":")
+	}
+	return host
+}
+
+func isNotToken(r rune) bool {
+	return !httpguts.IsTokenRune(r)
+}
+
+// Given a string of the form "host", "host:port", or "[ipv6::address]:port",
+// return true if the string includes a port.
+func hasPort(s string) bool { return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") }
diff --git a/pkg/engine/datasource/httpclient/httpclient.go b/pkg/engine/datasource/httpclient/httpclient.go
index e03982ca..adc00f61 100644
--- a/pkg/engine/datasource/httpclient/httpclient.go
+++ b/pkg/engine/datasource/httpclient/httpclient.go
@@ -25,7 +25,7 @@ const (
 	QUERYPARAMS = "query_params"
 
 	SCHEME = "scheme"
-	HOST = "host"
+	HOST   = "host"
 )
 
 var (
@@ -51,11 +51,11 @@ type Client interface {
 
 func wrapQuotesIfString(b []byte) []byte {
 
-	if bytes.HasPrefix(b,[]byte("$$")) && bytes.HasSuffix(b,[]byte("$$")){
+	if bytes.HasPrefix(b, []byte("$$")) && bytes.HasSuffix(b, []byte("$$")) {
 		return b
 	}
 
-	if bytes.HasPrefix(b,[]byte("{{")) && bytes.HasSuffix(b,[]byte("}}")){
+	if bytes.HasPrefix(b, []byte("{{")) && bytes.HasSuffix(b, []byte("}}")) {
 		return b
 	}
 
@@ -65,17 +65,17 @@ func wrapQuotesIfString(b []byte) []byte {
 		return b
 	case gjson.JSON:
 		var value interface{}
-		withoutTemplate := bytes.ReplaceAll(b,[]byte("$$"),nil)
+		withoutTemplate := bytes.ReplaceAll(b, []byte("$$"), nil)
 
 		buf := &bytes.Buffer{}
 		tmpl := byte_template.New()
-		_,_ = tmpl.Execute(buf,withoutTemplate, func(w io.Writer, path []byte) (n int, err error) {
+		_, _ = tmpl.Execute(buf, withoutTemplate, func(w io.Writer, path []byte) (n int, err error) {
 			return w.Write([]byte("0"))
 		})
 
 		withoutTemplate = buf.Bytes()
 
-		err := json.Unmarshal(withoutTemplate,&value)
+		err := json.Unmarshal(withoutTemplate, &value)
 		if err == nil {
 			return b
 		}
@@ -186,7 +186,7 @@ func requestInputParams(input []byte) (url, method, body, headers, queryParams [
 	return
 }
 
-func GetSubscriptionInput (input []byte) (scheme, host,path, body, headers []byte) {
+func GetSubscriptionInput(input []byte) (scheme, host, path, body, headers []byte) {
 	jsonparser.EachKey(input, func(i int, bytes []byte, valueType jsonparser.ValueType, err error) {
 		switch i {
 		case 0:
@@ -202,4 +202,4 @@ func GetSubscriptionInput (input []byte) (scheme, host,path, body, headers []byt
 		}
 	}, subscriptionInputPaths...)
 	return
-}
\ No newline at end of file
+}
diff --git a/pkg/engine/datasource/httpclient/nethttpclient.go b/pkg/engine/datasource/httpclient/nethttpclient.go
index a0817a8c..1ed2a938 100644
--- a/pkg/engine/datasource/httpclient/nethttpclient.go
+++ b/pkg/engine/datasource/httpclient/nethttpclient.go
@@ -37,7 +37,8 @@ func (n *NetHttpClient) Do(ctx context.Context, requestInput []byte, out io.Writ
 
 	url, method, body, headers, queryParams := requestInputParams(requestInput)
 
-	request, err := http.NewRequestWithContext(ctx, unsafebytes.BytesToString(method), unsafebytes.BytesToString(url), bytes.NewReader(body))
+	// Change to `http.NewRequestWithContext` when support for go 1.12 is dropped
+	request, err := NewRequestWithContext(ctx, unsafebytes.BytesToString(method), unsafebytes.BytesToString(url), bytes.NewReader(body))
 	if err != nil {
 		return err
 	}
diff --git a/pkg/engine/datasource/staticdatasource/static_datasource.go b/pkg/engine/datasource/staticdatasource/static_datasource.go
index 98f447db..d12eca13 100644
--- a/pkg/engine/datasource/staticdatasource/static_datasource.go
+++ b/pkg/engine/datasource/staticdatasource/static_datasource.go
@@ -4,7 +4,7 @@ import (
 	"context"
 	"encoding/json"
 
-	plan "github.com/jensneuse/graphql-go-tools/pkg/engine/plan"
+	"github.com/jensneuse/graphql-go-tools/pkg/engine/plan"
 	"github.com/jensneuse/graphql-go-tools/pkg/engine/resolve"
 )
 
diff --git a/pkg/engine/plan/plan.go b/pkg/engine/plan/plan.go
index fcdece8b..6c40d29e 100644
--- a/pkg/engine/plan/plan.go
+++ b/pkg/engine/plan/plan.go
@@ -168,13 +168,19 @@ func (p *Planner) Plan(operation, definition *ast.Document, operationName string
 
 	config := p.config
 
+	// select operation
+
+	p.selectOperation(operation, operationName, report)
+	if report.HasErrors() {
+		return
+	}
+
 	// pre-process required fields
 
-	p.preProcessRequiredFields(&config, operation, definition, operationName, report)
+	p.preProcessRequiredFields(&config, operation, definition, report)
 
 	// find planning paths
 
-	p.configurationVisitor.operationName = operationName
 	p.configurationVisitor.config = config
 	p.configurationWalker.Walk(operation, definition, report)
 
@@ -203,20 +209,42 @@ func (p *Planner) Plan(operation, definition *ast.Document, operationName string
 
 	// process the plan
 
-	p.planningVisitor.OperationName = operationName
 	p.planningWalker.Walk(operation, definition, report)
 
 	return p.planningVisitor.plan
 }
 
-func (p *Planner) preProcessRequiredFields(config *Configuration, operation, definition *ast.Document, operationName string, report *operationreport.Report) {
+func (p *Planner) selectOperation(operation *ast.Document, operationName string, report *operationreport.Report) {
+
+	numOfOperations := operation.NumOfOperationDefinitions()
+	operationName = strings.TrimSpace(operationName)
+	if len(operationName) == 0 && numOfOperations > 1 {
+		report.AddExternalError(operationreport.ErrRequiredOperationNameIsMissing())
+		return
+	}
+
+	if len(operationName) == 0 && numOfOperations == 1 {
+		operationName = operation.OperationDefinitionNameString(0)
+	}
+
+	if !operation.OperationNameExists(operationName) {
+		report.AddExternalError(operationreport.ErrOperationWithProvidedOperationNameNotFound(operationName))
+		return
+	}
+
+	p.requiredFieldsVisitor.operationName = operationName
+	p.configurationVisitor.operationName = operationName
+	p.planningVisitor.OperationName = operationName
+}
+
+func (p *Planner) preProcessRequiredFields(config *Configuration, operation, definition *ast.Document, report *operationreport.Report) {
 	if !p.hasRequiredFields(config) {
 		return
 	}
+
 	p.requiredFieldsVisitor.config = config
 	p.requiredFieldsVisitor.operation = operation
 	p.requiredFieldsVisitor.definition = definition
-	p.requiredFieldsVisitor.operationName = operationName
 	p.requiredFieldsWalker.Walk(operation, definition, report)
 }
 
diff --git a/pkg/engine/resolve/resolve.go b/pkg/engine/resolve/resolve.go
index 5dd38ef4..35f7d3c5 100644
--- a/pkg/engine/resolve/resolve.go
+++ b/pkg/engine/resolve/resolve.go
@@ -5,7 +5,6 @@ package resolve
 import (
 	"bytes"
 	"context"
-	"errors"
 	"fmt"
 	"hash"
 	"io"
@@ -17,6 +16,8 @@ import (
 	"github.com/buger/jsonparser"
 	"github.com/cespare/xxhash"
 
+	errors "golang.org/x/xerrors"
+
 	"github.com/jensneuse/graphql-go-tools/internal/pkg/unsafebytes"
 	"github.com/jensneuse/graphql-go-tools/pkg/engine/subscription"
 	"github.com/jensneuse/graphql-go-tools/pkg/fastbuffer"
diff --git a/pkg/engine/subscription/graphql-websocket-subscription/graphql_websocket_subscription.go b/pkg/engine/subscription/graphql-websocket-subscription/graphql_websocket_subscription.go
index 518f5217..78dfae2b 100644
--- a/pkg/engine/subscription/graphql-websocket-subscription/graphql_websocket_subscription.go
+++ b/pkg/engine/subscription/graphql-websocket-subscription/graphql_websocket_subscription.go
@@ -34,8 +34,7 @@ func New() *GraphQLWebsocketSubscriptionStream {
 }
 
 func (g *GraphQLWebsocketSubscriptionStream) Start(input []byte, next chan<- []byte, stop <-chan struct{}) {
-
-	scheme,host,path,body,headers := httpclient.GetSubscriptionInput(input)
+	scheme, host, path, body, headers := httpclient.GetSubscriptionInput(input)
 
 	var (
 		header http.Header
@@ -93,7 +92,7 @@ func (g *GraphQLWebsocketSubscriptionStream) Start(input []byte, next chan<- []b
 			if !ok {
 				return
 			}
-			content,_,_,err := jsonparser.Get(data,"data")
+			content, _, _, err := jsonparser.Get(data, "data")
 			if err != nil || len(content) == 0 {
 				continue
 			}
diff --git a/pkg/engine/subscription/graphql-websocket-subscription/websocket_client.go b/pkg/engine/subscription/graphql-websocket-subscription/websocket_client.go
index a71824f3..dbd32054 100644
--- a/pkg/engine/subscription/graphql-websocket-subscription/websocket_client.go
+++ b/pkg/engine/subscription/graphql-websocket-subscription/websocket_client.go
@@ -39,7 +39,6 @@ type WebsocketClient struct {
 }
 
 func (w *WebsocketClient) Open(scheme, host, path string, header http.Header) (err error) {
-
 	w.tmpl = byte_template.New()
 	w.addSubscription = make(chan addSubscriptionCmd)
 	w.removeSubscription = make(chan removeSubscriptionCmd)
@@ -87,8 +86,8 @@ func (w *WebsocketClient) CloseIfNoSubscriptions() (closed bool) {
 	return <-closedChan
 }
 
-func (w *WebsocketClient) connectionInit() error {
-	err := w.conn.WriteMessage(websocket.TextMessage, connectionInitMessage)
+func (w *WebsocketClient) connectionInit() (err error) {
+	err = w.conn.WriteMessage(websocket.TextMessage, connectionInitMessage)
 	if err != nil {
 		return err
 	}
@@ -111,6 +110,18 @@ func (w *WebsocketClient) connectionInit() error {
 }
 
 func (w *WebsocketClient) run() {
+	// run message handling separately from the control flow
+	go func() {
+		for {
+			select {
+			case <-w.done:
+				return
+			default:
+				w.handleNextMessage()
+			}
+		}
+	}()
+
 	for {
 		select {
 		case <-w.done:
@@ -125,8 +136,6 @@ func (w *WebsocketClient) run() {
 				w.Close()
 			}
 			closed <- shouldClose
-		default:
-			w.handleNextMessage()
 		}
 	}
 }
@@ -136,18 +145,22 @@ func (w *WebsocketClient) handleNextMessage() {
 	if err != nil {
 		return
 	}
+
 	payload, _, _, err := jsonparser.Get(data, "payload")
 	if err != nil {
 		return
 	}
+
 	id, err := jsonparser.GetString(data, "id")
 	if err != nil {
 		return
 	}
+
 	messageType, err := jsonparser.GetString(data, "type")
 	if err != nil {
 		return
 	}
+
 	switch messageType {
 	case "data":
 	case "complete":
@@ -155,10 +168,12 @@ func (w *WebsocketClient) handleNextMessage() {
 	default:
 		return
 	}
+
 	sub, ok := w.subscriptions[uint64(unsafebytes.BytesToInt64(unsafebytes.StringToBytes(id)))]
 	if !ok {
 		return
 	}
+
 	select {
 	case sub.next <- payload:
 	case <-sub.unsubscribe:
@@ -166,7 +181,6 @@ func (w *WebsocketClient) handleNextMessage() {
 }
 
 func (w *WebsocketClient) handleAdd(add addSubscriptionCmd) {
-
 	var (
 		err error
 		id  uint64
@@ -220,7 +234,6 @@ func (w *WebsocketClient) handleAdd(add addSubscriptionCmd) {
 }
 
 func (w *WebsocketClient) handleRemove(remove removeSubscriptionCmd) {
-
 	close(w.subscriptions[remove.id].stop)
 	delete(w.subscriptions, remove.id)
 
@@ -265,10 +278,18 @@ type addSubscriptionCmd struct {
 
 func (w *WebsocketClient) Subscribe(payload []byte) (subscription Subscription, ok bool) {
 	getSubscription := make(chan Subscription)
-	w.addSubscription <- addSubscriptionCmd{
+
+	cmd := addSubscriptionCmd{
 		payload:         payload,
 		getSubscription: getSubscription,
 	}
+
+	select {
+	case <-w.done:
+		close(getSubscription)
+	case w.addSubscription <- cmd:
+	}
+
 	subscription, ok = <-getSubscription
 	return
 }
diff --git a/pkg/engine/subscription/http_polling/http_polling.go b/pkg/engine/subscription/http_polling/http_polling.go
index 3869a525..10f8eb2d 100644
--- a/pkg/engine/subscription/http_polling/http_polling.go
+++ b/pkg/engine/subscription/http_polling/http_polling.go
@@ -55,7 +55,7 @@ func (h *HttpPollingStream) Start(input []byte, next chan<- []byte, stop <-chan
 	if err != nil {
 		return
 	}
-	skipSameResponse,err := jsonparser.GetBoolean(input,"skip_publish_same_response")
+	skipSameResponse, err := jsonparser.GetBoolean(input, "skip_publish_same_response")
 	if err != nil {
 		skipSameResponse = false
 	}
@@ -78,7 +78,7 @@ func (h *HttpPollingStream) Start(input []byte, next chan<- []byte, stop <-chan
 			}
 			if skipSameResponse {
 				hash.Reset()
-				_,err = hash.Write(src)
+				_, err = hash.Write(src)
 				if err != nil {
 					continue
 				}
diff --git a/pkg/execution/datasource/datasource.go b/pkg/execution/datasource/datasource.go
index 2c5ebe40..ed1eece1 100644
--- a/pkg/execution/datasource/datasource.go
+++ b/pkg/execution/datasource/datasource.go
@@ -18,6 +18,9 @@ import (
 	"github.com/jensneuse/graphql-go-tools/pkg/astvisitor"
 )
 
+var RootTypeName = []byte("root_type_name")
+var RootFieldName = []byte("root_field_name")
+
 var defaultHttpClient *http.Client
 
 func DefaultHttpClient() *http.Client {
diff --git a/pkg/execution/datasource/datasource_graphql.go b/pkg/execution/datasource/datasource_graphql.go
index eac1454f..c432bc17 100644
--- a/pkg/execution/datasource/datasource_graphql.go
+++ b/pkg/execution/datasource/datasource_graphql.go
@@ -48,11 +48,13 @@ type GraphQLDataSourcePlanner struct {
 	whitelistedSchemes           []string
 	whitelistedVariableRefs      []int
 	whitelistedVariableNameHashs map[uint64]bool
+	hooks                        Hooks
 }
 
 type GraphQLDataSourcePlannerFactoryFactory struct {
 	Client             *http.Client
 	WhitelistedSchemes []string
+	Hooks              Hooks
 }
 
 func (g *GraphQLDataSourcePlannerFactoryFactory) httpClient() *http.Client {
@@ -67,6 +69,7 @@ func (g GraphQLDataSourcePlannerFactoryFactory) Initialize(base BasePlanner, con
 		base:               base,
 		client:             g.httpClient(),
 		whitelistedSchemes: g.WhitelistedSchemes,
+		hooks:              g.Hooks,
 	}
 	err := json.NewDecoder(configReader).Decode(&factory.config)
 	return factory, err
@@ -77,6 +80,7 @@ type GraphQLDataSourcePlannerFactory struct {
 	config             GraphQLDataSourceConfig
 	client             *http.Client
 	whitelistedSchemes []string
+	hooks              Hooks
 }
 
 func (g *GraphQLDataSourcePlannerFactory) DataSourcePlanner() Planner {
@@ -89,6 +93,7 @@ func (g *GraphQLDataSourcePlannerFactory) DataSourcePlanner() Planner {
 		whitelistedSchemes:           g.whitelistedSchemes,
 		whitelistedVariableRefs:      []int{},
 		whitelistedVariableNameHashs: map[uint64]bool{},
+		hooks:                        g.hooks,
 	}
 }
 
@@ -162,6 +167,17 @@ func (g *GraphQLDataSourcePlanner) EnterField(ref int) {
 		typeName := g.Definition.NodeNameString(g.Walker.EnclosingTypeDefinition)
 		fieldNameStr := g.Operation.FieldNameString(ref)
 		fieldName := g.Operation.FieldNameBytes(ref)
+
+		g.Args = append(g.Args, &StaticVariableArgument{
+			Name:  RootTypeName,
+			Value: []byte(typeName),
+		})
+
+		g.Args = append(g.Args, &StaticVariableArgument{
+			Name:  RootFieldName,
+			Value: fieldName,
+		})
+
 		mapping := g.Config.MappingForTypeField(typeName, fieldNameStr)
 		if mapping != nil && !mapping.Disabled {
 			fieldName = unsafebytes.StringToBytes(mapping.Path)
@@ -321,6 +337,7 @@ func (g *GraphQLDataSourcePlanner) Plan(args []Argument) (DataSource, []Argument
 		Log:                g.Log,
 		Client:             g.client,
 		WhitelistedSchemes: g.whitelistedSchemes,
+		Hooks:              g.hooks,
 	}, g.Args
 }
 
@@ -328,11 +345,18 @@ type GraphQLDataSource struct {
 	Log                log.Logger
 	Client             *http.Client
 	WhitelistedSchemes []string
+	Hooks              Hooks
 }
 
 func (g *GraphQLDataSource) Resolve(ctx context.Context, args ResolverArgs, out io.Writer) (n int, err error) {
 	urlArg := args.ByKey(literal.URL)
 	queryArg := args.ByKey(literal.QUERY)
+	rootTypeName := args.ByKey(RootTypeName)
+	rootFieldName := args.ByKey(RootFieldName)
+	hookContext := HookContext{
+		TypeName:  string(rootTypeName),
+		FieldName: string(rootFieldName),
+	}
 
 	g.Log.Debug("GraphQLDataSource.Resolve.Args",
 		log.Strings("resolvedArgs", args.Dump()),
@@ -359,6 +383,8 @@ func (g *GraphQLDataSource) Resolve(ctx context.Context, args ResolverArgs, out
 		switch {
 		case bytes.Equal(keys[i], literal.URL):
 		case bytes.Equal(keys[i], literal.QUERY):
+		case bytes.Equal(keys[i], RootTypeName):
+		case bytes.Equal(keys[i], RootFieldName):
 		default:
 			variables[string(keys[i])] = string(args.ByKey(keys[i]))
 		}
@@ -403,6 +429,10 @@ func (g *GraphQLDataSource) Resolve(ctx context.Context, args ResolverArgs, out
 	request.Header.Add("Content-Type", "application/json")
 	request.Header.Add("Accept", "application/json")
 
+	if g.Hooks.PreSendHttpHook != nil {
+		g.Hooks.PreSendHttpHook.Execute(hookContext, request)
+	}
+
 	res, err := g.Client.Do(request)
 	if err != nil {
 		g.Log.Error("GraphQLDataSource.client.Do",
@@ -418,6 +448,10 @@ func (g *GraphQLDataSource) Resolve(ctx context.Context, args ResolverArgs, out
 		return n, err
 	}
 
+	if g.Hooks.PostReceiveHttpHook != nil {
+		g.Hooks.PostReceiveHttpHook.Execute(hookContext, res, data)
+	}
+
 	defer func() {
 		err := res.Body.Close()
 		if err != nil {
diff --git a/pkg/execution/datasource/datasource_http_json.go b/pkg/execution/datasource/datasource_http_json.go
index a357452d..ed5a2c0a 100644
--- a/pkg/execution/datasource/datasource_http_json.go
+++ b/pkg/execution/datasource/datasource_http_json.go
@@ -58,6 +58,7 @@ type HttpJsonDataSourceConfigHeader struct {
 type HttpJsonDataSourcePlannerFactoryFactory struct {
 	Client             *http.Client
 	WhitelistedSchemes []string
+	Hooks              Hooks
 }
 
 func (h *HttpJsonDataSourcePlannerFactoryFactory) httpClient() *http.Client {
@@ -72,6 +73,7 @@ func (h *HttpJsonDataSourcePlannerFactoryFactory) Initialize(base BasePlanner, c
 		base:               base,
 		client:             h.httpClient(),
 		whitelistedSchemes: h.WhitelistedSchemes,
+		hooks:              h.Hooks,
 	}
 	err := json.NewDecoder(configReader).Decode(&factory.config)
 	return factory, err
@@ -82,6 +84,7 @@ type HttpJsonDataSourcePlannerFactory struct {
 	config             HttpJsonDataSourceConfig
 	client             *http.Client
 	whitelistedSchemes []string
+	hooks              Hooks
 }
 
 func (h *HttpJsonDataSourcePlannerFactory) DataSourcePlanner() Planner {
@@ -90,6 +93,7 @@ func (h *HttpJsonDataSourcePlannerFactory) DataSourcePlanner() Planner {
 		dataSourceConfig:   h.config,
 		client:             h.client,
 		whitelistedSchemes: h.whitelistedSchemes,
+		hooks:              h.hooks,
 	}
 }
 
@@ -98,6 +102,7 @@ type HttpJsonDataSourcePlanner struct {
 	dataSourceConfig   HttpJsonDataSourceConfig
 	client             *http.Client
 	whitelistedSchemes []string
+	hooks              Hooks
 }
 
 func (h *HttpJsonDataSourcePlanner) Plan(args []Argument) (DataSource, []Argument) {
@@ -105,6 +110,7 @@ func (h *HttpJsonDataSourcePlanner) Plan(args []Argument) (DataSource, []Argumen
 		Log:                h.Log,
 		Client:             h.client,
 		WhitelistedSchemes: h.whitelistedSchemes,
+		Hooks:              h.hooks,
 	}, append(h.Args, args...)
 }
 
@@ -129,7 +135,22 @@ func (h *HttpJsonDataSourcePlanner) LeaveSelectionSet(ref int) {
 }
 
 func (h *HttpJsonDataSourcePlanner) EnterField(ref int) {
-	h.RootField.SetIfNotDefined(ref)
+	if !h.RootField.isDefined {
+		h.RootField.SetIfNotDefined(ref)
+
+		typeName := h.Definition.NodeNameBytes(h.Walker.EnclosingTypeDefinition)
+		fieldName := h.Operation.FieldNameBytes(ref)
+
+		h.Args = append(h.Args, &StaticVariableArgument{
+			Name:  RootTypeName,
+			Value: typeName,
+		})
+
+		h.Args = append(h.Args, &StaticVariableArgument{
+			Name:  RootFieldName,
+			Value: fieldName,
+		})
+	}
 }
 
 func (h *HttpJsonDataSourcePlanner) EnterArgument(ref int) {}
@@ -219,6 +240,7 @@ type HttpJsonDataSource struct {
 	Log                log.Logger
 	Client             *http.Client
 	WhitelistedSchemes []string
+	Hooks              Hooks
 }
 
 func (r *HttpJsonDataSource) Resolve(ctx context.Context, args ResolverArgs, out io.Writer) (n int, err error) {
@@ -227,6 +249,12 @@ func (r *HttpJsonDataSource) Resolve(ctx context.Context, args ResolverArgs, out
 	bodyArg := args.ByKey(literal.BODY)
 	headersArg := args.ByKey(literal.HEADERS)
 	typeNameArg := args.ByKey(literal.TYPENAME)
+	rootTypeName := args.ByKey(RootTypeName)
+	rootFieldName := args.ByKey(RootFieldName)
+	hookContext := HookContext{
+		TypeName:  string(rootTypeName),
+		FieldName: string(rootFieldName),
+	}
 
 	r.Log.Debug("HttpJsonDataSource.Resolve.Args",
 		log.Strings("resolvedArgs", args.Dump()),
@@ -296,6 +324,10 @@ func (r *HttpJsonDataSource) Resolve(ctx context.Context, args ResolverArgs, out
 
 	request.Header = header
 
+	if r.Hooks.PreSendHttpHook != nil {
+		r.Hooks.PreSendHttpHook.Execute(hookContext, request)
+	}
+
 	res, err := r.Client.Do(request)
 	if err != nil {
 		r.Log.Error("HttpJsonDataSource.Resolve.Client.Do",
@@ -312,6 +344,10 @@ func (r *HttpJsonDataSource) Resolve(ctx context.Context, args ResolverArgs, out
 		return
 	}
 
+	if r.Hooks.PostReceiveHttpHook != nil {
+		r.Hooks.PostReceiveHttpHook.Execute(hookContext, res, data)
+	}
+
 	defer func() {
 		err := res.Body.Close()
 		if err != nil {
diff --git a/pkg/execution/datasource/hooks.go b/pkg/execution/datasource/hooks.go
new file mode 100644
index 00000000..ccd397e3
--- /dev/null
+++ b/pkg/execution/datasource/hooks.go
@@ -0,0 +1,23 @@
+package datasource
+
+import (
+	"net/http"
+)
+
+type HookContext struct {
+	TypeName  string
+	FieldName string
+}
+
+type Hooks struct {
+	PreSendHttpHook     PreSendHttpHook
+	PostReceiveHttpHook PostReceiveHttpHook
+}
+
+type PreSendHttpHook interface {
+	Execute(ctx HookContext, req *http.Request)
+}
+
+type PostReceiveHttpHook interface {
+	Execute(ctx HookContext, resp *http.Response, body []byte)
+}
diff --git a/pkg/fastbuffer/fastbuffer.go b/pkg/fastbuffer/fastbuffer.go
index faa5ad38..85140f1e 100644
--- a/pkg/fastbuffer/fastbuffer.go
+++ b/pkg/fastbuffer/fastbuffer.go
@@ -7,7 +7,7 @@ import (
 
 func New() *FastBuffer {
 	return &FastBuffer{
-		b: make([]byte,0,1024),
+		b: make([]byte, 0, 1024),
 	}
 }
 
@@ -16,8 +16,8 @@ type FastBuffer struct {
 }
 
 func (f *FastBuffer) Write(p []byte) (n int, err error) {
-	f.b = append(f.b,p...)
-	return len(p),nil
+	f.b = append(f.b, p...)
+	return len(p), nil
 }
 
 func (f *FastBuffer) Reset() {
@@ -25,18 +25,18 @@ func (f *FastBuffer) Reset() {
 }
 
 func (f *FastBuffer) WriteBytes(b []byte) {
-	f.b = append(f.b,b...)
+	f.b = append(f.b, b...)
 }
 
-func (f *FastBuffer) WriteString(s string){
-	f.b = append(f.b,s...)
+func (f *FastBuffer) WriteString(s string) {
+	f.b = append(f.b, s...)
 }
 
 func (f *FastBuffer) Bytes() []byte {
 	return f.b
 }
 
-func (f *FastBuffer) Len () int {
+func (f *FastBuffer) Len() int {
 	return len(f.b)
 }
 
diff --git a/pkg/federation/schema.go b/pkg/federation/schema.go
index 86259185..131f67be 100644
--- a/pkg/federation/schema.go
+++ b/pkg/federation/schema.go
@@ -198,4 +198,4 @@ directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
 directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
 directive @key(fields: _FieldSet!) on OBJECT | INTERFACE
 directive @extends on OBJECT | INTERFACE
-`
\ No newline at end of file
+`
diff --git a/pkg/graphql/complexity.go b/pkg/graphql/complexity.go
index 54fa2d3d..c658ee49 100644
--- a/pkg/graphql/complexity.go
+++ b/pkg/graphql/complexity.go
@@ -17,24 +17,47 @@ type defaultComplexityCalculator struct {
 
 func (d defaultComplexityCalculator) Calculate(operation, definition *ast.Document) (ComplexityResult, error) {
 	report := operationreport.Report{}
-	nodeCount, complexity, depth := operation_complexity.CalculateOperationComplexity(operation, definition, &report)
+	globalComplexityResult, fieldsComplexityResult := operation_complexity.CalculateOperationComplexity(operation, definition, &report)
 
-	return complexityResult(nodeCount, complexity, depth, report)
+	return complexityResult(globalComplexityResult, fieldsComplexityResult, report)
 }
 
 type ComplexityResult struct {
+	NodeCount    int
+	Complexity   int
+	Depth        int
+	PerRootField []FieldComplexityResult
+	Errors       Errors
+}
+
+type FieldComplexityResult struct {
+	TypeName   string
+	FieldName  string
+	Alias      string
 	NodeCount  int
 	Complexity int
 	Depth      int
-	Errors     Errors
 }
 
-func complexityResult(nodeCount, complexity, depth int, report operationreport.Report) (ComplexityResult, error) {
+func complexityResult(globalComplexityResult operation_complexity.OperationStats, fieldsComplexityResult []operation_complexity.RootFieldStats, report operationreport.Report) (ComplexityResult, error) {
+	allFieldComplexityResults := make([]FieldComplexityResult, 0, len(fieldsComplexityResult))
+	for _, fieldResult := range fieldsComplexityResult {
+		allFieldComplexityResults = append(allFieldComplexityResults, FieldComplexityResult{
+			TypeName:   fieldResult.TypeName,
+			FieldName:  fieldResult.FieldName,
+			Alias:      fieldResult.Alias,
+			NodeCount:  fieldResult.Stats.NodeCount,
+			Complexity: fieldResult.Stats.Complexity,
+			Depth:      fieldResult.Stats.Depth,
+		})
+	}
+
 	result := ComplexityResult{
-		NodeCount:  nodeCount,
-		Complexity: complexity,
-		Depth:      depth,
-		Errors:     nil,
+		NodeCount:    globalComplexityResult.NodeCount,
+		Complexity:   globalComplexityResult.Complexity,
+		Depth:        globalComplexityResult.Depth,
+		PerRootField: allFieldComplexityResults,
+		Errors:       nil,
 	}
 
 	if !report.HasErrors() {
diff --git a/pkg/graphql/execution_engine.go b/pkg/graphql/execution_engine.go
index 64b24948..f2b921f3 100644
--- a/pkg/graphql/execution_engine.go
+++ b/pkg/graphql/execution_engine.go
@@ -19,11 +19,13 @@ import (
 type DataSourceHttpJsonOptions struct {
 	HttpClient         *http.Client
 	WhitelistedSchemes []string
+	Hooks              *datasource.Hooks
 }
 
 type DataSourceGraphqlOptions struct {
 	HttpClient         *http.Client
 	WhitelistedSchemes []string
+	Hooks              *datasource.Hooks
 }
 
 type ExecutionOptions struct {
@@ -72,6 +74,10 @@ func (e *ExecutionEngine) AddHttpJsonDataSourceWithOptions(name string, options
 		httpJsonFactoryFactory.WhitelistedSchemes = options.WhitelistedSchemes
 	}
 
+	if options.Hooks != nil {
+		httpJsonFactoryFactory.Hooks = *options.Hooks
+	}
+
 	return e.AddDataSource(name, httpJsonFactoryFactory)
 }
 
@@ -90,6 +96,10 @@ func (e *ExecutionEngine) AddGraphqlDataSourceWithOptions(name string, options D
 		graphqlFactoryFactory.WhitelistedSchemes = options.WhitelistedSchemes
 	}
 
+	if options.Hooks != nil {
+		graphqlFactoryFactory.Hooks = *options.Hooks
+	}
+
 	return e.AddDataSource(name, graphqlFactoryFactory)
 }
 
@@ -135,6 +145,10 @@ func (e *ExecutionEngine) Execute(ctx context.Context, operation *Request, optio
 	return &ExecutionResult{&buf}, err
 }
 
+func (e *ExecutionEngine) NewExecutionHandler() *execution.Handler {
+	return execution.NewHandler(e.basePlanner, nil)
+}
+
 type ExecutionResult struct {
 	buf *bytes.Buffer
 }
diff --git a/pkg/graphql/fixtures/introspection_response.golden b/pkg/graphql/fixtures/introspection_response.golden
new file mode 100644
index 00000000..f3ddabdd
--- /dev/null
+++ b/pkg/graphql/fixtures/introspection_response.golden
@@ -0,0 +1,1 @@
+{"data":{"__schema":{"queryType":{"name":"Query"},"mutationType":null,"subscriptionType":null,"types":[{"kind":"OBJECT","name":"Query","description":"","fields":[{"name":"hello","description":"","args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":[],"interfaces":[],"enumValues":[],"possibleTypes":[]},{"kind":"SCALAR","name":"Int","description":"The 'Int' scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.","fields":[],"inputFields":[],"interfaces":[],"enumValues":[],"possibleTypes":[]},{"kind":"SCALAR","name":"Float","description":"The 'Float' scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point).","fields":[],"inputFields":[],"interfaces":[],"enumValues":[],"possibleTypes":[]},{"kind":"SCALAR","name":"String","description":"The 'String' scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.","fields":[],"inputFields":[],"interfaces":[],"enumValues":[],"possibleTypes":[]},{"kind":"SCALAR","name":"Boolean","description":"The 'Boolean' scalar type represents 'true' or 'false' .","fields":[],"inputFields":[],"interfaces":[],"enumValues":[],"possibleTypes":[]},{"kind":"SCALAR","name":"ID","description":"The 'ID' scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as '4') or integer (such as 4) input value will be accepted as an ID.","fields":[],"inputFields":[],"interfaces":[],"enumValues":[],"possibleTypes":[]}],"directives":[{"name":"include","description":"Directs the executor to include this field or fragment only when the argument is true.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":"Included when true.","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"skip","description":"Directs the executor to skip this field or fragment when the argument is true.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":"Skipped when true.","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"deprecated","description":"Marks an element of a GraphQL schema as no longer supported.","locations":["FIELD_DEFINITION","ENUM_VALUE"],"args":[{"name":"reason","description":"Explains why this element was deprecated, usually also including a suggestion\n    for how to access supported similar data. Formatted in\n    [Markdown](https://daringfireball.net/projects/markdown/).","type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":"\"No longer supported\""}]}]}}}
diff --git a/pkg/graphql/normalization.go b/pkg/graphql/normalization.go
index dfcb43d7..4b4edcb8 100644
--- a/pkg/graphql/normalization.go
+++ b/pkg/graphql/normalization.go
@@ -23,7 +23,7 @@ func (r *Request) Normalize(schema *Schema) (result NormalizationResult, err err
 	r.document.Input.Variables = r.Variables
 
 	normalizer := astnormalization.NewNormalizer(true, true)
-	normalizer.NormalizeOperation(&r.document, &schema.document, &report)
+	normalizer.NormalizeNamedOperation(&r.document, &schema.document, []byte(r.OperationName), &report)
 	if report.HasErrors() {
 		return normalizationResultFromReport(report)
 	}
diff --git a/pkg/graphql/request.go b/pkg/graphql/request.go
index 522f2d22..95c9309a 100644
--- a/pkg/graphql/request.go
+++ b/pkg/graphql/request.go
@@ -8,9 +8,15 @@ import (
 
 	"github.com/jensneuse/graphql-go-tools/pkg/ast"
 	"github.com/jensneuse/graphql-go-tools/pkg/astparser"
+	"github.com/jensneuse/graphql-go-tools/pkg/middleware/operation_complexity"
 	"github.com/jensneuse/graphql-go-tools/pkg/operationreport"
 )
 
+const (
+	defaultInrospectionQueryName = "IntrospectionQuery"
+	schemaFieldName              = "__schema"
+)
+
 var (
 	ErrEmptyRequest = errors.New("the provided request is empty")
 	ErrNilSchema    = errors.New("the provided schema is nil")
@@ -47,7 +53,11 @@ func (r *Request) CalculateComplexity(complexityCalculator ComplexityCalculator,
 
 	report := r.parseQueryOnce()
 	if report.HasErrors() {
-		return complexityResult(0, 0, 0, report)
+		return complexityResult(
+			operation_complexity.OperationStats{},
+			[]operation_complexity.RootFieldStats{},
+			report,
+		)
 	}
 
 	return complexityCalculator.Calculate(&r.document, &schema.document)
@@ -75,3 +85,43 @@ func (r *Request) parseQueryOnce() (report operationreport.Report) {
 	r.document, report = astparser.ParseGraphqlDocumentString(r.Query)
 	return report
 }
+
+func (r *Request) IsIntrospectionQuery() (result bool, err error) {
+	report := r.parseQueryOnce()
+	if report.HasErrors() {
+		return false, report
+	}
+
+	if r.OperationName == defaultInrospectionQueryName {
+		return true, nil
+	}
+
+	if len(r.document.RootNodes) == 0 {
+		return
+	}
+
+	rootNode := r.document.RootNodes[0]
+	if rootNode.Kind != ast.NodeKindOperationDefinition {
+		return
+	}
+
+	operationDef := r.document.OperationDefinitions[rootNode.Ref]
+	if operationDef.OperationType != ast.OperationTypeQuery {
+		return
+	}
+	if !operationDef.HasSelections {
+		return
+	}
+
+	selectionSet := r.document.SelectionSets[operationDef.SelectionSet]
+	if len(selectionSet.SelectionRefs) == 0 {
+		return
+	}
+
+	selection := r.document.Selections[selectionSet.SelectionRefs[0]]
+	if selection.Kind != ast.SelectionKindField {
+		return
+	}
+
+	return r.document.FieldNameString(selection.Ref) == schemaFieldName, nil
+}
diff --git a/pkg/graphql/schema.go b/pkg/graphql/schema.go
index d3ad928e..61d54971 100644
--- a/pkg/graphql/schema.go
+++ b/pkg/graphql/schema.go
@@ -1,6 +1,8 @@
 package graphql
 
 import (
+	"bytes"
+	"encoding/json"
 	"io"
 	"io/ioutil"
 
@@ -8,6 +10,7 @@ import (
 	"github.com/jensneuse/graphql-go-tools/pkg/astparser"
 	"github.com/jensneuse/graphql-go-tools/pkg/asttransform"
 	"github.com/jensneuse/graphql-go-tools/pkg/astvalidation"
+	"github.com/jensneuse/graphql-go-tools/pkg/introspection"
 	"github.com/jensneuse/graphql-go-tools/pkg/operationreport"
 )
 
@@ -101,6 +104,21 @@ func (s *Schema) Validate() (result ValidationResult, err error) {
 	}, nil
 }
 
+func (s *Schema) IntrospectionResponse(out io.Writer) error {
+	var (
+		introspectionData = struct {
+			Data introspection.Data `json:"data"`
+		}{}
+		report operationreport.Report
+	)
+	gen := introspection.NewGenerator()
+	gen.Generate(&s.document, &report, &introspectionData.Data)
+	if report.HasErrors() {
+		return report
+	}
+	return json.NewEncoder(out).Encode(introspectionData)
+}
+
 func createSchema(schemaContent []byte) (*Schema, error) {
 	document, report := astparser.ParseGraphqlDocumentBytes(schemaContent)
 	if report.HasErrors() {
@@ -117,3 +135,9 @@ func createSchema(schemaContent []byte) (*Schema, error) {
 		document: document,
 	}, nil
 }
+
+func SchemaIntrospection(schema *Schema) (*ExecutionResult, error) {
+	var buf bytes.Buffer
+	err := schema.IntrospectionResponse(&buf)
+	return &ExecutionResult{&buf}, err
+}
diff --git a/pkg/http/handler.go b/pkg/http/handler.go
index f39dda0c..12195d65 100644
--- a/pkg/http/handler.go
+++ b/pkg/http/handler.go
@@ -49,7 +49,7 @@ func (g *GraphQLHTTPRequestHandler) upgradeWithNewGoroutine(w http.ResponseWrite
 	if err != nil {
 		return err
 	}
-	go g.handleWebsocket(conn)
+	g.handleWebsocket(conn)
 	return nil
 }
 
diff --git a/pkg/http/ws.go b/pkg/http/ws.go
index 991631c0..90f669ff 100644
--- a/pkg/http/ws.go
+++ b/pkg/http/ws.go
@@ -9,6 +9,7 @@ import (
 	"github.com/gobwas/ws/wsutil"
 	"github.com/jensneuse/abstractlogger"
 
+	"github.com/jensneuse/graphql-go-tools/pkg/execution"
 	"github.com/jensneuse/graphql-go-tools/pkg/subscription"
 )
 
@@ -30,7 +31,7 @@ func NewWebsocketSubscriptionClient(logger abstractlogger.Logger, clientConn net
 }
 
 // ReadFromClient will read a subscription message from the websocket client.
-func (w *WebsocketSubscriptionClient) ReadFromClient() (message subscription.Message, err error) {
+func (w *WebsocketSubscriptionClient) ReadFromClient() (message *subscription.Message, err error) {
 	var data []byte
 	var opCode ws.OpCode
 
@@ -48,7 +49,7 @@ func (w *WebsocketSubscriptionClient) ReadFromClient() (message subscription.Mes
 
 		w.isClosedConnectionError(err)
 
-		return message, err
+		return nil, err
 	}
 
 	err = json.Unmarshal(data, &message)
@@ -59,7 +60,7 @@ func (w *WebsocketSubscriptionClient) ReadFromClient() (message subscription.Mes
 			abstractlogger.Any("opCode", opCode),
 		)
 
-		return message, err
+		return nil, err
 	}
 
 	return message, nil
@@ -117,26 +118,43 @@ func (w *WebsocketSubscriptionClient) isClosedConnectionError(err error) bool {
 	return w.isClosedConnection
 }
 
-// handleWebsocket will handle the websocket connection.
-func (g *GraphQLHTTPRequestHandler) handleWebsocket(conn net.Conn) {
-	websocketClient := NewWebsocketSubscriptionClient(g.log, conn)
-	subscriptionHandler, err := subscription.NewHandler(g.log, websocketClient, g.executionHandler)
+func HandleWebsocket(done chan bool, errChan chan error, conn net.Conn, executionHandler *execution.Handler, logger abstractlogger.Logger) {
+	defer func() {
+		if err := conn.Close(); err != nil {
+			logger.Error("http.HandleWebsocket()",
+				abstractlogger.String("message", "could not close connection to client"),
+				abstractlogger.Error(err),
+			)
+		}
+	}()
+
+	websocketClient := NewWebsocketSubscriptionClient(logger, conn)
+	subscriptionHandler, err := subscription.NewHandler(logger, websocketClient, executionHandler)
 	if err != nil {
-		g.log.Error("http.GraphQLHTTPRequestHandler.handleWebsocket()",
+		logger.Error("http.HandleWebsocket()",
 			abstractlogger.String("message", "could not create subscriptionHandler"),
 			abstractlogger.Error(err),
 		)
 
+		errChan <- err
 		return
 	}
 
+	close(done)
 	subscriptionHandler.Handle(context.Background()) // Blocking
+}
 
-	err = conn.Close()
-	if err != nil {
+// handleWebsocket will handle the websocket connection.
+func (g *GraphQLHTTPRequestHandler) handleWebsocket(conn net.Conn) {
+	done := make(chan bool)
+	errChan := make(chan error)
+
+	go HandleWebsocket(done, errChan, conn, g.executionHandler, g.log)
+	select {
+	case err := <-errChan:
 		g.log.Error("http.GraphQLHTTPRequestHandler.handleWebsocket()",
-			abstractlogger.String("message", "could not close connection to client"),
 			abstractlogger.Error(err),
 		)
+	case <-done:
 	}
 }
diff --git a/pkg/introspection/generator.go b/pkg/introspection/generator.go
index 735da38d..02ebb819 100644
--- a/pkg/introspection/generator.go
+++ b/pkg/introspection/generator.go
@@ -422,7 +422,7 @@ func (i *introspectionVisitor) TypeRef(typeRef int) TypeRef {
 	switch i.definition.Types[typeRef].TypeKind {
 	case ast.TypeKindNamed:
 		name := i.definition.TypeNameBytes(typeRef)
-		node,exists := i.definition.Index.FirstNodeByNameBytes(name)
+		node, exists := i.definition.Index.FirstNodeByNameBytes(name)
 		if !exists {
 			return TypeRef{}
 		}
diff --git a/pkg/middleware/operation_complexity/operation_complexity.go b/pkg/middleware/operation_complexity/operation_complexity.go
index 4d877a6c..d6b93059 100644
--- a/pkg/middleware/operation_complexity/operation_complexity.go
+++ b/pkg/middleware/operation_complexity/operation_complexity.go
@@ -27,6 +27,19 @@ import (
 	"github.com/jensneuse/graphql-go-tools/pkg/operationreport"
 )
 
+type OperationStats struct {
+	NodeCount  int
+	Complexity int
+	Depth      int
+}
+
+type RootFieldStats struct {
+	TypeName  string
+	FieldName string
+	Alias     string
+	Stats     OperationStats
+}
+
 var (
 	nodeCountMultiply = []byte("nodeCountMultiply")
 	nodeCountSkip     = []byte("nodeCountSkip")
@@ -58,7 +71,7 @@ func NewOperationComplexityEstimator() *OperationComplexityEstimator {
 	}
 }
 
-func (n *OperationComplexityEstimator) Do(operation, definition *ast.Document, report *operationreport.Report) (nodeCount, complexity, depth int) {
+func (n *OperationComplexityEstimator) Do(operation, definition *ast.Document, report *operationreport.Report) (OperationStats, []RootFieldStats) {
 	n.visitor.count = 0
 	n.visitor.complexity = 0
 	n.visitor.maxFieldDepth = 0
@@ -67,13 +80,31 @@ func (n *OperationComplexityEstimator) Do(operation, definition *ast.Document, r
 	n.visitor.maxSelectionSetFieldDepth = 0
 	n.visitor.selectionSetDepth = 0
 
+	if n.visitor.calculatedRootFieldStats == nil {
+		n.visitor.calculatedRootFieldStats = make([]RootFieldStats, 0, len(definition.RootOperationTypeDefinitions))
+	}
+	n.visitor.calculatedRootFieldStats = n.visitor.calculatedRootFieldStats[:0]
+
+	if n.visitor.rootOperationTypeNames == nil {
+		n.visitor.rootOperationTypeNames = make(map[string]struct{}, len(definition.RootOperationTypeDefinitions))
+	}
+	for key := range n.visitor.rootOperationTypeNames {
+		delete(n.visitor.rootOperationTypeNames, key)
+	}
+
 	n.walker.Walk(operation, definition, report)
 
-	depth = n.visitor.maxFieldDepth - n.visitor.selectionSetDepth
-	return n.visitor.count, n.visitor.complexity, depth
+	depth := n.visitor.maxFieldDepth - n.visitor.selectionSetDepth
+	globalResult := OperationStats{
+		NodeCount:  n.visitor.count,
+		Complexity: n.visitor.complexity,
+		Depth:      depth,
+	}
+
+	return globalResult, n.visitor.calculatedRootFieldStats
 }
 
-func CalculateOperationComplexity(operation, definition *ast.Document, report *operationreport.Report) (nodeCount, complexity, depth int) {
+func CalculateOperationComplexity(operation, definition *ast.Document, report *operationreport.Report) (OperationStats, []RootFieldStats) {
 	estimator := NewOperationComplexityEstimator()
 	return estimator.Do(operation, definition, report)
 }
@@ -88,6 +119,15 @@ type complexityVisitor struct {
 
 	maxSelectionSetFieldDepth int
 	selectionSetDepth         int
+
+	rootOperationTypeNames map[string]struct{}
+
+	currentRootFieldStats                RootFieldStats
+	currentRootFieldMaxDepth             int
+	currentRootFieldMaxSelectionSetDepth int
+	currentRootFieldSelectionSetDepth    int
+
+	calculatedRootFieldStats []RootFieldStats
 }
 
 type multiplier struct {
@@ -105,6 +145,11 @@ func (c *complexityVisitor) calculateMultiplied(i int) int {
 func (c *complexityVisitor) EnterDocument(operation, definition *ast.Document) {
 	c.operation = operation
 	c.definition = definition
+
+	for i := 0; i < len(c.definition.RootOperationTypeDefinitions); i++ {
+		name := c.definition.Input.ByteSliceString(c.definition.RootOperationTypeDefinitions[i].NamedType.Name)
+		c.rootOperationTypeNames[name] = struct{}{}
+	}
 }
 
 func (c *complexityVisitor) EnterArgument(ref int) {
@@ -143,18 +188,30 @@ func (c *complexityVisitor) EnterField(ref int) {
 		return
 	}
 
+	typeName, fieldName, alias := c.extractFieldRelatedNames(ref, definition)
+	if c.isRootType(typeName) {
+		c.resetCurrentRootFieldComplexity(typeName, fieldName, alias)
+	}
+
 	if !c.operation.FieldHasSelections(ref) {
 		return
 	}
 
 	c.complexity = c.complexity + c.calculateMultiplied(1)
-
 	if c.Depth > c.maxFieldDepth {
 		c.maxFieldDepth = c.Depth
 	}
+
+	c.currentRootFieldStats.Stats.Complexity = c.currentRootFieldStats.Stats.Complexity + c.calculateMultiplied(1)
+	if c.Depth > c.currentRootFieldMaxDepth {
+		c.currentRootFieldMaxDepth = c.Depth
+	}
 }
 
 func (c *complexityVisitor) LeaveField(ref int) {
+	if c.isRootTypeField() {
+		c.endRootFieldComplexityCalculation()
+	}
 
 	if len(c.multipliers) == 0 {
 		return
@@ -172,13 +229,64 @@ func (c *complexityVisitor) EnterSelectionSet(ref int) {
 	}
 
 	c.count = c.count + c.calculateMultiplied(1)
-
 	if c.Depth > c.maxSelectionSetFieldDepth {
 		c.maxSelectionSetFieldDepth = c.Depth
 		c.selectionSetDepth++
 	}
+
+	c.currentRootFieldStats.Stats.NodeCount = c.currentRootFieldStats.Stats.NodeCount + c.calculateMultiplied(1)
+	if c.Depth > c.currentRootFieldMaxSelectionSetDepth {
+		c.currentRootFieldMaxSelectionSetDepth = c.Depth
+		c.currentRootFieldSelectionSetDepth++
+	}
 }
 
 func (c *complexityVisitor) EnterFragmentDefinition(ref int) {
 	c.SkipNode()
 }
+
+func (c *complexityVisitor) resetCurrentRootFieldComplexity(typeName, fieldName, alias string) {
+	c.currentRootFieldStats = RootFieldStats{
+		TypeName:  typeName,
+		FieldName: fieldName,
+		Alias:     alias,
+		Stats: OperationStats{
+			NodeCount:  0,
+			Complexity: 0,
+			Depth:      0,
+		},
+	}
+}
+
+func (c *complexityVisitor) endRootFieldComplexityCalculation() {
+	currentDepth := c.currentRootFieldMaxDepth - c.currentRootFieldSelectionSetDepth
+	if currentDepth > 0 {
+		currentDepth--
+	}
+	c.currentRootFieldStats.Stats.Depth = currentDepth
+	c.calculatedRootFieldStats = append(c.calculatedRootFieldStats, c.currentRootFieldStats)
+
+	c.currentRootFieldMaxDepth = 0
+	c.currentRootFieldMaxSelectionSetDepth = 0
+	c.currentRootFieldSelectionSetDepth = 0
+}
+
+func (c *complexityVisitor) extractFieldRelatedNames(ref, definitionRef int) (typeName, fieldName, alias string) {
+	fieldName = c.definition.FieldDefinitionNameString(definitionRef)
+	alias = c.operation.FieldAliasOrNameString(ref)
+	if fieldName == alias {
+		alias = ""
+	}
+
+	return c.EnclosingTypeDefinition.NameString(c.definition), fieldName, alias
+}
+
+func (c *complexityVisitor) isRootType(name string) bool {
+	_, ok := c.rootOperationTypeNames[name]
+	return ok
+}
+
+func (c *complexityVisitor) isRootTypeField() bool {
+	enclosingTypeName := c.EnclosingTypeDefinition.NameString(c.definition)
+	return c.isRootType(enclosingTypeName)
+}
diff --git a/pkg/postprocess/defer.go b/pkg/postprocess/defer.go
index 807666f7..df149c44 100644
--- a/pkg/postprocess/defer.go
+++ b/pkg/postprocess/defer.go
@@ -42,7 +42,7 @@ func (p *ProcessDefer) synchronousResponse(pre *plan.SynchronousResponsePlan) pl
 		FlushInterval: pre.FlushInterval,
 		Response: resolve.GraphQLStreamingResponse{
 			InitialResponse: pre.Response,
-			FlushInterval: pre.FlushInterval,
+			FlushInterval:   pre.FlushInterval,
 		},
 	}
 	p.traverseNode(p.out.Response.InitialResponse.Data)
@@ -123,7 +123,7 @@ func (p *ProcessDefer) bufferUsedOnNonDeferField(object *resolve.Object, field,
 	return false
 }
 
-func (p *ProcessDefer) processFieldSetBuffer(object *resolve.Object,field int) (patchFetch resolve.SingleFetch, ok bool) {
+func (p *ProcessDefer) processFieldSetBuffer(object *resolve.Object, field int) (patchFetch resolve.SingleFetch, ok bool) {
 	id := object.Fields[field].BufferID
 	if p.objects[len(p.objects)-1].Fetch == nil {
 		return patchFetch, false
diff --git a/pkg/starwars/starwars.go b/pkg/starwars/starwars.go
index 4e4942f9..32080ae3 100644
--- a/pkg/starwars/starwars.go
+++ b/pkg/starwars/starwars.go
@@ -30,6 +30,8 @@ const (
 	FileRemainingJedisSubscription = "testdata/subscriptions/remaining_jedis.subscription"
 	FileIntrospectionQuery         = "testdata/queries/introspection.query"
 	FileMultiQueries               = "testdata/queries/multi_queries.query"
+	FileMultiQueriesWithArguments  = "testdata/queries/multi_queries_with_arguments.query"
+	FileInvalidQuery               = "testdata/queries/invalid.query"
 )
 
 var (
diff --git a/pkg/subscription/handler.go b/pkg/subscription/handler.go
index b3c98db6..a4c4ef21 100644
--- a/pkg/subscription/handler.go
+++ b/pkg/subscription/handler.go
@@ -4,10 +4,13 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
+	"strings"
+	"sync"
 	"time"
 
 	"github.com/jensneuse/abstractlogger"
 
+	"github.com/jensneuse/graphql-go-tools/pkg/ast"
 	"github.com/jensneuse/graphql-go-tools/pkg/execution"
 )
 
@@ -37,7 +40,7 @@ type Message struct {
 // client provides an interface which can be implemented by any possible subscription client like websockets, mqtt, etc.
 type Client interface {
 	// ReadFromClient will invoke a read operation from the client connection.
-	ReadFromClient() (Message, error)
+	ReadFromClient() (*Message, error)
 	// WriteToClient will invoke a write operation to the client connection.
 	WriteToClient(Message) error
 	// IsConnected will indicate if a connection is still established.
@@ -59,6 +62,8 @@ type Handler struct {
 	subCancellations subscriptionCancellations
 	// executionHandler will handle the graphql execution.
 	executionHandler *execution.Handler
+	// bufferPool will hold buffers.
+	bufferPool *sync.Pool
 }
 
 // NewHandler creates a new subscription handler.
@@ -80,6 +85,11 @@ func NewHandler(logger abstractlogger.Logger, client Client, executionHandler *e
 		subscriptionUpdateInterval: subscriptionUpdateInterval,
 		subCancellations:           subscriptionCancellations{},
 		executionHandler:           executionHandler,
+		bufferPool: &sync.Pool{
+			New: func() interface{} {
+				return bytes.NewBuffer(make([]byte, 0, 1024))
+			},
+		},
 	}, nil
 }
 
@@ -106,7 +116,7 @@ func (h *Handler) Handle(ctx context.Context) {
 			)
 
 			h.handleConnectionError("could not read message from client")
-		} else {
+		} else if message != nil {
 			switch message.Type {
 			case MessageTypeConnectionInit:
 				h.handleInit()
@@ -156,23 +166,54 @@ func (h *Handler) handleInit() {
 
 // handleStart will handle s start message.
 func (h *Handler) handleStart(id string, payload []byte) {
-	ctx := h.subCancellations.Add(id)
-	go h.startSubscription(ctx, id, payload)
+	executor, node, executionContext, err := h.executionHandler.Handle(payload, []byte(""))
+	if err != nil {
+		h.logger.Error("subscription.Handler.handleStart()",
+			abstractlogger.Error(err),
+		)
+
+		h.handleError(id, cleanErrorMessage(err))
+		return
+	}
+
+	if node.OperationType() == ast.OperationTypeSubscription {
+		ctx := h.subCancellations.Add(id)
+		go h.startSubscription(ctx, id, executor, node, executionContext)
+		return
+	}
+
+	go h.handleNonSubscriptionOperation(id, executor, node, executionContext)
 }
 
-// startSubscription will invoke the actual subscription.
-func (h *Handler) startSubscription(ctx context.Context, id string, data []byte) {
-	executor, node, executionContext, err := h.executionHandler.Handle(data, []byte(""))
+// handleNonSubscriptionOperation will handle a non-subscription operation like a query or a mutation.
+func (h *Handler) handleNonSubscriptionOperation(id string, executor *execution.Executor, node execution.RootNode, executionContext execution.Context) {
+	buf := h.bufferPool.Get().(*bytes.Buffer)
+	buf.Reset()
+
+	err := executor.Execute(executionContext, node, buf)
 	if err != nil {
-		h.logger.Error("subscription.Handler.startSubscription()",
+		h.logger.Error("subscription.Handle.handleNonSubscriptionOperation()",
 			abstractlogger.Error(err),
 		)
 
-		h.handleError(id, "error on subscription execution")
+		h.handleError(id, err)
+		return
 	}
 
+	h.logger.Debug("subscription.Handle.handleNonSubscriptionOperation()",
+		abstractlogger.ByteString("execution_result", buf.Bytes()),
+	)
+
+	h.sendData(id, buf.Bytes())
+	h.sendComplete(id)
+}
+
+// startSubscription will invoke the actual subscription.
+func (h *Handler) startSubscription(ctx context.Context, id string, executor *execution.Executor, node execution.RootNode, executionContext execution.Context) {
 	executionContext.Context = ctx
-	buf := bytes.NewBuffer(make([]byte, 0, 1024))
+	buf := h.bufferPool.Get().(*bytes.Buffer)
+	buf.Reset()
+
 	h.executeSubscription(buf, id, executor, node, executionContext)
 
 	for {
@@ -195,7 +236,7 @@ func (h *Handler) executeSubscription(buf *bytes.Buffer, id string, executor *ex
 			abstractlogger.Error(err),
 		)
 
-		h.handleError(id, "error on subscription execution")
+		h.handleError(id, err)
 		return
 	}
 
@@ -209,6 +250,7 @@ func (h *Handler) executeSubscription(buf *bytes.Buffer, id string, executor *ex
 // handleStop will handle a stop message,
 func (h *Handler) handleStop(id string) {
 	h.subCancellations.Cancel(id)
+	h.sendComplete(id)
 }
 
 // sendData will send a data message to the client.
@@ -338,3 +380,8 @@ func (h *Handler) handleError(id string, errorPayload interface{}) {
 func (h *Handler) ActiveSubscriptions() int {
 	return len(h.subCancellations)
 }
+
+func cleanErrorMessage(err error) string {
+	errMsg := strings.TrimPrefix(err.Error(), "external: ")
+	return errMsg
+}


Test patch:
diff --git a/pkg/ast/ast_operation_definition_test.go b/pkg/ast/ast_operation_definition_test.go
new file mode 100644
index 00000000..a7a8007b
--- /dev/null
+++ b/pkg/ast/ast_operation_definition_test.go
@@ -0,0 +1,78 @@
+package ast
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDocument_OperationNameExists(t *testing.T) {
+	run := func(operationDefinitionsFunc func(doc *Document) []OperationDefinition, operationName string, expectedExists bool) func(t *testing.T) {
+		return func(t *testing.T) {
+			doc := Document{}
+			operationDefinitions := operationDefinitionsFunc(&doc)
+			doc.OperationDefinitions = append(doc.OperationDefinitions, operationDefinitions...)
+			for i := range doc.OperationDefinitions {
+				doc.RootNodes = append(doc.RootNodes, Node{
+					Kind: NodeKindOperationDefinition,
+					Ref: i,
+				})
+			}
+			exists := doc.OperationNameExists(operationName)
+			assert.Equal(t, expectedExists, exists)
+		}
+	}
+
+	t.Run("not found on empty document", run(
+		func(doc *Document) []OperationDefinition {
+			return []OperationDefinition{}
+		},
+		"MyOperation",
+		false,
+	))
+
+	t.Run("not found on document with multiple operations", run(
+		func(doc *Document) []OperationDefinition {
+			return []OperationDefinition{
+				{
+					Name: doc.Input.AppendInputString("OtherOperation"),
+				},
+				{
+					Name: doc.Input.AppendInputString("AnotherOperation"),
+				},
+			}
+		},
+		"MyOperation",
+		false,
+	))
+
+	t.Run("found on document with a single operations", run(
+		func(doc *Document) []OperationDefinition {
+			return []OperationDefinition{
+				{
+					Name: doc.Input.AppendInputString("MyOperation"),
+				},
+			}
+		},
+		"MyOperation",
+		true,
+	))
+
+	t.Run("found on document with multiple operations", run(
+		func(doc *Document) []OperationDefinition {
+			return []OperationDefinition{
+				{
+					Name: doc.Input.AppendInputString("OtherOperation"),
+				},
+				{
+					Name: doc.Input.AppendInputString("MyOperation"),
+				},
+				{
+					Name: doc.Input.AppendInputString("AnotherOperation"),
+				},
+			}
+		},
+		"MyOperation",
+		true,
+	))
+}
diff --git a/pkg/ast/ast_type_test.go b/pkg/ast/ast_type_test.go
index 76a3e30e..646c6413 100644
--- a/pkg/ast/ast_type_test.go
+++ b/pkg/ast/ast_type_test.go
@@ -10,7 +10,7 @@ func TestDocument_TypeValueNeedsQuotes(t *testing.T) {
 	doc := Document{}
 	definition := NewDocument()
 
-	definition.Index.AddNodeStr("SomeEnum",Node{
+	definition.Index.AddNodeStr("SomeEnum", Node{
 		Kind: NodeKindEnumTypeDefinition,
 	})
 
diff --git a/pkg/astnormalization/astnormalization_test.go b/pkg/astnormalization/astnormalization_test.go
index b2b54394..b28e756e 100644
--- a/pkg/astnormalization/astnormalization_test.go
+++ b/pkg/astnormalization/astnormalization_test.go
@@ -185,7 +185,7 @@ var mustString = func(str string, err error) string {
 	return str
 }
 
-var runWithVariables = func(t *testing.T, normalizeFunc registerNormalizeVariablesFunc, definition, operation,operationName, expectedOutput, variablesInput, expectedVariables string) {
+var runWithVariables = func(t *testing.T, normalizeFunc registerNormalizeVariablesFunc, definition, operation, operationName, expectedOutput, variablesInput, expectedVariables string) {
 	definitionDocument := unsafeparser.ParseGraphqlDocumentString(definition)
 	operationDocument := unsafeparser.ParseGraphqlDocumentString(operation)
 	expectedOutputDocument := unsafeparser.ParseGraphqlDocumentString(expectedOutput)
diff --git a/pkg/astprinter/astprinter_test.go b/pkg/astprinter/astprinter_test.go
index a4dc70cf..ed2a9e8d 100644
--- a/pkg/astprinter/astprinter_test.go
+++ b/pkg/astprinter/astprinter_test.go
@@ -81,7 +81,7 @@ func TestPrint(t *testing.T) {
 					  """
 					  vary: [String]! = []
 					) on QUERY`,
-	`"""
+			`"""
 directive @cache
 """
 directive @cache("maxAge defines the maximum time in seconds a response will be understood 'fresh', defaults to 300 (5 minutes)"
diff --git a/pkg/astvalidation/astvalidation_test.go b/pkg/astvalidation/astvalidation_test.go
index 9a2f097f..0f426f09 100644
--- a/pkg/astvalidation/astvalidation_test.go
+++ b/pkg/astvalidation/astvalidation_test.go
@@ -3473,7 +3473,7 @@ func TestExecutionValidation(t *testing.T) {
 					AllVariablesUsed(), Invalid)
 			})
 			t.Run("variables in array object", func(t *testing.T) {
-				runWithDefinition(todoSchema,`mutation AddTak($title: String!, $completed: Boolean!, $name: String! @fromClaim(name: "sub")) {
+				runWithDefinition(todoSchema, `mutation AddTak($title: String!, $completed: Boolean!, $name: String! @fromClaim(name: "sub")) {
   									addTask(input: [{title: $title, completed: $completed, user: {name: $name}}]){
 										task {
 										  id
@@ -4978,4 +4978,4 @@ directive @fromClaim(
   """
   name: String!
 ) on VARIABLE_DEFINITION
-`
\ No newline at end of file
+`
diff --git a/pkg/astvalidation/overlapping_fields_test.go b/pkg/astvalidation/overlapping_fields_test.go
index d4bcd8d2..ab1f0497 100644
--- a/pkg/astvalidation/overlapping_fields_test.go
+++ b/pkg/astvalidation/overlapping_fields_test.go
@@ -24,7 +24,7 @@ func TestOverlappingFieldsCanBeMerged(t *testing.T) {
 		definition := unsafeparser.ParseGraphqlDocumentBytes(definitionBytes)
 		operation := unsafeparser.ParseGraphqlDocumentBytes(operationBytes)
 		report := operationreport.Report{}
-		normalizer := astnormalization.NewNormalizer(false,false)
+		normalizer := astnormalization.NewNormalizer(false, false)
 		validator := DefaultOperationValidator()
 
 		normalizer.NormalizeOperation(&operation, &definition, &report)
@@ -45,7 +45,7 @@ func TestOverlappingFieldsCanBeMerged(t *testing.T) {
 		definition := unsafeparser.ParseGraphqlDocumentBytes(definitionBytes)
 		operation := unsafeparser.ParseGraphqlDocumentBytes(operationBytes)
 		report := operationreport.Report{}
-		normalizer := astnormalization.NewNormalizer(false,false)
+		normalizer := astnormalization.NewNormalizer(false, false)
 		validator := DefaultOperationValidator()
 
 		normalizer.NormalizeOperation(&operation, &definition, &report)
@@ -70,7 +70,7 @@ func BenchmarkOverlappingFieldsCanBeMerged(b *testing.B) {
 			definition := unsafeparser.ParseGraphqlDocumentBytes(definitionBytes)
 			operation := unsafeparser.ParseGraphqlDocumentBytes(operationBytes)
 			report := operationreport.Report{}
-			normalizer := astnormalization.NewNormalizer(false,false)
+			normalizer := astnormalization.NewNormalizer(false, false)
 			validator := DefaultOperationValidator()
 
 			b.ReportAllocs()
@@ -96,7 +96,7 @@ func BenchmarkOverlappingFieldsCanBeMerged(b *testing.B) {
 			definition := unsafeparser.ParseGraphqlDocumentBytes(definitionBytes)
 			operation := unsafeparser.ParseGraphqlDocumentBytes(operationBytes)
 			report := operationreport.Report{}
-			normalizer := astnormalization.NewNormalizer(false,false)
+			normalizer := astnormalization.NewNormalizer(false, false)
 			validator := DefaultOperationValidator()
 
 			b.ReportAllocs()
diff --git a/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go b/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go
index fb8b6c88..962ce4c4 100644
--- a/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go
+++ b/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go
@@ -1269,14 +1269,14 @@ func TestGraphQLDataSource(t *testing.T) {
 					ChildNodes: []plan.TypeField{
 						{
 							TypeName:   "Review",
-							FieldNames: []string{"body", "author","product"},
+							FieldNames: []string{"body", "author", "product"},
 						},
 						{
 							TypeName:   "User",
-							FieldNames: []string{"id","username"},
+							FieldNames: []string{"id", "username"},
 						},
 						{
-							TypeName: "Product",
+							TypeName:   "Product",
 							FieldNames: []string{"upc"},
 						},
 					},
@@ -1304,23 +1304,23 @@ func TestGraphQLDataSource(t *testing.T) {
 					},
 				},
 				{
-					TypeName: "User",
-					FieldName: "reviews",
+					TypeName:       "User",
+					FieldName:      "reviews",
 					RequiresFields: []string{"id"},
 				},
 				{
-					TypeName: "Product",
-					FieldName: "name",
+					TypeName:       "Product",
+					FieldName:      "name",
 					RequiresFields: []string{"upc"},
 				},
 				{
-					TypeName: "Product",
-					FieldName: "price",
+					TypeName:       "Product",
+					FieldName:      "price",
 					RequiresFields: []string{"upc"},
 				},
 				{
-					TypeName: "Product",
-					FieldName: "reviews",
+					TypeName:       "Product",
+					FieldName:      "reviews",
 					RequiresFields: []string{"upc"},
 				},
 			},
diff --git a/pkg/engine/datasource/httpclient/httpclient_test.go b/pkg/engine/datasource/httpclient/httpclient_test.go
index 9c61e323..4a4e8463 100644
--- a/pkg/engine/datasource/httpclient/httpclient_test.go
+++ b/pkg/engine/datasource/httpclient/httpclient_test.go
@@ -62,17 +62,17 @@ func TestHttpClient(t *testing.T) {
 	in = SetInputBodyWithPath(nil, []byte(`{`), "query")
 	assert.Equal(t, `{"body":{"query":"{"}}`, string(in))
 
-	in = SetInputBodyWithPath(nil,[]byte(`{topProducts {upc name price}}}`),"query")
-	assert.Equal(t,`{"body":{"query":"{topProducts {upc name price}}}"}}`,string(in))
+	in = SetInputBodyWithPath(nil, []byte(`{topProducts {upc name price}}}`), "query")
+	assert.Equal(t, `{"body":{"query":"{topProducts {upc name price}}}"}}`, string(in))
 
-	in = SetInputBodyWithPath(nil,[]byte(`$$0$$`),"variables.foo")
-	assert.Equal(t,`{"body":{"variables":{"foo":$$0$$}}}`,string(in))
+	in = SetInputBodyWithPath(nil, []byte(`$$0$$`), "variables.foo")
+	assert.Equal(t, `{"body":{"variables":{"foo":$$0$$}}}`, string(in))
 
-	in = SetInputBodyWithPath(nil,[]byte(`"$$0$$"`),"variables.foo")
-	assert.Equal(t,`{"body":{"variables":{"foo":"$$0$$"}}}`,string(in))
+	in = SetInputBodyWithPath(nil, []byte(`"$$0$$"`), "variables.foo")
+	assert.Equal(t, `{"body":{"variables":{"foo":"$$0$$"}}}`, string(in))
 
-	in = SetInputBodyWithPath(nil,[]byte(`{"bar":$$0$$}`),"variables.foo")
-	assert.Equal(t,`{"body":{"variables":{"foo":{"bar":$$0$$}}}}`,string(in))
+	in = SetInputBodyWithPath(nil, []byte(`{"bar":$$0$$}`), "variables.foo")
+	assert.Equal(t, `{"body":{"variables":{"foo":{"bar":$$0$$}}}}`, string(in))
 }
 
 func TestHttpClientDo(t *testing.T) {
@@ -131,8 +131,8 @@ func TestHttpClientDo(t *testing.T) {
 			assert.Equal(t, fooValues[1], "baz")
 
 			yearValues := r.URL.Query()["year"]
-			assert.Len(t,yearValues,1)
-			assert.Equal(t,yearValues[0],"2020")
+			assert.Len(t, yearValues, 1)
+			assert.Equal(t, yearValues[0], "2020")
 
 			_, err := w.Write([]byte("ok"))
 			assert.NoError(t, err)
diff --git a/pkg/engine/datasource/rest_datasource/rest_datasource_test.go b/pkg/engine/datasource/rest_datasource/rest_datasource_test.go
index e406310e..6d888e65 100644
--- a/pkg/engine/datasource/rest_datasource/rest_datasource_test.go
+++ b/pkg/engine/datasource/rest_datasource/rest_datasource_test.go
@@ -91,8 +91,8 @@ func TestFastHttpJsonDataSourcePlanning(t *testing.T) {
 			Response: &resolve.GraphQLResponse{
 				Data: &resolve.Object{
 					Fetch: &resolve.SingleFetch{
-						BufferId: 0,
-						Input:    `{"method":"GET","url":"https://example.com/friend"}`,
+						BufferId:   0,
+						Input:      `{"method":"GET","url":"https://example.com/friend"}`,
 						DataSource: &Source{},
 					},
 					Fields: []*resolve.Field{
@@ -103,8 +103,8 @@ func TestFastHttpJsonDataSourcePlanning(t *testing.T) {
 							Value: &resolve.Object{
 								Nullable: true,
 								Fetch: &resolve.SingleFetch{
-									BufferId: 1,
-									Input:    `{"method":"GET","url":"https://example.com/friend/$$0$$/pet"}`,
+									BufferId:   1,
+									Input:      `{"method":"GET","url":"https://example.com/friend/$$0$$/pet"}`,
 									DataSource: &Source{},
 									Variables: resolve.NewVariables(
 										&resolve.ObjectVariable{
@@ -203,8 +203,8 @@ func TestFastHttpJsonDataSourcePlanning(t *testing.T) {
 			Response: &resolve.GraphQLResponse{
 				Data: &resolve.Object{
 					Fetch: &resolve.SingleFetch{
-						BufferId: 0,
-						Input:    `{"method":"GET","url":"https://example.com/$$0$$/$$1$$"}`,
+						BufferId:   0,
+						Input:      `{"method":"GET","url":"https://example.com/$$0$$/$$1$$"}`,
 						DataSource: &Source{},
 						Variables: resolve.NewVariables(
 							&resolve.ContextVariable{
@@ -338,9 +338,9 @@ func TestFastHttpJsonDataSourcePlanning(t *testing.T) {
 			Response: &resolve.GraphQLResponse{
 				Data: &resolve.Object{
 					Fetch: &resolve.SingleFetch{
-						BufferId: 0,
-						Input:    `{"body":{"foo":"bar"},"method":"POST","url":"https://example.com/friend"}`,
-						DataSource: &Source{},
+						BufferId:             0,
+						Input:                `{"body":{"foo":"bar"},"method":"POST","url":"https://example.com/friend"}`,
+						DataSource:           &Source{},
 						DisallowSingleFlight: true,
 					},
 					Fields: []*resolve.Field{
@@ -398,8 +398,8 @@ func TestFastHttpJsonDataSourcePlanning(t *testing.T) {
 			Response: &resolve.GraphQLResponse{
 				Data: &resolve.Object{
 					Fetch: &resolve.SingleFetch{
-						BufferId: 0,
-						Input:    `{"headers":{"Authorization":["Bearer 123"],"X-API-Key":["456"]},"method":"GET","url":"https://example.com/friend"}`,
+						BufferId:   0,
+						Input:      `{"headers":{"Authorization":["Bearer 123"],"X-API-Key":["456"]},"method":"GET","url":"https://example.com/friend"}`,
 						DataSource: &Source{},
 					},
 					Fields: []*resolve.Field{
@@ -460,8 +460,8 @@ func TestFastHttpJsonDataSourcePlanning(t *testing.T) {
 			Response: &resolve.GraphQLResponse{
 				Data: &resolve.Object{
 					Fetch: &resolve.SingleFetch{
-						BufferId: 0,
-						Input:    `{"query_params":[{"name":"static","value":"staticValue"},{"name":"static","value":"secondStaticValue"},{"name":"name","value":"$$0$$"},{"name":"id","value":"$$1$$"}],"method":"GET","url":"https://example.com/friend"}`,
+						BufferId:   0,
+						Input:      `{"query_params":[{"name":"static","value":"staticValue"},{"name":"static","value":"secondStaticValue"},{"name":"name","value":"$$0$$"},{"name":"id","value":"$$1$$"}],"method":"GET","url":"https://example.com/friend"}`,
 						DataSource: &Source{},
 						Variables: resolve.NewVariables(
 							&resolve.ContextVariable{
@@ -548,8 +548,8 @@ func TestFastHttpJsonDataSourcePlanning(t *testing.T) {
 			Response: &resolve.GraphQLResponse{
 				Data: &resolve.Object{
 					Fetch: &resolve.SingleFetch{
-						BufferId: 0,
-						Input:    `{"query_params":[{"name":"names","value":"$$0$$"}],"method":"GET","url":"https://example.com/friend"}`,
+						BufferId:   0,
+						Input:      `{"query_params":[{"name":"names","value":"$$0$$"}],"method":"GET","url":"https://example.com/friend"}`,
 						DataSource: &Source{},
 						Variables: resolve.NewVariables(
 							&resolve.ContextVariable{
diff --git a/pkg/engine/datasourcetesting/datasourcetesting.go b/pkg/engine/datasourcetesting/datasourcetesting.go
index 388e6d8d..b1f4e7d2 100644
--- a/pkg/engine/datasourcetesting/datasourcetesting.go
+++ b/pkg/engine/datasourcetesting/datasourcetesting.go
@@ -10,7 +10,7 @@ import (
 	"github.com/jensneuse/graphql-go-tools/pkg/astprinter"
 	"github.com/jensneuse/graphql-go-tools/pkg/asttransform"
 	"github.com/jensneuse/graphql-go-tools/pkg/astvalidation"
-	plan "github.com/jensneuse/graphql-go-tools/pkg/engine/plan"
+	"github.com/jensneuse/graphql-go-tools/pkg/engine/plan"
 	"github.com/jensneuse/graphql-go-tools/pkg/operationreport"
 )
 
diff --git a/pkg/engine/plan/plan_test.go b/pkg/engine/plan/plan_test.go
index 2a1674a1..c12c03f8 100644
--- a/pkg/engine/plan/plan_test.go
+++ b/pkg/engine/plan/plan_test.go
@@ -14,21 +14,25 @@ import (
 )
 
 func TestPlanner_Plan(t *testing.T) {
+	testLogic := func(definition, operation, operationName string, config Configuration, report *operationreport.Report) Plan {
+		def := unsafeparser.ParseGraphqlDocumentString(definition)
+		op := unsafeparser.ParseGraphqlDocumentString(operation)
+		err := asttransform.MergeDefinitionWithBaseSchema(&def)
+		if err != nil {
+			t.Fatal(err)
+		}
+		norm := astnormalization.NewNormalizer(true, true)
+		norm.NormalizeOperation(&op, &def, report)
+		valid := astvalidation.DefaultOperationValidator()
+		valid.Validate(&op, &def, report)
+		p := NewPlanner(config)
+		return p.Plan(&op, &def, operationName, report)
+	}
+
 	test := func(definition, operation, operationName string, expectedPlan Plan, config Configuration) func(t *testing.T) {
 		return func(t *testing.T) {
-			def := unsafeparser.ParseGraphqlDocumentString(definition)
-			op := unsafeparser.ParseGraphqlDocumentString(operation)
-			err := asttransform.MergeDefinitionWithBaseSchema(&def)
-			if err != nil {
-				t.Fatal(err)
-			}
-			norm := astnormalization.NewNormalizer(true, true)
 			var report operationreport.Report
-			norm.NormalizeOperation(&op, &def, &report)
-			valid := astvalidation.DefaultOperationValidator()
-			valid.Validate(&op, &def, &report)
-			p := NewPlanner(config)
-			plan := p.Plan(&op, &def, operationName, &report)
+			plan := testLogic(definition, operation, operationName, config, &report)
 			if report.HasErrors() {
 				t.Fatal(report.Error())
 			}
@@ -36,6 +40,14 @@ func TestPlanner_Plan(t *testing.T) {
 		}
 	}
 
+	testWithError := func(definition, operation, operationName string, config Configuration) func(t *testing.T) {
+		return func(t *testing.T) {
+			var report operationreport.Report
+			_ = testLogic(definition, operation, operationName, config, &report)
+			assert.True(t, report.HasErrors())
+		}
+	}
+
 	t.Run("stream & defer Query", test(testDefinition, `
 		query MyQuery($id: ID!) @flushInterval(milliSeconds: 100) {
 			droid(id: $id){
@@ -139,6 +151,100 @@ func TestPlanner_Plan(t *testing.T) {
 	}, Configuration{
 		DefaultFlushInterval: 0,
 	}))
+
+	t.Run("operation selection", func(t *testing.T) {
+		t.Run("should successfully plan a single named query by providing an operation name", test(testDefinition, `
+				query MyHero {
+					hero{
+						name
+					}
+				}
+			`, "MyHero", expectedMyHeroPlan, Configuration{},
+		))
+
+		t.Run("should successfully plan multiple named queries by providing an operation name", test(testDefinition, `
+				query MyDroid($id: ID!) {
+					droid(id: $id){
+						name
+					}
+				}
+		
+				query MyHero {
+					hero{
+						name
+					}
+				}
+			`, "MyHero", expectedMyHeroPlan, Configuration{},
+		))
+
+		t.Run("should successfully plan a single named query without providing an operation name", test(testDefinition, `
+				query MyHero {
+					hero{
+						name
+					}
+				}
+			`, "", expectedMyHeroPlan, Configuration{},
+		))
+
+		t.Run("should successfully plan a single unnamed query without providing an operation name", test(testDefinition, `
+				{
+					hero{
+						name
+					}
+				}
+			`, "", expectedMyHeroPlan, Configuration{},
+		))
+
+		t.Run("should write into error report when no query with name was found", testWithError(testDefinition, `
+				query MyHero {
+					hero{
+						name
+					}
+				}
+			`, "NoHero", Configuration{},
+		))
+
+		t.Run("should write into error report when no operation name was provided on multiple named queries", testWithError(testDefinition, `
+				query MyDroid($id: ID!) {
+					droid(id: $id){
+						name
+					}
+				}
+		
+				query MyHero {
+					hero{
+						name
+					}
+				}
+			`, "", Configuration{},
+		))
+	})
+
+}
+
+var expectedMyHeroPlan = &SynchronousResponsePlan{
+	FlushInterval: 0,
+	Response: &resolve.GraphQLResponse{
+		Data: &resolve.Object{
+			Fields: []*resolve.Field{
+				{
+					Name: []byte("hero"),
+					Value: &resolve.Object{
+						Path:     []string{"hero"},
+						Nullable: true,
+						Fields: []*resolve.Field{
+							{
+								Name: []byte("name"),
+								Value: &resolve.String{
+									Path: []string{"name"},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	},
 }
 
 const testDefinition = `
diff --git a/pkg/engine/resolve/resolve_mock_test.go b/pkg/engine/resolve/resolve_mock_test.go
index 193133b2..2c760ff6 100644
--- a/pkg/engine/resolve/resolve_mock_test.go
+++ b/pkg/engine/resolve/resolve_mock_test.go
@@ -6,8 +6,9 @@ package resolve
 
 import (
 	context "context"
-	gomock "github.com/golang/mock/gomock"
 	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
 )
 
 // MockDataSource is a mock of DataSource interface
diff --git a/pkg/engine/resolve/resolve_test.go b/pkg/engine/resolve/resolve_test.go
index a1a3a57a..cd546ca3 100644
--- a/pkg/engine/resolve/resolve_test.go
+++ b/pkg/engine/resolve/resolve_test.go
@@ -1003,7 +1003,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) {
 					{
 						BufferID:  0,
 						HasBuffer: true,
-						Name: []byte("anotherServiceOne"),
+						Name:      []byte("anotherServiceOne"),
 						Value: &Object{
 							Path: []string{"anotherServiceOne"},
 							Fields: []*Field{
@@ -1019,7 +1019,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) {
 					{
 						BufferID:  1,
 						HasBuffer: true,
-						Name: []byte("secondServiceTwo"),
+						Name:      []byte("secondServiceTwo"),
 						Value: &Object{
 							Path: []string{"secondServiceTwo"},
 							Fields: []*Field{
@@ -1035,7 +1035,7 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) {
 					{
 						BufferID:  0,
 						HasBuffer: true,
-						Name: []byte("reusingServiceOne"),
+						Name:      []byte("reusingServiceOne"),
 						Value: &Object{
 							Path: []string{"reusingServiceOne"},
 							Fields: []*Field{
diff --git a/pkg/engine/subscription/graphql-websocket-subscription/graphql_websocket_subscription_test.go b/pkg/engine/subscription/graphql-websocket-subscription/graphql_websocket_subscription_test.go
index df5ba718..aff0133a 100644
--- a/pkg/engine/subscription/graphql-websocket-subscription/graphql_websocket_subscription_test.go
+++ b/pkg/engine/subscription/graphql-websocket-subscription/graphql_websocket_subscription_test.go
@@ -13,14 +13,11 @@ import (
 )
 
 func TestGraphQLWebsocketSubscriptionStream(t *testing.T) {
-
 	server := FakeGraphQLSubscriptionServer(t)
 	defer server.Close()
 
 	host := server.Listener.Addr().String()
 
-	//host = "localhost:4444"
-
 	stream := New()
 	ctx, cancel := context.WithCancel(context.Background())
 	manager := subscription.NewManager(stream)
diff --git a/pkg/engine/subscription/graphql-websocket-subscription/websocket_client_test.go b/pkg/engine/subscription/graphql-websocket-subscription/websocket_client_test.go
index 2baa1ef0..9630c38e 100644
--- a/pkg/engine/subscription/graphql-websocket-subscription/websocket_client_test.go
+++ b/pkg/engine/subscription/graphql-websocket-subscription/websocket_client_test.go
@@ -17,14 +17,13 @@ import (
 )
 
 func TestWebsocketClient(t *testing.T) {
-
 	server := FakeGraphQLSubscriptionServer(t)
 	defer server.Close()
 
- 	host := server.Listener.Addr().String()
+	host := server.Listener.Addr().String()
 
 	client := &WebsocketClient{}
-	//err := client.Open("ws", "localhost:4444", "/", nil)
+
 	err := client.Open("ws", host, "", nil)
 	defer client.Close()
 	assert.NoError(t, err)
@@ -32,37 +31,33 @@ func TestWebsocketClient(t *testing.T) {
 	totalMessages := &atomic.Int64{}
 
 	subscribe := func(wg *sync.WaitGroup, receiveMessages int) {
-		wg.Add(1)
-		go func(wg *sync.WaitGroup, receiveMessages int) {
-			subscription, ok := client.Subscribe([]byte(`{"query":"subscription{counter{count}}"}`))
-			assert.True(t, ok)
-
-			template := `{"data":{"counter":{"count":%d}}}`
+		subscription, ok := client.Subscribe([]byte(`{"query":"subscription{counter{count}}"}`))
+		assert.True(t, ok)
 
-			for i := 0; i < receiveMessages; i++ {
-				expected := fmt.Sprintf(template, i)
-				data, ok := subscription.Next(nil)
-				assert.True(t, ok)
-				assert.Equal(t, expected, string(data))
+		template := `{"data":{"counter":{"count":%d}}}`
 
-				totalMessages.Inc()
-			}
+		for i := 0; i < receiveMessages; i++ {
+			expected := fmt.Sprintf(template, i)
+			data, ok := subscription.Next(nil)
+			assert.True(t, ok)
+			assert.Equal(t, expected, string(data))
 
-			client.Unsubscribe(subscription)
+			totalMessages.Inc()
+		}
 
-			wg.Done()
+		client.Unsubscribe(subscription)
 
-		}(wg, receiveMessages)
+		wg.Done()
 	}
 
 	wg := &sync.WaitGroup{}
+	wg.Add(3)
 
-	subscribe(wg, 1)
-	subscribe(wg, 2)
-	subscribe(wg, 3)
+	go subscribe(wg, 1)
+	go subscribe(wg, 2)
+	go subscribe(wg, 3)
 
 	wg.Wait()
-
 	assert.Equal(t, 0, len(client.subscriptions))
 	assert.Equal(t, int64(6), totalMessages.Load())
 }
@@ -86,19 +81,19 @@ func FakeGraphQLSubscriptionServer(t *testing.T) *httptest.Server {
 		streams := map[string]func(){}
 		writeMux := &sync.Mutex{}
 
-		startStream := func(id string,done <- chan struct{}){
+		startStream := func(id string, done <-chan struct{}) {
 			counter := 0
 			for {
 				time.Sleep(time.Millisecond)
 				select {
-					case <- done:
-						message := fmt.Sprintf(`{"type":"complete","id":"%s","payload":null}`,id)
-						writeMux.Lock()
-						err = c.WriteMessage(websocket.TextMessage, []byte(message))
-						writeMux.Unlock()
-						return
+				case <-done:
+					message := fmt.Sprintf(`{"type":"complete","id":"%s","payload":null}`, id)
+					writeMux.Lock()
+					err = c.WriteMessage(websocket.TextMessage, []byte(message))
+					writeMux.Unlock()
+					return
 				default:
-					message := fmt.Sprintf(`{"type":"data","id":"%s","payload":{"data":{"counter":{"count":%d}}}}`,id,counter)
+					message := fmt.Sprintf(`{"type":"data","id":"%s","payload":{"data":{"counter":{"count":%d}}}}`, id, counter)
 					writeMux.Lock()
 					err = c.WriteMessage(websocket.TextMessage, []byte(message))
 					writeMux.Unlock()
@@ -117,19 +112,21 @@ func FakeGraphQLSubscriptionServer(t *testing.T) *httptest.Server {
 			}
 			assert.Equal(t, websocket.TextMessage, mt)
 
-			messageType,err := jsonparser.GetString(message,"type")
-			assert.NoError(t,err)
-			messageID,err := jsonparser.GetString(message,"id")
-			assert.NoError(t,err)
+			messageType, err := jsonparser.GetString(message, "type")
+			assert.NoError(t, err)
+
+			messageID, err := jsonparser.GetString(message, "id")
+			assert.NoError(t, err)
+
 			switch messageType {
 			case "start":
-				ctx,cancel := context.WithCancel(context.Background())
+				ctx, cancel := context.WithCancel(context.Background())
 				streams[messageID] = cancel
-				go startStream(messageID,ctx.Done())
+				go startStream(messageID, ctx.Done())
 			case "stop":
 				cancel := streams[messageID]
 				cancel()
-				delete(streams,messageID)
+				delete(streams, messageID)
 			}
 		}
 	}))
diff --git a/pkg/engine/subscription/http_polling/http_polling_test.go b/pkg/engine/subscription/http_polling/http_polling_test.go
index 8b50eccb..00b3b89d 100644
--- a/pkg/engine/subscription/http_polling/http_polling_test.go
+++ b/pkg/engine/subscription/http_polling/http_polling_test.go
@@ -26,7 +26,7 @@ func TestHttpPolling(t *testing.T) {
 	httpPollingStream := New(httpclient.NewNetHttpClient(httpclient.DefaultNetHttpClient))
 
 	manager := subscription.NewManager(httpPollingStream)
-	ctx,cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(context.Background())
 	defer cancel()
 	manager.Run(ctx.Done())
 
diff --git a/pkg/engine/subscription/manager_test.go b/pkg/engine/subscription/manager_test.go
index e2d80c0f..000bf4c9 100644
--- a/pkg/engine/subscription/manager_test.go
+++ b/pkg/engine/subscription/manager_test.go
@@ -12,7 +12,7 @@ import (
 
 type FakeStream struct {
 	done bool
-	wg *sync.WaitGroup
+	wg   *sync.WaitGroup
 }
 
 func (f *FakeStream) Start(input []byte, next chan<- []byte, stop <-chan struct{}) {
@@ -40,7 +40,7 @@ func TestSubscriptionManager(t *testing.T) {
 	}
 	fakeStream.wg.Add(1)
 	manager := NewManager(fakeStream)
-	ctx,cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(context.Background())
 	defer cancel()
 	manager.Run(ctx.Done())
 
diff --git a/pkg/execution/datasource_graphql_test.go b/pkg/execution/datasource_graphql_test.go
index ddc3cc9b..c8944c00 100644
--- a/pkg/execution/datasource_graphql_test.go
+++ b/pkg/execution/datasource_graphql_test.go
@@ -19,6 +19,18 @@ import (
 	"github.com/jensneuse/graphql-go-tools/pkg/operationreport"
 )
 
+type preSendHttpHookFunc func(ctx datasource.HookContext, req *http.Request)
+
+func (p preSendHttpHookFunc) Execute(ctx datasource.HookContext, req *http.Request) {
+	p(ctx, req)
+}
+
+type postReceiveHttpHookFunc func(ctx datasource.HookContext, resp *http.Response, body []byte)
+
+func (p postReceiveHttpHookFunc) Execute(ctx datasource.HookContext, resp *http.Response, body []byte) {
+	p(ctx, resp, body)
+}
+
 var graphqlDataSourceName = "graphql"
 
 func TestGraphqlDataSource_WithPlanning(t *testing.T) {
@@ -26,6 +38,7 @@ func TestGraphqlDataSource_WithPlanning(t *testing.T) {
 		definition            string
 		operation             datasource.GraphqlRequest
 		typeFieldConfigs      []datasource.TypeFieldConfiguration
+		hooksFactory          func(t *testing.T) datasource.Hooks
 		assertRequestBody     bool
 		expectedRequestBodies []string
 		upstreamResponses     []string
@@ -60,7 +73,12 @@ func TestGraphqlDataSource_WithPlanning(t *testing.T) {
 			basePlanner, err := datasource.NewBaseDataSourcePlanner([]byte(tc.definition), plannerConfig, abstractlogger.NoopLogger)
 			require.NoError(t, err)
 
-			err = basePlanner.RegisterDataSourcePlannerFactory(graphqlDataSourceName, &datasource.GraphQLDataSourcePlannerFactoryFactory{})
+			var hooks datasource.Hooks
+			if tc.hooksFactory != nil {
+				hooks = tc.hooksFactory(t)
+			}
+
+			err = basePlanner.RegisterDataSourcePlannerFactory(graphqlDataSourceName, &datasource.GraphQLDataSourcePlannerFactoryFactory{Hooks: hooks})
 			require.NoError(t, err)
 
 			definitionDocument := unsafeparser.ParseGraphqlDocumentString(tc.definition)
@@ -103,7 +121,7 @@ func TestGraphqlDataSource_WithPlanning(t *testing.T) {
 				Query:         "{ continents { code name } }",
 			},
 			typeFieldConfigs: []datasource.TypeFieldConfiguration{
-				typeFieldConfigContinents,
+				graphqlTypeFieldConfigContinents,
 			},
 			assertRequestBody: false,
 			upstreamResponses: []string{
@@ -122,7 +140,41 @@ func TestGraphqlDataSource_WithPlanning(t *testing.T) {
 				Query:         `{ country(code: "DE") { code name } }`,
 			},
 			typeFieldConfigs: []datasource.TypeFieldConfiguration{
-				typeFieldConfigCountry,
+				graphqlTypeFieldConfigCountry,
+			},
+			assertRequestBody: false,
+			upstreamResponses: []string{
+				`{ "data": { "country": { "code": "DE", "name": "Germany" } } }`,
+			},
+			expectedResponseBody: `{ "data": { "country": { "code": "DE", "name": "Germany" } } }`,
+		}),
+	)
+
+	t.Run("should execute hooks", run(
+		testCase{
+			definition: countriesSchema,
+			operation: datasource.GraphqlRequest{
+				OperationName: "",
+				Variables:     nil,
+				Query:         `{ country(code: "DE") { code name } }`,
+			},
+			typeFieldConfigs: []datasource.TypeFieldConfiguration{
+				graphqlTypeFieldConfigCountry,
+			},
+			hooksFactory: func(t *testing.T) datasource.Hooks {
+				return datasource.Hooks{
+					PreSendHttpHook: preSendHttpHookFunc(func(ctx datasource.HookContext, req *http.Request) {
+						assert.Equal(t, ctx.TypeName, "Query")
+						assert.Equal(t, ctx.FieldName, "country")
+						assert.Regexp(t, `http://127.0.0.1:[0-9]+`, req.URL.String())
+					}),
+					PostReceiveHttpHook: postReceiveHttpHookFunc(func(ctx datasource.HookContext, resp *http.Response, body []byte) {
+						assert.Equal(t, ctx.TypeName, "Query")
+						assert.Equal(t, ctx.FieldName, "country")
+						assert.Equal(t, 200, resp.StatusCode)
+						assert.Equal(t, body, []byte(`{ "data": { "country": { "code": "DE", "name": "Germany" } } }`))
+					}),
+				}
 			},
 			assertRequestBody: false,
 			upstreamResponses: []string{
@@ -141,8 +193,8 @@ func TestGraphqlDataSource_WithPlanning(t *testing.T) {
 				Query:         `{ continents { code name } country(code: "DE") { code name } }`,
 			},
 			typeFieldConfigs: []datasource.TypeFieldConfiguration{
-				typeFieldConfigCountry,
-				typeFieldConfigContinents,
+				graphqlTypeFieldConfigCountry,
+				graphqlTypeFieldConfigContinents,
 			},
 			assertRequestBody: true,
 			expectedRequestBodies: []string{
@@ -166,8 +218,8 @@ func TestGraphqlDataSource_WithPlanning(t *testing.T) {
 				Query:         `{ country(code: "DE") { code name } continent(code: "EU") { code name } }`,
 			},
 			typeFieldConfigs: []datasource.TypeFieldConfiguration{
-				typeFieldConfigCountry,
-				typeFieldConfigContinent,
+				graphqlTypeFieldConfigCountry,
+				graphqlTypeFieldConfigContinent,
 			},
 			assertRequestBody: true,
 			expectedRequestBodies: []string{
@@ -218,7 +270,7 @@ func createPlannerConfigToUpstream(t *testing.T, upstreamURL []string, method st
 	}
 }
 
-var typeFieldConfigContinents = datasource.TypeFieldConfiguration{
+var graphqlTypeFieldConfigContinents = datasource.TypeFieldConfiguration{
 	TypeName:  "Query",
 	FieldName: "continents",
 	Mapping: &datasource.MappingConfiguration{
@@ -230,7 +282,7 @@ var typeFieldConfigContinents = datasource.TypeFieldConfiguration{
 	},
 }
 
-var typeFieldConfigContinent = datasource.TypeFieldConfiguration{
+var graphqlTypeFieldConfigContinent = datasource.TypeFieldConfiguration{
 	TypeName:  "Query",
 	FieldName: "continent",
 	Mapping: &datasource.MappingConfiguration{
@@ -242,7 +294,7 @@ var typeFieldConfigContinent = datasource.TypeFieldConfiguration{
 	},
 }
 
-var typeFieldConfigCountry = datasource.TypeFieldConfiguration{
+var graphqlTypeFieldConfigCountry = datasource.TypeFieldConfiguration{
 	TypeName:  "Query",
 	FieldName: "country",
 	Mapping: &datasource.MappingConfiguration{
diff --git a/pkg/execution/datasource_http_json_test.go b/pkg/execution/datasource_http_json_test.go
index 1070bce4..d3f6c1a2 100644
--- a/pkg/execution/datasource_http_json_test.go
+++ b/pkg/execution/datasource_http_json_test.go
@@ -5,16 +5,22 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
 	"testing"
 
 	"github.com/jensneuse/abstractlogger"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	"github.com/tidwall/gjson"
 
+	"github.com/jensneuse/graphql-go-tools/internal/pkg/unsafeparser"
 	"github.com/jensneuse/graphql-go-tools/pkg/ast"
+	"github.com/jensneuse/graphql-go-tools/pkg/astnormalization"
 	"github.com/jensneuse/graphql-go-tools/pkg/execution/datasource"
 	"github.com/jensneuse/graphql-go-tools/pkg/lexer/literal"
+	"github.com/jensneuse/graphql-go-tools/pkg/operationreport"
 )
 
 const httpJsonDataSourceSchema = `
@@ -105,6 +111,14 @@ func TestHttpJsonDataSourcePlanner_Plan(t *testing.T) {
 									Client: datasource.DefaultHttpClient(),
 								},
 								Args: []datasource.Argument{
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_type_name"),
+										Value: []byte("Query"),
+									},
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_field_name"),
+										Value: []byte("simpleType"),
+									},
 									&datasource.StaticVariableArgument{
 										Name:  []byte("url"),
 										Value: []byte("example.com/"),
@@ -203,6 +217,14 @@ func TestHttpJsonDataSourcePlanner_Plan(t *testing.T) {
 									Client: datasource.DefaultHttpClient(),
 								},
 								Args: []datasource.Argument{
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_type_name"),
+										Value: []byte("Query"),
+									},
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_field_name"),
+										Value: []byte("listOfStrings"),
+									},
 									&datasource.StaticVariableArgument{
 										Name:  []byte("url"),
 										Value: []byte("example.com/"),
@@ -282,6 +304,14 @@ func TestHttpJsonDataSourcePlanner_Plan(t *testing.T) {
 									Client: datasource.DefaultHttpClient(),
 								},
 								Args: []datasource.Argument{
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_type_name"),
+										Value: []byte("Query"),
+									},
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_field_name"),
+										Value: []byte("listOfObjects"),
+									},
 									&datasource.StaticVariableArgument{
 										Name:  []byte("url"),
 										Value: []byte("example.com/"),
@@ -383,6 +413,14 @@ func TestHttpJsonDataSourcePlanner_Plan(t *testing.T) {
 									Client: datasource.DefaultHttpClient(),
 								},
 								Args: []datasource.Argument{
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_type_name"),
+										Value: []byte("Query"),
+									},
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_field_name"),
+										Value: []byte("unionType"),
+									},
 									&datasource.StaticVariableArgument{
 										Name:  []byte("url"),
 										Value: []byte("example.com/"),
@@ -525,6 +563,14 @@ func TestHttpJsonDataSourcePlanner_Plan(t *testing.T) {
 									Client: datasource.DefaultHttpClient(),
 								},
 								Args: []datasource.Argument{
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_type_name"),
+										Value: []byte("Query"),
+									},
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_field_name"),
+										Value: []byte("interfaceType"),
+									},
 									&datasource.StaticVariableArgument{
 										Name:  []byte("url"),
 										Value: []byte("example.com/"),
@@ -677,3 +723,151 @@ func TestHttpJsonDataSource_Resolve(t *testing.T) {
 		`{"500":"ErrorInterface","200":"AnotherSuccess","defaultTypeName":"SuccessInterface"}`,
 		"AnotherSuccess"))
 }
+
+var httpJsonDataSourceName = "http_json"
+
+func TestHttpJsonDataSource_WithPlanning(t *testing.T) {
+	type testCase struct {
+		definition            string
+		operation             datasource.GraphqlRequest
+		typeFieldConfigs      []datasource.TypeFieldConfiguration
+		hooksFactory          func(t *testing.T) datasource.Hooks
+		assertRequestBody     bool
+		expectedRequestBodies []string
+		upstreamResponses     []string
+		expectedResponseBody  string
+	}
+
+	run := func(tc testCase) func(t *testing.T) {
+		return func(t *testing.T) {
+			upstreams := make([]*httptest.Server, len(tc.upstreamResponses))
+			for i := 0; i < len(tc.upstreamResponses); i++ {
+				if tc.assertRequestBody {
+					require.Len(t, tc.expectedRequestBodies, len(tc.upstreamResponses))
+				}
+
+				var expectedRequestBody string
+				if tc.assertRequestBody {
+					expectedRequestBody = tc.expectedRequestBodies[i]
+				}
+
+				upstream := upstreamHttpJsonServer(t, tc.assertRequestBody, expectedRequestBody, tc.upstreamResponses[i])
+				defer upstream.Close()
+
+				upstreams[i] = upstream
+			}
+
+			var upstreamURLs []string
+			for _, upstream := range upstreams {
+				upstreamURLs = append(upstreamURLs, upstream.URL)
+			}
+
+			plannerConfig := createPlannerConfigToUpstream(t, upstreamURLs, http.MethodPost, tc.typeFieldConfigs)
+			basePlanner, err := datasource.NewBaseDataSourcePlanner([]byte(tc.definition), plannerConfig, abstractlogger.NoopLogger)
+			require.NoError(t, err)
+
+			var hooks datasource.Hooks
+			if tc.hooksFactory != nil {
+				hooks = tc.hooksFactory(t)
+			}
+
+			err = basePlanner.RegisterDataSourcePlannerFactory(httpJsonDataSourceName, &datasource.HttpJsonDataSourcePlannerFactoryFactory{Hooks: hooks})
+			require.NoError(t, err)
+
+			definitionDocument := unsafeparser.ParseGraphqlDocumentString(tc.definition)
+			operationDocument := unsafeparser.ParseGraphqlDocumentString(tc.operation.Query)
+
+			var report operationreport.Report
+			operationDocument.Input.Variables = tc.operation.Variables
+			normalizer := astnormalization.NewNormalizer(true, true)
+			normalizer.NormalizeOperation(&operationDocument, &definitionDocument, &report)
+			require.False(t, report.HasErrors())
+
+			tc.operation.Variables = operationDocument.Input.Variables
+
+			planner := NewPlanner(basePlanner)
+			plan := planner.Plan(&operationDocument, &definitionDocument, tc.operation.OperationName, &report)
+			require.False(t, report.HasErrors())
+
+			variables, extraArguments := VariablesFromJson(tc.operation.Variables, nil)
+			executionContext := Context{
+				Context:        context.Background(),
+				Variables:      variables,
+				ExtraArguments: extraArguments,
+			}
+
+			var buf bytes.Buffer
+			executor := NewExecutor(nil)
+			err = executor.Execute(executionContext, plan, &buf)
+			require.NoError(t, err)
+
+			assert.JSONEq(t, tc.expectedResponseBody, buf.String())
+		}
+	}
+
+	t.Run("should execute hooks", run(
+		testCase{
+			definition: countriesSchema,
+			operation: datasource.GraphqlRequest{
+				OperationName: "",
+				Variables:     nil,
+				Query:         `{ country(code: "DE") { code name } }`,
+			},
+			typeFieldConfigs: []datasource.TypeFieldConfiguration{
+				httpJsonTypeFieldConfigCountry,
+			},
+			hooksFactory: func(t *testing.T) datasource.Hooks {
+				return datasource.Hooks{
+					PreSendHttpHook: preSendHttpHookFunc(func(ctx datasource.HookContext, req *http.Request) {
+						assert.Equal(t, ctx.TypeName, "Query")
+						assert.Equal(t, ctx.FieldName, "country")
+						assert.Regexp(t, `http://127.0.0.1:[0-9]+`, req.URL.String())
+					}),
+					PostReceiveHttpHook: postReceiveHttpHookFunc(func(ctx datasource.HookContext, resp *http.Response, body []byte) {
+						assert.Equal(t, ctx.TypeName, "Query")
+						assert.Equal(t, ctx.FieldName, "country")
+						assert.Equal(t, 200, resp.StatusCode)
+						assert.Equal(t, body, []byte(`{ "code": "DE", "name": "Germany" }`))
+					}),
+				}
+			},
+			assertRequestBody: false,
+			upstreamResponses: []string{
+				`{ "code": "DE", "name": "Germany" }`,
+			},
+			expectedResponseBody: `{ "data": { "country": { "code": "DE", "name": "Germany" } } }`,
+		}),
+	)
+}
+
+func upstreamHttpJsonServer(t *testing.T, assertRequestBody bool, expectedRequestBody string, response string) *httptest.Server {
+	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		require.NotNil(t, r.Body)
+
+		bodyBytes, err := ioutil.ReadAll(r.Body)
+		require.NoError(t, err)
+
+		if assertRequestBody {
+			isEqual := assert.JSONEq(t, expectedRequestBody, string(bodyBytes))
+			if !isEqual {
+				w.WriteHeader(http.StatusBadRequest)
+				return
+			}
+		}
+
+		_, err = w.Write([]byte(response))
+		require.NoError(t, err)
+	}))
+}
+
+var httpJsonTypeFieldConfigCountry = datasource.TypeFieldConfiguration{
+	TypeName:  "Query",
+	FieldName: "country",
+	Mapping: &datasource.MappingConfiguration{
+		Disabled: true,
+		Path:     "country",
+	},
+	DataSource: datasource.SourceConfig{
+		Name: httpJsonDataSourceName,
+	},
+}
diff --git a/pkg/execution/planning_test.go b/pkg/execution/planning_test.go
index 8e735091..ec85cd72 100644
--- a/pkg/execution/planning_test.go
+++ b/pkg/execution/planning_test.go
@@ -162,6 +162,14 @@ func TestPlanner_Plan(t *testing.T) {
 						Fetch: &SingleFetch{
 							Source: &DataSourceInvocation{
 								Args: []datasource.Argument{
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_type_name"),
+										Value: []byte("Query"),
+									},
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_field_name"),
+										Value: []byte("country"),
+									},
 									&datasource.StaticVariableArgument{
 										Name:  []byte("url"),
 										Value: []byte("countries.trevorblades.com/"),
@@ -275,6 +283,14 @@ func TestPlanner_Plan(t *testing.T) {
 						Fetch: &SingleFetch{
 							Source: &DataSourceInvocation{
 								Args: []datasource.Argument{
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_type_name"),
+										Value: []byte("Mutation"),
+									},
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_field_name"),
+										Value: []byte("likePost"),
+									},
 									&datasource.StaticVariableArgument{
 										Name:  []byte("url"),
 										Value: []byte("fakebook.com/"),
@@ -439,6 +455,14 @@ func TestPlanner_Plan(t *testing.T) {
 											Client: datasource.DefaultHttpClient(),
 										},
 										Args: []datasource.Argument{
+											&datasource.StaticVariableArgument{
+												Name:  []byte("root_type_name"),
+												Value: []byte("Query"),
+											},
+											&datasource.StaticVariableArgument{
+												Name:  []byte("root_field_name"),
+												Value: []byte("httpBinGet"),
+											},
 											&datasource.StaticVariableArgument{
 												Name:  []byte("url"),
 												Value: []byte("httpbin.org/get"),
@@ -458,6 +482,14 @@ func TestPlanner_Plan(t *testing.T) {
 								&SingleFetch{
 									Source: &DataSourceInvocation{
 										Args: []datasource.Argument{
+											&datasource.StaticVariableArgument{
+												Name:  []byte("root_type_name"),
+												Value: []byte("Query"),
+											},
+											&datasource.StaticVariableArgument{
+												Name:  []byte("root_field_name"),
+												Value: []byte("post"),
+											},
 											&datasource.StaticVariableArgument{
 												Name:  []byte("url"),
 												Value: []byte("jsonplaceholder.typicode.com/posts/{{ .arguments.id }}"),
@@ -545,6 +577,14 @@ func TestPlanner_Plan(t *testing.T) {
 									Fetch: &SingleFetch{
 										Source: &DataSourceInvocation{
 											Args: []datasource.Argument{
+												&datasource.StaticVariableArgument{
+													Name:  []byte("root_type_name"),
+													Value: []byte("JSONPlaceholderPost"),
+												},
+												&datasource.StaticVariableArgument{
+													Name:  []byte("root_field_name"),
+													Value: []byte("comments"),
+												},
 												&datasource.StaticVariableArgument{
 													Name:  []byte("url"),
 													Value: []byte("jsonplaceholder.typicode.com/comments?postId={{ .object.id }}"),
@@ -648,6 +688,14 @@ func TestPlanner_Plan(t *testing.T) {
 									Client: datasource.DefaultHttpClient(),
 								},
 								Args: []datasource.Argument{
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_type_name"),
+										Value: []byte("Query"),
+									},
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_field_name"),
+										Value: []byte("withBody"),
+									},
 									&datasource.StaticVariableArgument{
 										Name:  []byte("url"),
 										Value: []byte("httpbin.org/anything"),
@@ -719,6 +767,14 @@ func TestPlanner_Plan(t *testing.T) {
 									Client: datasource.DefaultHttpClient(),
 								},
 								Args: []datasource.Argument{
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_type_name"),
+										Value: []byte("Query"),
+									},
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_field_name"),
+										Value: []byte("withPath"),
+									},
 									&datasource.StaticVariableArgument{
 										Name:  []byte("url"),
 										Value: []byte("httpbin.org/anything"),
@@ -789,6 +845,14 @@ func TestPlanner_Plan(t *testing.T) {
 									Client: datasource.DefaultHttpClient(),
 								},
 								Args: []datasource.Argument{
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_type_name"),
+										Value: []byte("Query"),
+									},
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_field_name"),
+										Value: []byte("listItems"),
+									},
 									&datasource.StaticVariableArgument{
 										Name:  []byte("url"),
 										Value: []byte("httpbin.org/anything"),
@@ -872,6 +936,14 @@ func TestPlanner_Plan(t *testing.T) {
 									Client: datasource.DefaultHttpClient(),
 								},
 								Args: []datasource.Argument{
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_type_name"),
+										Value: []byte("Query"),
+									},
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_field_name"),
+										Value: []byte("listWithPath"),
+									},
 									&datasource.StaticVariableArgument{
 										Name:  []byte("url"),
 										Value: []byte("httpbin.org/anything"),
@@ -968,6 +1040,14 @@ func TestPlanner_Plan(t *testing.T) {
 									Client: datasource.DefaultHttpClient(),
 								},
 								Args: []datasource.Argument{
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_type_name"),
+										Value: []byte("Query"),
+									},
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_field_name"),
+										Value: []byte("withHeaders"),
+									},
 									&datasource.StaticVariableArgument{
 										Name:  []byte("url"),
 										Value: []byte("httpbin.org/anything"),
@@ -1291,6 +1371,14 @@ func TestPlanner_Plan(t *testing.T) {
 						Fetch: &SingleFetch{
 							Source: &DataSourceInvocation{
 								Args: []datasource.Argument{
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_type_name"),
+										Value: []byte("Query"),
+									},
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_field_name"),
+										Value: []byte("user"),
+									},
 									&datasource.StaticVariableArgument{
 										Name:  literal.URL,
 										Value: []byte("localhost:8001/graphql"),
@@ -1404,6 +1492,14 @@ func TestPlanner_Plan(t *testing.T) {
 						Fetch: &SingleFetch{
 							Source: &DataSourceInvocation{
 								Args: []datasource.Argument{
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_type_name"),
+										Value: []byte("Query"),
+									},
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_field_name"),
+										Value: []byte("restUser"),
+									},
 									&datasource.StaticVariableArgument{
 										Name:  literal.URL,
 										Value: []byte("localhost:9001/user/{{ .arguments.id }}"),
@@ -1530,6 +1626,14 @@ func TestPlanner_Plan(t *testing.T) {
 						Fetch: &SingleFetch{
 							Source: &DataSourceInvocation{
 								Args: []datasource.Argument{
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_type_name"),
+										Value: []byte("Query"),
+									},
+									&datasource.StaticVariableArgument{
+										Name:  []byte("root_field_name"),
+										Value: []byte("user"),
+									},
 									&datasource.StaticVariableArgument{
 										Name:  literal.URL,
 										Value: []byte("localhost:8001/graphql"),
@@ -1567,6 +1671,14 @@ func TestPlanner_Plan(t *testing.T) {
 									Fetch: &SingleFetch{
 										Source: &DataSourceInvocation{
 											Args: []datasource.Argument{
+												&datasource.StaticVariableArgument{
+													Name:  []byte("root_type_name"),
+													Value: []byte("User"),
+												},
+												&datasource.StaticVariableArgument{
+													Name:  []byte("root_field_name"),
+													Value: []byte("friends"),
+												},
 												&datasource.StaticVariableArgument{
 													Name:  literal.URL,
 													Value: []byte("localhost:9001/user/{{ .object.id }}/friends"),
diff --git a/pkg/fastbuffer/fastbuffer_test.go b/pkg/fastbuffer/fastbuffer_test.go
index 12be879b..8f1411e7 100644
--- a/pkg/fastbuffer/fastbuffer_test.go
+++ b/pkg/fastbuffer/fastbuffer_test.go
@@ -10,38 +10,38 @@ import (
 func TestFastBuffer(t *testing.T) {
 	buf := New()
 	buf.WriteBytes([]byte("Hello"))
-	assert.Equal(t,"Hello",string(buf.Bytes()))
+	assert.Equal(t, "Hello", string(buf.Bytes()))
 	buf.WriteBytes([]byte(", World!"))
-	assert.Equal(t,"Hello, World!",string(buf.Bytes()))
+	assert.Equal(t, "Hello, World!", string(buf.Bytes()))
 	buf.Reset()
-	assert.Equal(t,"",string(buf.Bytes()))
+	assert.Equal(t, "", string(buf.Bytes()))
 	buf.WriteBytes([]byte("Hello, World!"))
-	assert.Equal(t,"Hello, World!",string(buf.Bytes()))
+	assert.Equal(t, "Hello, World!", string(buf.Bytes()))
 	buf.Reset()
 	buf.WriteBytes([]byte("Goodbye!"))
-	assert.Equal(t,"Goodbye!",string(buf.Bytes()))
+	assert.Equal(t, "Goodbye!", string(buf.Bytes()))
 
-	buf.b = make([]byte,0)
+	buf.b = make([]byte, 0)
 	foobar := []byte("FooBar")
 	buf.WriteBytes(foobar)
 	foobar[0] = 'B'
-	assert.Equal(t,"FooBar",string(buf.Bytes()))
+	assert.Equal(t, "FooBar", string(buf.Bytes()))
 }
 
-func BenchmarkFastBuffer(b *testing.B){
+func BenchmarkFastBuffer(b *testing.B) {
 	data := []byte("Hello, World!")
 	b.Run("bytes.Buffer", func(b *testing.B) {
 
-		buf := bytes.NewBuffer(make([]byte,0,1024))
+		buf := bytes.NewBuffer(make([]byte, 0, 1024))
 
 		b.ResetTimer()
 		b.ReportAllocs()
 		b.SetBytes(int64(len(data)))
 
-		for i := 0;i<b.N;i++{
+		for i := 0; i < b.N; i++ {
 			buf.Reset()
 			buf.Write(data)
-			if !bytes.Equal(data,buf.Bytes()){
+			if !bytes.Equal(data, buf.Bytes()) {
 				b.Error("!=")
 			}
 		}
@@ -54,12 +54,12 @@ func BenchmarkFastBuffer(b *testing.B){
 		b.ReportAllocs()
 		b.SetBytes(int64(len(data)))
 
-		for i := 0;i<b.N;i++{
+		for i := 0; i < b.N; i++ {
 			buf.Reset()
 			buf.WriteBytes(data)
-			if !bytes.Equal(data,buf.Bytes()){
+			if !bytes.Equal(data, buf.Bytes()) {
 				b.Error("!=")
 			}
 		}
 	})
-}
\ No newline at end of file
+}
diff --git a/pkg/federation/schema_test.go b/pkg/federation/schema_test.go
index fc63dea5..980bbce0 100644
--- a/pkg/federation/schema_test.go
+++ b/pkg/federation/schema_test.go
@@ -7,9 +7,9 @@ import (
 )
 
 func TestSchemaBuilder_BuildFederationSchema(t *testing.T) {
-	actual,err := BuildFederationSchema(baseSchema,serviceSDL)
-	assert.NoError(t,err)
-	assert.Equal(t,federatedSchema, actual)
+	actual, err := BuildFederationSchema(baseSchema, serviceSDL)
+	assert.NoError(t, err)
+	assert.Equal(t, federatedSchema, actual)
 }
 
 const serviceSDL = `extend type Query {topProducts(first: Int = 5): [Product]}type Product @key(fields: "upc") {upc: String!name: String! price: Int!} extend type Query {me: User} type User @key(fields: "id"){ id: ID! username: String!} type Review { body: String! author: User! @provides(fields: "username") product: Product! } extend type User @key(fields: "id") { id: ID! @external reviews: [Review] } extend type Product @key(fields: "upc") { upc: String! @external reviews: [Review] }`
@@ -98,4 +98,4 @@ directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
 directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
 directive @key(fields: _FieldSet!) on OBJECT | INTERFACE
 directive @extends on OBJECT | INTERFACE
-`
\ No newline at end of file
+`
diff --git a/pkg/graphql/normalization_test.go b/pkg/graphql/normalization_test.go
index a48ff4b8..9a008802 100644
--- a/pkg/graphql/normalization_test.go
+++ b/pkg/graphql/normalization_test.go
@@ -36,6 +36,33 @@ func TestRequest_Normalize(t *testing.T) {
 		assert.True(t, result.Successful)
 		assert.True(t, request.isNormalized)
 	})
+
+	t.Run("should successfully normalize single query with arguments", func(t *testing.T) {
+		schema := starwarsSchema(t)
+		request := requestForQuery(t, starwars.FileDroidWithArgQuery)
+		documentBeforeNormalization := request.document
+
+		result, err := request.Normalize(schema)
+		assert.NoError(t, err)
+		assert.NotEqual(t, documentBeforeNormalization, request.document)
+		assert.Equal(t, []byte(`{"a":"R2D2"}`), request.document.Input.Variables)
+		assert.True(t, result.Successful)
+		assert.True(t, request.isNormalized)
+	})
+
+	t.Run("should successfully normalize multiple queries with arguments", func(t *testing.T) {
+		schema := starwarsSchema(t)
+		request := requestForQuery(t, starwars.FileMultiQueriesWithArguments)
+		request.OperationName = "GetDroid"
+		documentBeforeNormalization := request.document
+
+		result, err := request.Normalize(schema)
+		assert.NoError(t, err)
+		assert.NotEqual(t, documentBeforeNormalization, request.document)
+		assert.Equal(t, []byte(`{"a":"1"}`), request.document.Input.Variables)
+		assert.True(t, result.Successful)
+		assert.True(t, request.isNormalized)
+	})
 }
 
 func Test_normalizationResultFromReport(t *testing.T) {
diff --git a/pkg/graphql/request_test.go b/pkg/graphql/request_test.go
index e4293d38..d63c6d93 100644
--- a/pkg/graphql/request_test.go
+++ b/pkg/graphql/request_test.go
@@ -2,6 +2,7 @@ package graphql
 
 import (
 	"bytes"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -60,6 +61,7 @@ func TestRequest_CalculateComplexity(t *testing.T) {
 		assert.Equal(t, 0, result.NodeCount, "unexpected node count")
 		assert.Equal(t, 0, result.Complexity, "unexpected complexity")
 		assert.Equal(t, 0, result.Depth, "unexpected depth")
+		assert.Nil(t, result.PerRootField, "per root field results is not nil")
 	})
 
 	t.Run("should successfully calculate the complexity of request", func(t *testing.T) {
@@ -71,6 +73,44 @@ func TestRequest_CalculateComplexity(t *testing.T) {
 		assert.Equal(t, 1, result.NodeCount, "unexpected node count")
 		assert.Equal(t, 1, result.Complexity, "unexpected complexity")
 		assert.Equal(t, 2, result.Depth, "unexpected depth")
+		assert.Equal(t, []FieldComplexityResult{
+			{
+				TypeName:   "Query",
+				FieldName:  "hero",
+				Alias:      "",
+				NodeCount:  1,
+				Complexity: 1,
+				Depth:      1,
+			},
+		}, result.PerRootField, "unexpected per root field results")
+	})
+
+	t.Run("should successfully calculate the complexity of request with multiple query fields", func(t *testing.T) {
+		schema := starwarsSchema(t)
+
+		request := requestForQuery(t, starwars.FileHeroWithAliasesQuery)
+		result, err := request.CalculateComplexity(DefaultComplexityCalculator, schema)
+		assert.NoError(t, err)
+		assert.Equal(t, 2, result.NodeCount, "unexpected node count")
+		assert.Equal(t, 2, result.Complexity, "unexpected complexity")
+		assert.Equal(t, 2, result.Depth, "unexpected depth")
+		assert.Equal(t, []FieldComplexityResult{
+			{
+				TypeName:   "Query",
+				FieldName:  "hero",
+				Alias:      "empireHero",
+				NodeCount:  1,
+				Complexity: 1,
+				Depth:      1,
+			},
+			{
+				TypeName:   "Query",
+				FieldName:  "hero",
+				Alias:      "jediHero",
+				NodeCount:  1,
+				Complexity: 1,
+				Depth:      1,
+			}}, result.PerRootField, "unexpected per root field results")
 	})
 }
 
@@ -93,3 +133,50 @@ func requestForQuery(t *testing.T, fileName string) Request {
 
 	return request
 }
+
+func TestRequest_IsIntrospectionQuery(t *testing.T) {
+	t.Run("named introspection query", func(t *testing.T) {
+		var request Request
+		err := UnmarshalRequest(strings.NewReader(namedIntrospectionQuery), &request)
+		assert.NoError(t, err)
+
+		isIntrospectionQuery, err := request.IsIntrospectionQuery()
+		assert.NoError(t, err)
+		assert.True(t, isIntrospectionQuery)
+	})
+
+	t.Run("silent introspection query", func(t *testing.T) {
+		var request Request
+		err := UnmarshalRequest(strings.NewReader(silentIntrospectionQuery), &request)
+		assert.NoError(t, err)
+
+		isIntrospectionQuery, err := request.IsIntrospectionQuery()
+		assert.NoError(t, err)
+		assert.True(t, isIntrospectionQuery)
+	})
+
+	t.Run("not introspection query", func(t *testing.T) {
+		var request Request
+		err := UnmarshalRequest(strings.NewReader(nonIntrospectionQuery), &request)
+		assert.NoError(t, err)
+
+		isIntrospectionQuery, err := request.IsIntrospectionQuery()
+		assert.NoError(t, err)
+		assert.False(t, isIntrospectionQuery)
+	})
+
+	t.Run("mutation query", func(t *testing.T) {
+		var request Request
+		err := UnmarshalRequest(strings.NewReader(mutationQuery), &request)
+		assert.NoError(t, err)
+
+		isIntrospectionQuery, err := request.IsIntrospectionQuery()
+		assert.NoError(t, err)
+		assert.False(t, isIntrospectionQuery)
+	})
+}
+
+const namedIntrospectionQuery = `{"operationName":"IntrospectionQuery","variables":{},"query":"query IntrospectionQuery {\n  __schema {\n    queryType {\n      name\n    }\n    mutationType {\n      name\n    }\n    subscriptionType {\n      name\n    }\n    types {\n      ...FullType\n    }\n    directives {\n      name\n      description\n      locations\n      args {\n        ...InputValue\n      }\n    }\n  }\n}\n\nfragment FullType on __Type {\n  kind\n  name\n  description\n  fields(includeDeprecated: true) {\n    name\n    description\n    args {\n      ...InputValue\n    }\n    type {\n      ...TypeRef\n    }\n    isDeprecated\n    deprecationReason\n  }\n  inputFields {\n    ...InputValue\n  }\n  interfaces {\n    ...TypeRef\n  }\n  enumValues(includeDeprecated: true) {\n    name\n    description\n    isDeprecated\n    deprecationReason\n  }\n  possibleTypes {\n    ...TypeRef\n  }\n}\n\nfragment InputValue on __InputValue {\n  name\n  description\n  type {\n    ...TypeRef\n  }\n  defaultValue\n}\n\nfragment TypeRef on __Type {\n  kind\n  name\n  ofType {\n    kind\n    name\n    ofType {\n      kind\n      name\n      ofType {\n        kind\n        name\n        ofType {\n          kind\n          name\n          ofType {\n            kind\n            name\n            ofType {\n              kind\n              name\n              ofType {\n                kind\n                name\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"}`
+const silentIntrospectionQuery = `{"operationName":null,"variables":{},"query":"{\n  __schema {\n    queryType {\n      name\n    }\n    mutationType {\n      name\n    }\n    subscriptionType {\n      name\n    }\n    types {\n      ...FullType\n    }\n    directives {\n      name\n      description\n      locations\n      args {\n        ...InputValue\n      }\n    }\n  }\n}\n\nfragment FullType on __Type {\n  kind\n  name\n  description\n  fields(includeDeprecated: true) {\n    name\n    description\n    args {\n      ...InputValue\n    }\n    type {\n      ...TypeRef\n    }\n    isDeprecated\n    deprecationReason\n  }\n  inputFields {\n    ...InputValue\n  }\n  interfaces {\n    ...TypeRef\n  }\n  enumValues(includeDeprecated: true) {\n    name\n    description\n    isDeprecated\n    deprecationReason\n  }\n  possibleTypes {\n    ...TypeRef\n  }\n}\n\nfragment InputValue on __InputValue {\n  name\n  description\n  type {\n    ...TypeRef\n  }\n  defaultValue\n}\n\nfragment TypeRef on __Type {\n  kind\n  name\n  ofType {\n    kind\n    name\n    ofType {\n      kind\n      name\n      ofType {\n        kind\n        name\n        ofType {\n          kind\n          name\n          ofType {\n            kind\n            name\n            ofType {\n              kind\n              name\n              ofType {\n                kind\n                name\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"}`
+const nonIntrospectionQuery = `{"operationName":"Foo","query":"query Foo {bar}"}`
+const mutationQuery = `{"operationName":null,"query":"mutation Foo {bar}"}`
diff --git a/pkg/graphql/schema_test.go b/pkg/graphql/schema_test.go
index 2e489ea0..b072e3b8 100644
--- a/pkg/graphql/schema_test.go
+++ b/pkg/graphql/schema_test.go
@@ -2,8 +2,10 @@ package graphql
 
 import (
 	"bytes"
+	"io/ioutil"
 	"testing"
 
+	"github.com/sebdah/goldie"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -352,6 +354,35 @@ func TestSchema_Validate(t *testing.T) {
 	))
 }
 
+func TestSchema_IntrospectionResponse(t *testing.T) {
+	schemaBytes := []byte("schema { query: Query } type Query { hello: String }")
+	schemaReader := bytes.NewBuffer(schemaBytes)
+	schema, err := NewSchemaFromReader(schemaReader)
+	assert.NoError(t, err)
+	out := &bytes.Buffer{}
+	err = schema.IntrospectionResponse(out)
+	assert.NoError(t, err)
+	goldie.Assert(t, "introspection_response", out.Bytes())
+}
+
+func TestSchemaIntrospection(t *testing.T) {
+	schemaString := "schema { query: Query } type Query { hello: String }"
+	schema, err := NewSchemaFromString(schemaString)
+	assert.NoError(t, err)
+
+	result, err := SchemaIntrospection(schema)
+	assert.NoError(t, err)
+
+	resp := result.GetAsHTTPResponse()
+	assert.NotNil(t, resp)
+	assert.NotNil(t, resp.Body)
+
+	bodyBytes, err := ioutil.ReadAll(resp.Body)
+	assert.NoError(t, err)
+
+	goldie.Assert(t, "introspection_response", bodyBytes)
+}
+
 var invalidSchema = `type Query {
 	foo: Bar
 }`
diff --git a/pkg/http/ws_test.go b/pkg/http/ws_test.go
index fd38b1fd..d8509890 100644
--- a/pkg/http/ws_test.go
+++ b/pkg/http/ws_test.go
@@ -62,7 +62,7 @@ func TestWebsocketSubscriptionClient_ReadFromClient(t *testing.T) {
 		connToServer, connToClient := net.Pipe()
 		websocketClient := NewWebsocketSubscriptionClient(abstractlogger.NoopLogger, connToClient)
 
-		messageToServer := subscription.Message{
+		messageToServer := &subscription.Message{
 			Id:      "1",
 			Type:    subscription.MessageTypeData,
 			Payload: []byte(`{"data":null}`),
diff --git a/pkg/middleware/operation_complexity/operation_complexity_test.go b/pkg/middleware/operation_complexity/operation_complexity_test.go
index 1cc45fbe..edf66959 100644
--- a/pkg/middleware/operation_complexity/operation_complexity_test.go
+++ b/pkg/middleware/operation_complexity/operation_complexity_test.go
@@ -11,7 +11,30 @@ import (
 	"github.com/jensneuse/graphql-go-tools/pkg/operationreport"
 )
 
-func TestNodeCount(t *testing.T) {
+func TestCalculateOperationComplexity(t *testing.T) {
+	t.Run("query with a scalar return type", func(t *testing.T) {
+		run(t, testDefinition, `
+				{
+				  currentPeriod
+				}`,
+			OperationStats{
+				NodeCount:  0,
+				Complexity: 0,
+				Depth:      0,
+			},
+			[]RootFieldStats{
+				{
+					TypeName:  "Query",
+					FieldName: "currentPeriod",
+					Stats: OperationStats{
+						NodeCount:  0,
+						Complexity: 0,
+						Depth:      0,
+					},
+				},
+			},
+		)
+	})
 	t.Run("one user", func(t *testing.T) {
 		run(t, testDefinition, `
 				{
@@ -24,7 +47,92 @@ func TestNodeCount(t *testing.T) {
 					  country
 					}
 				  }
-				}`, 2, 2, 3)
+				}`,
+			OperationStats{
+				NodeCount:  2,
+				Complexity: 2,
+				Depth:      3,
+			},
+			[]RootFieldStats{
+				{
+					TypeName:  "Query",
+					FieldName: "users",
+					Stats: OperationStats{
+						NodeCount:  2,
+						Complexity: 2,
+						Depth:      2,
+					},
+				},
+			},
+		)
+	})
+	t.Run("one user with inline fragments", func(t *testing.T) {
+		run(t, testDefinition, `
+				{
+				  users(first: 1) {
+					... {
+						id
+						balance
+						name
+						address {
+						  city
+						  country
+						}
+					}
+				  }
+				}`,
+			OperationStats{
+				NodeCount:  2,
+				Complexity: 2,
+				Depth:      3,
+			},
+			[]RootFieldStats{
+				{
+					TypeName:  "Query",
+					FieldName: "users",
+					Stats: OperationStats{
+						NodeCount:  2,
+						Complexity: 2,
+						Depth:      2,
+					},
+				},
+			},
+		)
+	})
+	t.Run("one user with fragments", func(t *testing.T) {
+		run(t, testDefinition, `
+				{
+				  users(first: 1) {
+					...UserFragment
+				  }
+				}
+				fragment UserFragment on User {
+					id
+					balance
+					name
+					address {
+					  city
+					  country
+					}
+                }
+				`,
+			OperationStats{
+				NodeCount:  2,
+				Complexity: 2,
+				Depth:      3,
+			},
+			[]RootFieldStats{
+				{
+					TypeName:  "Query",
+					FieldName: "users",
+					Stats: OperationStats{
+						NodeCount:  2,
+						Complexity: 2,
+						Depth:      2,
+					},
+				},
+			},
+		)
 	})
 	t.Run("multiple users", func(t *testing.T) {
 		run(t, testDefinition, `
@@ -38,7 +146,24 @@ func TestNodeCount(t *testing.T) {
 					  country
 					}
 				  }
-				}`, 20, 11, 3)
+				}`,
+			OperationStats{
+				NodeCount:  20,
+				Complexity: 11,
+				Depth:      3,
+			},
+			[]RootFieldStats{
+				{
+					TypeName:  "Query",
+					FieldName: "users",
+					Stats: OperationStats{
+						NodeCount:  20,
+						Complexity: 11,
+						Depth:      2,
+					},
+				},
+			},
+		)
 	})
 	t.Run("multiple users with multiple transactions", func(t *testing.T) {
 		run(t, testDefinition, `
@@ -56,7 +181,24 @@ func TestNodeCount(t *testing.T) {
 						amount
 					}
 				  }
-				}`, 70, 21, 3)
+				}`,
+			OperationStats{
+				NodeCount:  70,
+				Complexity: 21,
+				Depth:      3,
+			},
+			[]RootFieldStats{
+				{
+					TypeName:  "Query",
+					FieldName: "users",
+					Stats: OperationStats{
+						NodeCount:  70,
+						Complexity: 21,
+						Depth:      2,
+					},
+				},
+			},
+		)
 	})
 	t.Run("multiple users with multiple transactions with nested senders", func(t *testing.T) {
 		run(t, testDefinition, `
@@ -88,32 +230,254 @@ func TestNodeCount(t *testing.T) {
 						}
 					}
 				  }
-				}`, 920, 221, 5)
+				}`,
+			OperationStats{
+				NodeCount:  920,
+				Complexity: 221,
+				Depth:      5,
+			},
+			[]RootFieldStats{
+				{
+					TypeName:  "Query",
+					FieldName: "users",
+					Stats: OperationStats{
+						NodeCount:  920,
+						Complexity: 221,
+						Depth:      4,
+					},
+				},
+			},
+		)
+	})
+	t.Run("multiple queries and one being an alias", func(t *testing.T) {
+		run(t, testDefinition, `
+				{
+				  person: user(id: "1") {
+					name
+ 				  }
+				  users(first: 1) {
+					id
+					balance
+					name
+					address {
+					  city
+					  country
+					}
+				  }
+				  bestUsers: users(first: 10) {
+					id
+					balance
+					name
+					address {
+					  city
+					  country
+					}
+					transactions(first: 5) {
+						id
+						amount
+					}
+				  }
+				}`,
+			OperationStats{
+				NodeCount:  73,
+				Complexity: 24,
+				Depth:      3,
+			},
+			[]RootFieldStats{
+				{
+					TypeName:  "Query",
+					FieldName: "user",
+					Alias:     "person",
+					Stats: OperationStats{
+						NodeCount:  1,
+						Complexity: 1,
+						Depth:      1,
+					},
+				},
+				{
+					TypeName:  "Query",
+					FieldName: "users",
+					Stats: OperationStats{
+						NodeCount:  2,
+						Complexity: 2,
+						Depth:      2,
+					},
+				},
+				{
+					TypeName:  "Query",
+					FieldName: "users",
+					Alias:     "bestUsers",
+					Stats: OperationStats{
+						NodeCount:  70,
+						Complexity: 21,
+						Depth:      2,
+					},
+				},
+			},
+		)
+	})
+	t.Run("multiple queries with different depth higher depth first", func(t *testing.T) {
+		run(t, testDefinition, `
+				{
+					transactions(first: 1) {
+						id
+						sender {
+							id
+							transactions(first: 1) {
+								id
+							}
+						}
+					}
+					users(first: 1) {
+						id
+						address {
+					  		city
+						}
+					}
+				}`,
+			OperationStats{
+				NodeCount:  5,
+				Complexity: 5,
+				Depth:      4,
+			},
+			[]RootFieldStats{
+				{
+					TypeName:  "Query",
+					FieldName: "transactions",
+					Stats: OperationStats{
+						NodeCount:  3,
+						Complexity: 3,
+						Depth:      3,
+					},
+				},
+				{
+					TypeName:  "Query",
+					FieldName: "users",
+					Stats: OperationStats{
+						NodeCount:  2,
+						Complexity: 2,
+						Depth:      2,
+					},
+				},
+			},
+		)
+	})
+	t.Run("multiple queries with different depth higher depth last", func(t *testing.T) {
+		run(t, testDefinition, `
+				{
+					users(first: 1) {
+						id
+						address {
+					  		city
+						}
+					}
+					transactions(first: 1) {
+						id
+						sender {
+							id
+							transactions(first: 1) {
+								id
+							}
+						}
+					}
+				}`,
+			OperationStats{
+				NodeCount:  5,
+				Complexity: 5,
+				Depth:      4,
+			},
+			[]RootFieldStats{
+				{
+					TypeName:  "Query",
+					FieldName: "users",
+					Stats: OperationStats{
+						NodeCount:  2,
+						Complexity: 2,
+						Depth:      2,
+					},
+				},
+				{
+					TypeName:  "Query",
+					FieldName: "transactions",
+					Stats: OperationStats{
+						NodeCount:  3,
+						Complexity: 3,
+						Depth:      3,
+					},
+				},
+			},
+		)
+	})
+	t.Run("multiple mutations with alias", func(t *testing.T) {
+		run(t, testDefinition, `
+				mutation AlterUsers {
+				  createJohn: createUser(input: {balance: 10, name: "John Doe", email: "john@doe.fake"}) {
+                    id
+				  }
+				  createJane: createUser(input: {balance: 100, name: "Jane Doe", email: "jane@doe.fake"}) {
+                    id
+				  }
+				}`,
+			OperationStats{
+				NodeCount:  2,
+				Complexity: 2,
+				Depth:      2,
+			},
+			[]RootFieldStats{
+				{
+					TypeName:  "Mutation",
+					FieldName: "createUser",
+					Alias:     "createJohn",
+					Stats: OperationStats{
+						NodeCount:  1,
+						Complexity: 1,
+						Depth:      1,
+					},
+				},
+				{
+					TypeName:  "Mutation",
+					FieldName: "createUser",
+					Alias:     "createJane",
+					Stats: OperationStats{
+						NodeCount:  1,
+						Complexity: 1,
+						Depth:      1,
+					},
+				},
+			},
+		)
 	})
 	t.Run("introspection query", func(t *testing.T) {
-		run(t, testDefinition, introspectionQuery, 0, 0, 0)
+		run(t, testDefinition, introspectionQuery,
+			OperationStats{
+				NodeCount:  0,
+				Complexity: 0,
+				Depth:      0,
+			},
+			[]RootFieldStats{},
+		)
 	})
 }
 
-var run = func(t *testing.T, definition, operation string, expectedNodeCount, expectedComplexity, expectedDepth int) {
+var run = func(t *testing.T, definition, operation string, expectedGlobalComplexityResult OperationStats, expectedFieldsComplexityResult []RootFieldStats) {
 	def := unsafeparser.ParseGraphqlDocumentString(definition)
 	op := unsafeparser.ParseGraphqlDocumentString(operation)
 	report := operationreport.Report{}
 
 	astnormalization.NormalizeOperation(&op, &def, &report)
 
-	actualNodeCount, actualComplexity, actualDepth := CalculateOperationComplexity(&op, &def, &report)
+	actualGlobalComplexityResult, actualFieldsComplexityResult := CalculateOperationComplexity(&op, &def, &report)
 	if report.HasErrors() {
 		require.NoError(t, report)
 	}
 
-	assert.Equal(t, expectedNodeCount, actualNodeCount, "unexpected node count")
-	assert.Equal(t, expectedComplexity, actualComplexity, "unexpected complexity")
-	assert.Equal(t, expectedDepth, actualDepth, "unexpected depth")
+	assert.Equal(t, expectedGlobalComplexityResult.NodeCount, actualGlobalComplexityResult.NodeCount, "unexpected global node count")
+	assert.Equal(t, expectedGlobalComplexityResult.Complexity, actualGlobalComplexityResult.Complexity, "unexpected global complexity")
+	assert.Equal(t, expectedGlobalComplexityResult.Depth, actualGlobalComplexityResult.Depth, "unexpected global depth")
+	assert.Equal(t, expectedFieldsComplexityResult, actualFieldsComplexityResult, "unexpected fields complexity result")
 }
 
 func BenchmarkEstimateComplexity(b *testing.B) {
-
 	def := unsafeparser.ParseGraphqlDocumentString(testDefinition)
 	op := unsafeparser.ParseGraphqlDocumentString(complexQuery)
 
@@ -124,19 +488,19 @@ func BenchmarkEstimateComplexity(b *testing.B) {
 	b.ReportAllocs()
 
 	for i := 0; i < b.N; i++ {
-		nodeCount, complexity, depth := estimator.Do(&op, &def, &report)
+		globalComplexityResult, _ := estimator.Do(&op, &def, &report)
 		if report.HasErrors() {
 			b.Fatal(report)
 		}
 
-		if nodeCount != 920 {
-			b.Fatalf("want nodeCount: 920, got: %d\n", nodeCount)
+		if globalComplexityResult.NodeCount != 920 {
+			b.Fatalf("want nodeCount: 920, got: %d\n", globalComplexityResult.NodeCount)
 		}
-		if complexity != 221 {
-			b.Fatalf("want complexity: 221, got: %d\n", complexity)
+		if globalComplexityResult.Complexity != 221 {
+			b.Fatalf("want complexity: 221, got: %d\n", globalComplexityResult.Complexity)
 		}
-		if depth != 5 {
-			b.Fatalf("want depth: 5, got: %d\n", depth)
+		if globalComplexityResult.Depth != 5 {
+			b.Fatalf("want depth: 5, got: %d\n", globalComplexityResult.Depth)
 		}
 	}
 }
@@ -232,6 +596,7 @@ type Query {
     user(id: ID!): User
     users(first: Int! @nodeCountMultiply, afterID: ID): [User]
     transactions(first: Int! @nodeCountMultiply, afterID: ID): [Transaction]
+    currentPeriod: String
 }
 
 type Mutation {
diff --git a/pkg/starwars/testdata/queries/invalid.query b/pkg/starwars/testdata/queries/invalid.query
new file mode 100644
index 00000000..9d8a80a8
--- /dev/null
+++ b/pkg/starwars/testdata/queries/invalid.query
@@ -0,0 +1,5 @@
+{
+    hero {
+        invalid
+    }
+}
\ No newline at end of file
diff --git a/pkg/starwars/testdata/queries/multi_queries_with_arguments.query b/pkg/starwars/testdata/queries/multi_queries_with_arguments.query
new file mode 100644
index 00000000..8cdb9a9e
--- /dev/null
+++ b/pkg/starwars/testdata/queries/multi_queries_with_arguments.query
@@ -0,0 +1,22 @@
+query GetDroid {
+    droid(id: "1") {
+        name
+    }
+}
+
+query Search {
+    search(name: "C3PO") {
+        ...on Droid {
+            name
+            primaryFunction
+        }
+        ...on Human {
+            name
+            height
+        }
+        ...on Starship {
+            name
+            length
+        }
+    }
+}
\ No newline at end of file
diff --git a/pkg/starwars/testdata/star_wars.graphql b/pkg/starwars/testdata/star_wars.graphql
index 556dec9e..8d664dcd 100644
--- a/pkg/starwars/testdata/star_wars.graphql
+++ b/pkg/starwars/testdata/star_wars.graphql
@@ -54,7 +54,7 @@ type Droid implements Character {
     friends: [Character]
 }
 
-type Startship {
+type Starship {
     name: String!
     length: Float!
 }
\ No newline at end of file
diff --git a/pkg/subscription/handler_test.go b/pkg/subscription/handler_test.go
index c70c4ce7..a895d1c6 100644
--- a/pkg/subscription/handler_test.go
+++ b/pkg/subscription/handler_test.go
@@ -22,7 +22,7 @@ func TestHandler_Handle(t *testing.T) {
 		_, client, handlerRoutine := setupSubscriptionHandlerTest(t)
 
 		t.Run("should send connection error message when error on read occurrs", func(t *testing.T) {
-			client.prepareConnectionInitMessage().withError().and().resetReceivedMessages()
+			client.prepareConnectionInitMessage().withError().and().send()
 
 			ctx, cancelFunc := context.WithCancel(context.Background())
 
@@ -39,7 +39,7 @@ func TestHandler_Handle(t *testing.T) {
 		})
 
 		t.Run("should successfully init connection and respond with ack", func(t *testing.T) {
-			client.prepareConnectionInitMessage().withoutError().and().resetReceivedMessages()
+			client.reconnect().and().prepareConnectionInitMessage().withoutError().and().send()
 
 			ctx, cancelFunc := context.WithCancel(context.Background())
 
@@ -64,7 +64,7 @@ func TestHandler_Handle(t *testing.T) {
 
 			subscriptionHandler.ChangeKeepAliveInterval(keepAliveInterval)
 
-			client.prepareConnectionInitMessage().withoutError().and().resetReceivedMessages()
+			client.prepareConnectionInitMessage().withoutError().and().send()
 			ctx, cancelFunc := context.WithCancel(context.Background())
 
 			handlerRoutineFunc := handlerRoutine(ctx)
@@ -89,12 +89,106 @@ func TestHandler_Handle(t *testing.T) {
 		})
 	})
 
+	t.Run("erroneous operation(s)", func(t *testing.T) {
+		_, client, handlerRoutine := setupSubscriptionHandlerTest(t)
+		ctx, cancelFunc := context.WithCancel(context.Background())
+		handlerRoutineFunc := handlerRoutine(ctx)
+		go handlerRoutineFunc()
+
+		t.Run("should send error when query contains syntax errors", func(t *testing.T) {
+			payload := []byte(`{"operationName": "Broken", "query Broken {": "", "variables": null}`)
+			client.prepareStartMessage("1", payload).withoutError().send()
+
+			waitForClientHavingAMessage := func() bool {
+				return client.hasMoreMessagesThan(0)
+			}
+			require.Eventually(t, waitForClientHavingAMessage, 5*time.Second, 5*time.Millisecond)
+
+			jsonErrorMsg, err := json.Marshal("document doesn't contain any executable operation, locations: [], path: []")
+			require.NoError(t, err)
+
+			expectedMessage := Message{
+				Id:      "1",
+				Type:    MessageTypeError,
+				Payload: jsonErrorMsg,
+			}
+
+			messagesFromServer := client.readFromServer()
+			assert.Contains(t, messagesFromServer, expectedMessage)
+		})
+
+		cancelFunc()
+	})
+
+	t.Run("non-subscription query", func(t *testing.T) {
+		subscriptionHandler, client, handlerRoutine := setupSubscriptionHandlerTest(t)
+
+		t.Run("should process query and return error when query is not valid", func(t *testing.T) {
+			payload := starwars.LoadQuery(t, starwars.FileInvalidQuery, nil)
+			client.prepareStartMessage("1", payload).withoutError().and().send()
+
+			ctx, cancelFunc := context.WithCancel(context.Background())
+			cancelFunc()
+			handlerRoutineFunc := handlerRoutine(ctx)
+			go handlerRoutineFunc()
+
+			waitForClientHavingAMessage := func() bool {
+				return client.hasMoreMessagesThan(0)
+			}
+			require.Eventually(t, waitForClientHavingAMessage, 1*time.Second, 5*time.Millisecond)
+
+			jsonErrMessage, err := json.Marshal("field: invalid not defined on type: Character, locations: [], path: [query,hero,invalid]")
+			require.NoError(t, err)
+			expectedErrorMessage := Message{
+				Id:      "1",
+				Type:    MessageTypeError,
+				Payload: jsonErrMessage,
+			}
+
+			messagesFromServer := client.readFromServer()
+			assert.Contains(t, messagesFromServer, expectedErrorMessage)
+			assert.Equal(t, 0, subscriptionHandler.ActiveSubscriptions())
+		})
+
+		t.Run("should process and send result for a query", func(t *testing.T) {
+			payload := starwars.LoadQuery(t, starwars.FileSimpleHeroQuery, nil)
+			client.prepareStartMessage("1", payload).withoutError().and().send()
+
+			ctx, cancelFunc := context.WithCancel(context.Background())
+			cancelFunc()
+			handlerRoutineFunc := handlerRoutine(ctx)
+			go handlerRoutineFunc()
+
+			waitForClientHavingTwoMessages := func() bool {
+				return client.hasMoreMessagesThan(1)
+			}
+			require.Eventually(t, waitForClientHavingTwoMessages, 1*time.Second, 5*time.Millisecond)
+
+			expectedDataMessage := Message{
+				Id:      "1",
+				Type:    MessageTypeData,
+				Payload: []byte(`{"data":null}`),
+			}
+
+			expectedCompleteMessage := Message{
+				Id:      "1",
+				Type:    MessageTypeComplete,
+				Payload: nil,
+			}
+
+			messagesFromServer := client.readFromServer()
+			assert.Contains(t, messagesFromServer, expectedDataMessage)
+			assert.Contains(t, messagesFromServer, expectedCompleteMessage)
+			assert.Equal(t, 0, subscriptionHandler.ActiveSubscriptions())
+		})
+	})
+
 	t.Run("subscription query", func(t *testing.T) {
 		subscriptionHandler, client, handlerRoutine := setupSubscriptionHandlerTest(t)
 
 		t.Run("should start subscription on start", func(t *testing.T) {
 			payload := starwars.LoadQuery(t, starwars.FileRemainingJedisSubscription, nil)
-			client.prepareStartMessage("1", payload).withoutError().and().resetReceivedMessages()
+			client.prepareStartMessage("1", payload).withoutError().and().send()
 
 			ctx, cancelFunc := context.WithCancel(context.Background())
 			handlerRoutineFunc := handlerRoutine(ctx)
@@ -114,8 +208,8 @@ func TestHandler_Handle(t *testing.T) {
 			assert.Equal(t, 1, subscriptionHandler.ActiveSubscriptions())
 		})
 
-		t.Run("should stop subscription on stop", func(t *testing.T) {
-			client.prepareStopMessage("1").withoutError().and().resetReceivedMessages()
+		t.Run("should stop subscription on stop and send complete message to client", func(t *testing.T) {
+			client.reconnect().prepareStopMessage("1").withoutError().and().send()
 
 			ctx, cancelFunc := context.WithCancel(context.Background())
 			handlerRoutineFunc := handlerRoutine(ctx)
@@ -130,6 +224,15 @@ func TestHandler_Handle(t *testing.T) {
 			assert.Eventually(t, waitForCanceledSubscription, 1*time.Second, 5*time.Millisecond)
 			assert.Equal(t, 0, subscriptionHandler.ActiveSubscriptions())
 
+			expectedMessage := Message{
+				Id:      "1",
+				Type:    MessageTypeComplete,
+				Payload: nil,
+			}
+
+			messagesFromServer := client.readFromServer()
+			assert.Contains(t, messagesFromServer, expectedMessage)
+
 			cancelFunc()
 		})
 	})
@@ -138,7 +241,7 @@ func TestHandler_Handle(t *testing.T) {
 		_, client, handlerRoutine := setupSubscriptionHandlerTest(t)
 
 		t.Run("should successfully disconnect from client", func(t *testing.T) {
-			client.prepareConnectionTerminateMessage().withoutError()
+			client.prepareConnectionTerminateMessage().withoutError().and().send()
 			require.True(t, client.connected)
 
 			ctx, cancelFunc := context.WithCancel(context.Background())
diff --git a/pkg/subscription/mock_client_test.go b/pkg/subscription/mock_client_test.go
index e01f775a..fd0ed4a9 100644
--- a/pkg/subscription/mock_client_test.go
+++ b/pkg/subscription/mock_client_test.go
@@ -6,22 +6,28 @@ import (
 
 type mockClient struct {
 	messagesFromServer []Message
-	messageToServer    Message
+	messageToServer    *Message
 	err                error
+	messagePipe        chan *Message
 	connected          bool
 	serverHasRead      bool
 }
 
 func newMockClient() *mockClient {
 	return &mockClient{
-		connected: true,
+		connected:   true,
+		messagePipe: make(chan *Message, 1),
 	}
 }
 
-func (c *mockClient) ReadFromClient() (Message, error) {
-	returnMessage, returnErr := c.messageToServer, c.err
+func (c *mockClient) ReadFromClient() (*Message, error) {
+	returnErr := c.err
+	returnMessage := <-c.messagePipe
+	if returnErr != nil {
+		return nil, returnErr
+	}
+
 	c.serverHasRead = true
-	c.messageToServer = Message{}
 	c.err = nil
 	return returnMessage, returnErr
 }
@@ -40,12 +46,16 @@ func (c *mockClient) Disconnect() error {
 	return nil
 }
 
+func (c *mockClient) hasMoreMessagesThan(num int) bool {
+	return len(c.messagesFromServer) > num
+}
+
 func (c *mockClient) readFromServer() []Message {
 	return c.messagesFromServer
 }
 
 func (c *mockClient) prepareConnectionInitMessage() *mockClient {
-	c.messageToServer = Message{
+	c.messageToServer = &Message{
 		Type: MessageTypeConnectionInit,
 	}
 
@@ -53,7 +63,7 @@ func (c *mockClient) prepareConnectionInitMessage() *mockClient {
 }
 
 func (c *mockClient) prepareStartMessage(id string, payload []byte) *mockClient {
-	c.messageToServer = Message{
+	c.messageToServer = &Message{
 		Id:      id,
 		Type:    MessageTypeStart,
 		Payload: payload,
@@ -63,7 +73,7 @@ func (c *mockClient) prepareStartMessage(id string, payload []byte) *mockClient
 }
 
 func (c *mockClient) prepareStopMessage(id string) *mockClient {
-	c.messageToServer = Message{
+	c.messageToServer = &Message{
 		Id:      id,
 		Type:    MessageTypeStop,
 		Payload: nil,
@@ -73,13 +83,19 @@ func (c *mockClient) prepareStopMessage(id string) *mockClient {
 }
 
 func (c *mockClient) prepareConnectionTerminateMessage() *mockClient {
-	c.messageToServer = Message{
+	c.messageToServer = &Message{
 		Type: MessageTypeConnectionTerminate,
 	}
 
 	return c
 }
 
+func (c *mockClient) send() bool {
+	c.messagePipe <- c.messageToServer
+	c.messageToServer = nil
+	return true
+}
+
 func (c *mockClient) withoutError() *mockClient {
 	c.err = nil
 	return c
@@ -94,7 +110,13 @@ func (c *mockClient) and() *mockClient {
 	return c
 }
 
-func (c *mockClient) resetReceivedMessages() *mockClient {
+func (c *mockClient) reset() *mockClient {
 	c.messagesFromServer = []Message{}
 	return c
 }
+
+func (c *mockClient) reconnect() *mockClient {
+	c.reset()
+	c.connected = true
+	return c
+}