diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 7603b46..526b6f2 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -13,7 +13,7 @@ on:
 jobs:
   test-go:
     name: Run Go lint and unit tests
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-latest
     permissions:
       pull-requests: write
       # Required: allow read access to the content for analysis.
@@ -26,14 +26,6 @@ jobs:
       - uses: actions/setup-go@v5
         with:
           go-version: "1.20"
-      - uses: actions/cache@v4
-        with:
-          path: |
-            ~/go/pkg/mod
-            ~/.cache/go-build
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
-          restore-keys: |
-            ${{ runner.os }}-go-
       - name: Install dependencies
         run: |
           go get -t -v ./...
@@ -47,13 +39,11 @@ jobs:
           cd ./example/hasura
           docker-compose up -d
       - name: Lint
-        uses: golangci/golangci-lint-action@v4
+        uses: golangci/golangci-lint-action@v6
         with:
           version: latest
           only-new-issues: true
           skip-cache: true
-          skip-pkg-cache: true
-          skip-build-cache: true
           args: --timeout=120s
       - name: Run Go unit tests for example/subscription
         run: |
@@ -63,13 +53,13 @@ jobs:
       - name: Run Go unit tests
         run: go test -v -race -timeout 3m -coverprofile=coverage.out ./...
       - name: Go coverage format
-        if: ${{ github.event_name == 'pull_request' }}
+        if: ${{ github.event_name == 'pull_request' && github.repository == 'hasura/go-graphql-client' }}
         run: |
           go get github.com/boumenot/gocover-cobertura
           go install github.com/boumenot/gocover-cobertura
           gocover-cobertura < coverage.out > coverage.xml
       - name: Code Coverage Summary Report
-        if: ${{ github.event_name == 'pull_request' }}
+        if: ${{ github.event_name == 'pull_request' && github.repository == 'hasura/go-graphql-client' }}
         uses: irongut/CodeCoverageSummary@v1.3.0
         with:
           filename: coverage.xml
@@ -83,7 +73,7 @@ jobs:
           thresholds: "60 80"
       - name: Add Coverage PR Comment
         uses: marocchino/sticky-pull-request-comment@v2
-        if: ${{ github.event_name == 'pull_request' }}
+        if: ${{ github.event_name == 'pull_request' && github.repository == 'hasura/go-graphql-client' }}
         with:
           path: code-coverage-results.md
       - name: Dump docker logs on failure
diff --git a/README.md b/README.md
index 9cb43c4..61d5fd4 100644
--- a/README.md
+++ b/README.md
@@ -1,54 +1,54 @@
-go-graphql-client
-=======
+# go-graphql-client
 
 [![Unit tests](https://github.com/hasura/go-graphql-client/actions/workflows/test.yml/badge.svg)](https://github.com/hasura/go-graphql-client/actions/workflows/test.yml)
 
 **Preface:** This is a fork of `https://github.com/shurcooL/graphql` with extended features (subscription client, named operation)
 
-The subscription client follows Apollo client specification https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md, using websocket protocol with https://github.com/nhooyr/websocket, a minimal and idiomatic WebSocket library for Go.
+The subscription client follows Apollo client specification https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md, using WebSocket protocol with https://github.com/nhooyr/websocket, a minimal and idiomatic WebSocket library for Go.
 
 Package `graphql` provides a GraphQL client implementation.
 
 For more information, see package [`github.com/shurcooL/githubv4`](https://github.com/shurcooL/githubv4), which is a specialized version targeting GitHub GraphQL API v4. That package is driving the feature development.
 
-**Note**: Before v0.8.0, `QueryRaw`, `MutateRaw` and `Subscribe` methods return `*json.RawMessage`. This output type is redundant to be decoded. From v0.8.0, the output type is changed to `[]byte`.
+**Note**: Before v0.8.0, `QueryRaw`, `MutateRaw`, and `Subscribe` methods return `*json.RawMessage`. This output type is redundant to be decoded. From v0.8.0, the output type is changed to `[]byte`.
 
 - [go-graphql-client](#go-graphql-client)
-	- [Installation](#installation)
-	- [Usage](#usage)
-		- [Authentication](#authentication)
-		- [Simple Query](#simple-query)
-		- [Arguments and Variables](#arguments-and-variables)
-		- [Custom scalar tag](#custom-scalar-tag)
-		- [Skip GraphQL field](#skip-graphql-field)
-		- [Inline Fragments](#inline-fragments)
-		- [Specify GraphQL type name](#specify-graphql-type-name)
-		- [Mutations](#mutations)
-			- [Mutations Without Fields](#mutations-without-fields)
-		- [Subscription](#subscription)
-			- [Usage](#usage-1)
-			- [Subscribe](#subscribe)
-			- [Stop the subscription](#stop-the-subscription)
-			- [Authentication](#authentication-1)
-			- [Options](#options)
-			- [Subscription Protocols](#subscription-protocols)
-			- [Handle connection error](#handle-connection-error)
-			- [Events](#events)
-			- [Custom HTTP Client](#custom-http-client)
-			- [Custom WebSocket client](#custom-websocket-client)
-		- [Options](#options-1)
-		- [Execute pre-built query](#execute-pre-built-query)
-		- [With operation name (deprecated)](#with-operation-name-deprecated)
-		- [Raw bytes response](#raw-bytes-response)
-		- [Multiple mutations with ordered map](#multiple-mutations-with-ordered-map)
-		- [Debugging and Unit test](#debugging-and-unit-test)
-	- [Directories](#directories)
-	- [References](#references)
-	- [License](#license)
+  - [Installation](#installation)
+  - [Usage](#usage)
+    - [Authentication](#authentication)
+    - [Simple Query](#simple-query)
+    - [Arguments and Variables](#arguments-and-variables)
+    - [Custom scalar tag](#custom-scalar-tag)
+    - [Skip GraphQL field](#skip-graphql-field)
+    - [Inline Fragments](#inline-fragments)
+    - [Specify GraphQL type name](#specify-graphql-type-name)
+    - [Mutations](#mutations)
+      - [Mutations Without Fields](#mutations-without-fields)
+    - [Subscription](#subscription)
+      - [Usage](#usage-1)
+      - [Subscribe](#subscribe)
+      - [Stop the subscription](#stop-the-subscription)
+      - [Authentication](#authentication-1)
+      - [Options](#options)
+      - [Subscription Protocols](#subscription-protocols)
+      - [Handle connection error](#handle-connection-error)
+      - [Events](#events)
+      - [Custom HTTP Client](#custom-http-client)
+      - [Custom WebSocket client](#custom-websocket-client)
+    - [Options](#options-1)
+    - [Execute pre-built query](#execute-pre-built-query)
+    - [With operation name (deprecated)](#with-operation-name-deprecated)
+    - [Raw bytes response](#raw-bytes-response)
+    - [Multiple mutations with ordered map](#multiple-mutations-with-ordered-map)
+    - [Debugging and Unit test](#debugging-and-unit-test)
+  - [Directories](#directories)
+  - [References](#references)
+  - [License](#license)
 
 ## Installation
 
-`go-graphql-client` requires Go version 1.20 or later. For older Go versions: 
+`go-graphql-client` requires Go version 1.20 or later. For older Go versions:
+
 - **>= 1.16 < 1.20**: downgrade the library to version v0.9.x
 - **< 1.16**: downgrade the library version below v0.7.1.
 
@@ -188,7 +188,8 @@ if err != nil {
 }
 ```
 
-Variables get encoded as normal json. So if you supply a struct for a variable and want to rename fields, you can do this like that:
+Variables get encoded as normal JSON. So if you supply a struct for a variable and want to rename fields, you can do this like this:
+
 ```Go
 type Dimensions struct {
 	Width int `json:"ship_width"`,
@@ -213,6 +214,7 @@ variables := map[string]interface{}{
 err := client.Mutate(context.TODO(), &mutation, variables)
 
 ```
+
 which will set `ship_dimensions` to an object with the properties `ship_width` and `ship_height`.
 
 ### Custom scalar tag
@@ -344,7 +346,7 @@ fmt.Println(q.Hero.Height)
 
 ### Specify GraphQL type name
 
-The GraphQL type is automatically inferred from Go type by reflection. However, it's cumbersome in some use cases, e.g lowercase names. In Go, a type name with a first lowercase letter is considered private. If we need to reuse it for other packages, there are 2 approaches: type alias or implement `GetGraphQLType` method.
+The GraphQL type is automatically inferred from Go type by reflection. However, it's cumbersome in some use cases, e.g. lowercase names. In Go, a type name with a first lowercase letter is considered private. If we need to reuse it for other packages, there are 2 approaches: type alias or implement `GetGraphQLType` method.
 
 ```go
 type UserReviewInput struct {
@@ -533,7 +535,7 @@ if err != nil {
 
 #### Stop the subscription
 
-You can programmatically stop the subscription while the client is running by using the `Unsubscribe` method, or returning a special error to stop it in the callback.
+You can programmatically stop the subscription while the client is running by using the `Unsubscribe` method or returning a special error to stop it in the callback.
 
 ```Go
 subscriptionId, err := client.Subscribe(&query, nil, func(dataValue []byte, errValue error) error {
@@ -580,7 +582,7 @@ client := graphql.NewSubscriptionClient(serverEndpoint).
             "Authorization": []string{"Bearer random-secret"},
         },
     })
-``` 
+```
 
 #### Options
 
@@ -608,6 +610,7 @@ client.
 #### Subscription Protocols
 
 The subscription client supports 2 protocols:
+
 - [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md) (default)
 - [graphql-ws](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md)
 
@@ -669,7 +672,7 @@ client.WithWebSocketOptions(WebsocketOptions{
 
 #### Custom WebSocket client
 
-By default the subscription client uses [nhooyr WebSocket client](https://github.com/nhooyr/websocket). If you need to customize the client, or prefer using [Gorilla WebSocket](https://github.com/gorilla/websocket), let's follow the Websocket interface and replace the constructor with `WithWebSocket` method:
+By default, the subscription client uses [nhooyr WebSocket client](https://github.com/nhooyr/websocket). If you need to customize the client or prefer using [Gorilla WebSocket](https://github.com/gorilla/websocket), let's follow the WebSocket interface and replace the constructor with `WithWebSocket` method:
 
 ```go
 // WebsocketHandler abstracts WebSocket connection functions
@@ -721,7 +724,7 @@ client.Run()
 
 ### Options
 
-There are extensible parts in the GraphQL query that we sometimes use. They are optional so that we shouldn't required them in the method. To make it flexible, we can abstract these options as optional arguments that follow this interface.
+There are extensible parts in the GraphQL query that we sometimes use. They are optional so we shouldn't require them in the method. To make it flexible, we can abstract these options as optional arguments that follow this interface.
 
 ```go
 type Option interface {
@@ -732,7 +735,12 @@ type Option interface {
 client.Query(ctx context.Context, q interface{}, variables map[string]interface{}, options ...Option) error
 ```
 
-Currently we support 2 option types: `operation_name` and `operation_directive`. The operation name option is built-in because it is unique. We can use the option directly with `OperationName`
+Currently, there are 3 option types:
+- `operation_name`
+- `operation_directive`
+- `bind_extensions`
+
+The operation name option is built-in because it is unique. We can use the option directly with `OperationName`.
 
 ```go
 // query MyQuery {
@@ -741,7 +749,7 @@ Currently we support 2 option types: `operation_name` and `operation_directive`.
 client.Query(ctx, &q, variables, graphql.OperationName("MyQuery"))
 ```
 
-In contrast, operation directive is various and customizable on different GraphQL servers. There isn't any built-in directive in the library. You need to define yourself. For example:
+In contrast, operation directives are various and customizable on different GraphQL servers. There isn't any built-in directive in the library. You need to define yourself. For example:
 
 ```go
 // define @cached directive for Hasura queries
@@ -770,7 +778,7 @@ client.Query(ctx, &q, variables, graphql.OperationName("MyQuery"), cachedDirecti
 
 ### Execute pre-built query
 
-The `Exec` function allows you to executing pre-built queries. While using reflection to build queries is convenient as you get some resemblance of type safety, it gets very cumbersome when you need to create queries semi-dynamically. For instance, imagine you are building a CLI tool to query data from a graphql endpoint and you want users to be able to narrow down the query by passing cli flags or something.
+The `Exec` function allows you to execute pre-built queries. While using reflection to build queries is convenient as you get some resemblance of type safety, it gets very cumbersome when you need to create queries semi-dynamically. For instance, imagine you are building a CLI tool to query data from a graphql endpoint and you want users to be able to narrow down the query by passing CLI flags or something.
 
 ```Go
 // filters would be built dynamically somehow from the command line flags
@@ -818,9 +826,44 @@ if err != nil {
 err = json.Unmarshal(raw, &res)
 ```
 
-### With operation name (deprecated)
+### Get extensions from response
+
+The response map may also contain an entry with the `extensions` key. To decode this field you need to bind a struct or map pointer. The client will optionally unmarshal the field using JSON decoder.
+
+```go
+var q struct {
+	User struct {
+		ID   string `graphql:"id"`
+		Name string `graphql:"name"`
+	}
+}
+
+var ext struct {
+	ID     int    `json:"id"`
+	Domain string `json:"domain"`
+}
+
+err := client.Query(context.Background(), &q, map[string]interface{}{}, graphql.BindExtensions(&ext))
+if err != nil {
+	t.Fatal(err)
+}
+```
 
-Operation name is still on API decision plan https://github.com/shurcooL/graphql/issues/12. However, in my opinion separate methods are easier choice to avoid breaking changes
+Additionally, if you need information about the extensions returned in the response use `ExecRawWithExtensions`. This function returns a map with extensions as the second variable.
+
+```Go
+query := `query{something(where: { foo: { _eq: "bar" }}){id}}`
+
+data, extensions, err := client.ExecRawWithExtensions(ctx, query, map[string]any{})
+if err != nil {
+	panic(err)
+}
+
+// You can now use the `extensions` variable to access the extensions data
+fmt.Println("Extensions:", extensions)
+```
+
+### With operation name (deprecated)
 
 ```Go
 func (c *Client) NamedQuery(ctx context.Context, name string, q interface{}, variables map[string]interface{}) error
@@ -832,7 +875,7 @@ func (sc *SubscriptionClient) NamedSubscribe(name string, v interface{}, variabl
 
 ### Raw bytes response
 
-In the case we developers want to decode JSON response ourself. Moreover, the default `UnmarshalGraphQL` function isn't ideal with complicated nested interfaces
+In the case when we developers want to decode JSON response ourselves. Moreover, the default `UnmarshalGraphQL` function isn't ideal with complicated nested interfaces
 
 ```Go
 func (c *Client) QueryRaw(ctx context.Context, q interface{}, variables map[string]interface{}) ([]byte, error)
@@ -846,7 +889,7 @@ func (c *Client) NamedMutateRaw(ctx context.Context, name string, q interface{},
 
 ### Multiple mutations with ordered map
 
-You might need to make multiple mutations in single query. It's not very convenient with structs
+You might need to make multiple mutations in a single query. It's not very convenient with structs
 so you can use ordered map `[][2]interface{}` instead.
 
 For example, to make the following GraphQL mutation:
@@ -884,41 +927,42 @@ variables := map[string]interface{}{
 
 ### Debugging and Unit test
 
-Enable debug mode with the `WithDebug` function. If the request is failed, the request and response information will be included in `extensions[].internal` property.
+Enable debug mode with the `WithDebug` function. If the request fails, the request and response information will be included in `extensions[].internal` property.
 
 ```json
 {
-	"errors": [
-		{
-			"message":"Field 'user' is missing required arguments: login",
-			"extensions": {
-				"internal": {
-					"request": {
-						"body":"{\"query\":\"{user{name}}\"}",
-						"headers": {
-							"Content-Type": ["application/json"]
-						}
-					},
-					"response": {
-						"body":"{\"errors\": [{\"message\": \"Field 'user' is missing required arguments: login\",\"locations\": [{\"line\": 7,\"column\": 3}]}]}",
-						"headers": {
-							"Content-Type": ["application/json"]
-						}
-					}
-				}
-			},
-			"locations": [
-				{
-					"line":7,
-					"column":3
-				}
-			]
-		}
-	]
+  "errors": [
+    {
+      "message": "Field 'user' is missing required arguments: login",
+      "extensions": {
+        "internal": {
+          "request": {
+            "body": "{\"query\":\"{user{name}}\"}",
+            "headers": {
+              "Content-Type": ["application/json"]
+            }
+          },
+          "response": {
+            "body": "{\"errors\": [{\"message\": \"Field 'user' is missing required arguments: login\",\"locations\": [{\"line\": 7,\"column\": 3}]}]}",
+            "headers": {
+              "Content-Type": ["application/json"]
+            }
+          }
+        }
+      },
+      "locations": [
+        {
+          "line": 7,
+          "column": 3
+        }
+      ]
+    }
+  ]
 }
 ```
 
 For debugging queries, you can use `Construct*` functions to see what the generated query looks like:
+
 ```go
 // ConstructQuery build GraphQL query string from struct and variables
 func ConstructQuery(v interface{}, variables map[string]interface{}, options ...Option) (string, error)
@@ -934,25 +978,22 @@ func ConstructSubscription(v interface{}, variables map[string]interface{}, opti
 func UnmarshalGraphQL(data []byte, v interface{}) error
 ```
 
-Because the GraphQL query string is generated in runtime using reflection, it isn't really safe. To assure the GraphQL query is expected, it's necessary to write some unit test for query construction.
+Because the GraphQL query string is generated in runtime using reflection, it isn't really safe. To ensure the GraphQL query is expected, it's necessary to write some unit tests for query construction.
 
-Directories
------------
+## Directories
 
 | Path                                                                                   | Synopsis                                                                                                        |
-|----------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
+| -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
 | [example/graphqldev](https://godoc.org/github.com/shurcooL/graphql/example/graphqldev) | graphqldev is a test program currently being used for developing graphql package.                               |
-| [ident](https://godoc.org/github.com/shurcooL/graphql/ident)                           | Package ident provides functions for parsing and converting identifier names between various naming convention. |
+| [ident](https://godoc.org/github.com/shurcooL/graphql/ident)                           | Package ident provides functions for parsing and converting identifier names between various naming conventions. |
 | [internal/jsonutil](https://godoc.org/github.com/shurcooL/graphql/internal/jsonutil)   | Package jsonutil provides a function for decoding JSON into a GraphQL query data structure.                     |
 
-References
-----------
+## References
+
 - https://github.com/shurcooL/graphql
 - https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md
 - https://github.com/nhooyr/websocket
 
+## License
 
-License
--------
-
--	[MIT License](LICENSE)
+- [MIT License](LICENSE)
diff --git a/example/graphql-ws-bc/server/package-lock.json b/example/graphql-ws-bc/server/package-lock.json
index ce9c413..e5e8253 100644
--- a/example/graphql-ws-bc/server/package-lock.json
+++ b/example/graphql-ws-bc/server/package-lock.json
@@ -9,15 +9,15 @@
       "version": "1.0.0",
       "license": "MIT",
       "dependencies": {
-        "graphql": "^16.8.1",
-        "graphql-ws": "^5.14.3",
+        "graphql": "^16.9.0",
+        "graphql-ws": "^5.16.0",
         "subscriptions-transport-ws": "^0.11.0",
-        "ws": "^8.16.0"
+        "ws": "^8.17.1"
       },
       "devDependencies": {
         "@types/ws": "^8.5.10",
         "ts-node": "^10.9.2",
-        "typescript": "^5.3.3"
+        "typescript": "^5.5.2"
       }
     },
     "node_modules/@cspotcode/source-map-support": {
@@ -152,17 +152,17 @@
       "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="
     },
     "node_modules/graphql": {
-      "version": "16.8.1",
-      "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz",
-      "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==",
+      "version": "16.9.0",
+      "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz",
+      "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==",
       "engines": {
         "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
       }
     },
     "node_modules/graphql-ws": {
-      "version": "5.14.3",
-      "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.14.3.tgz",
-      "integrity": "sha512-F/i2xNIVbaEF2xWggID0X/UZQa2V8kqKDPO8hwmu53bVOcTL7uNkxnexeEgSCVxYBQUTUNEI8+e4LO1FOhKPKQ==",
+      "version": "5.16.0",
+      "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.16.0.tgz",
+      "integrity": "sha512-Ju2RCU2dQMgSKtArPbEtsK5gNLnsQyTNIo/T7cZNp96niC1x0KdJNZV0TIoilceBPQwfb5itrGl8pkFeOUMl4A==",
       "engines": {
         "node": ">=10"
       },
@@ -198,9 +198,9 @@
       }
     },
     "node_modules/subscriptions-transport-ws/node_modules/ws": {
-      "version": "7.5.9",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
-      "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
+      "version": "7.5.10",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
+      "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
       "engines": {
         "node": ">=8.3.0"
       },
@@ -269,9 +269,9 @@
       }
     },
     "node_modules/typescript": {
-      "version": "5.3.3",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
-      "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
+      "version": "5.5.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz",
+      "integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==",
       "dev": true,
       "bin": {
         "tsc": "bin/tsc",
@@ -294,9 +294,9 @@
       "dev": true
     },
     "node_modules/ws": {
-      "version": "8.16.0",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
-      "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
+      "version": "8.17.1",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+      "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
       "engines": {
         "node": ">=10.0.0"
       },
diff --git a/example/graphql-ws-bc/server/package.json b/example/graphql-ws-bc/server/package.json
index 827fa39..fb222ab 100644
--- a/example/graphql-ws-bc/server/package.json
+++ b/example/graphql-ws-bc/server/package.json
@@ -8,14 +8,14 @@
   },
   "license": "MIT",
   "dependencies": {
-    "graphql": "^16.8.1",
-    "graphql-ws": "^5.14.3",
+    "graphql": "^16.9.0",
+    "graphql-ws": "^5.16.0",
     "subscriptions-transport-ws": "^0.11.0",
-    "ws": "^8.16.0"
+    "ws": "^8.17.1"
   },
   "devDependencies": {
     "@types/ws": "^8.5.10",
     "ts-node": "^10.9.2",
-    "typescript": "^5.3.3"
+    "typescript": "^5.5.2"
   }
 }
diff --git a/example/subscription/subscription_test.go b/example/subscription/subscription_test.go
index be3a6e8..336b262 100644
--- a/example/subscription/subscription_test.go
+++ b/example/subscription/subscription_test.go
@@ -8,6 +8,7 @@ import (
 	"log"
 	"net/http"
 	"sync"
+	"sync/atomic"
 	"testing"
 	"time"
 
@@ -370,8 +371,7 @@ func testSubscription_LifeCycleEvents(t *testing.T, syncMode bool) {
 
 	var lock sync.Mutex
 	subscriptionResults := []gql.Subscription{}
-	wasConnected := false
-	wasDisconnected := false
+	var wasConnected, wasDisconnected int32
 	addResult := func(s gql.Subscription) int {
 		lock.Lock()
 		defer lock.Unlock()
@@ -436,20 +436,16 @@ func testSubscription_LifeCycleEvents(t *testing.T, syncMode bool) {
 		WithTimeout(3 * time.Second).
 		WithSyncMode(syncMode).
 		OnConnected(func() {
-			lock.Lock()
-			defer lock.Unlock()
 			log.Println("connected")
-			wasConnected = true
+			atomic.StoreInt32(&wasConnected, 1)
 		}).
 		OnError(func(sc *gql.SubscriptionClient, err error) error {
 			t.Fatalf("got error: %v, want: nil", err)
 			return err
 		}).
 		OnDisconnected(func() {
-			lock.Lock()
-			defer lock.Unlock()
 			log.Println("disconnected")
-			wasDisconnected = true
+			atomic.StoreInt32(&wasDisconnected, 1)
 		}).
 		OnSubscriptionComplete(func(s gql.Subscription) {
 			log.Println("OnSubscriptionComplete: ", s)
@@ -542,10 +538,13 @@ func testSubscription_LifeCycleEvents(t *testing.T, syncMode bool) {
 		}
 	}
 
-	if !wasConnected {
+	// workaround for race condition
+	time.Sleep(time.Second)
+
+	if atomic.LoadInt32(&wasConnected) != 1 {
 		t.Fatalf("expected OnConnected event, got none")
 	}
-	if !wasDisconnected {
+	if atomic.LoadInt32(&wasDisconnected) != 1 {
 		t.Fatalf("expected OnDisconnected event, got none")
 	}
 }
diff --git a/go.mod b/go.mod
index 6910049..03937b1 100644
--- a/go.mod
+++ b/go.mod
@@ -4,5 +4,5 @@ go 1.20
 
 require (
 	github.com/google/uuid v1.6.0
-	nhooyr.io/websocket v1.8.10
+	nhooyr.io/websocket v1.8.11
 )
diff --git a/go.sum b/go.sum
index b14acc6..50b33e2 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,4 @@
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
-nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
+nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0=
+nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
diff --git a/graphql.go b/graphql.go
index 706092d..31bad9d 100644
--- a/graphql.go
+++ b/graphql.go
@@ -47,28 +47,28 @@ func NewClient(url string, httpClient Doer) *Client {
 // Query executes a single GraphQL query request,
 // with a query derived from q, populating the response into it.
 // q should be a pointer to struct that corresponds to the GraphQL schema.
-func (c *Client) Query(ctx context.Context, q interface{}, variables map[string]interface{}, options ...Option) error {
+func (c *Client) Query(ctx context.Context, q any, variables map[string]any, options ...Option) error {
 	return c.do(ctx, queryOperation, q, variables, options...)
 }
 
 // NamedQuery executes a single GraphQL query request, with operation name
 //
 // Deprecated: this is the shortcut of Query method, with NewOperationName option
-func (c *Client) NamedQuery(ctx context.Context, name string, q interface{}, variables map[string]interface{}, options ...Option) error {
+func (c *Client) NamedQuery(ctx context.Context, name string, q any, variables map[string]any, options ...Option) error {
 	return c.do(ctx, queryOperation, q, variables, append(options, OperationName(name))...)
 }
 
 // Mutate executes a single GraphQL mutation request,
 // with a mutation derived from m, populating the response into it.
 // m should be a pointer to struct that corresponds to the GraphQL schema.
-func (c *Client) Mutate(ctx context.Context, m interface{}, variables map[string]interface{}, options ...Option) error {
+func (c *Client) Mutate(ctx context.Context, m any, variables map[string]any, options ...Option) error {
 	return c.do(ctx, mutationOperation, m, variables, options...)
 }
 
 // NamedMutate executes a single GraphQL mutation request, with operation name
 //
 // Deprecated: this is the shortcut of Mutate method, with NewOperationName option
-func (c *Client) NamedMutate(ctx context.Context, name string, m interface{}, variables map[string]interface{}, options ...Option) error {
+func (c *Client) NamedMutate(ctx context.Context, name string, m any, variables map[string]any, options ...Option) error {
 	return c.do(ctx, mutationOperation, m, variables, append(options, OperationName(name))...)
 }
 
@@ -76,13 +76,13 @@ func (c *Client) NamedMutate(ctx context.Context, name string, m interface{}, va
 // with a query derived from q, populating the response into it.
 // q should be a pointer to struct that corresponds to the GraphQL schema.
 // return raw bytes message.
-func (c *Client) QueryRaw(ctx context.Context, q interface{}, variables map[string]interface{}, options ...Option) ([]byte, error) {
+func (c *Client) QueryRaw(ctx context.Context, q any, variables map[string]any, options ...Option) ([]byte, error) {
 	return c.doRaw(ctx, queryOperation, q, variables, options...)
 }
 
 // NamedQueryRaw executes a single GraphQL query request, with operation name
 // return raw bytes message.
-func (c *Client) NamedQueryRaw(ctx context.Context, name string, q interface{}, variables map[string]interface{}, options ...Option) ([]byte, error) {
+func (c *Client) NamedQueryRaw(ctx context.Context, name string, q any, variables map[string]any, options ...Option) ([]byte, error) {
 	return c.doRaw(ctx, queryOperation, q, variables, append(options, OperationName(name))...)
 }
 
@@ -90,18 +90,18 @@ func (c *Client) NamedQueryRaw(ctx context.Context, name string, q interface{},
 // with a mutation derived from m, populating the response into it.
 // m should be a pointer to struct that corresponds to the GraphQL schema.
 // return raw bytes message.
-func (c *Client) MutateRaw(ctx context.Context, m interface{}, variables map[string]interface{}, options ...Option) ([]byte, error) {
+func (c *Client) MutateRaw(ctx context.Context, m any, variables map[string]any, options ...Option) ([]byte, error) {
 	return c.doRaw(ctx, mutationOperation, m, variables, options...)
 }
 
 // NamedMutateRaw executes a single GraphQL mutation request, with operation name
 // return raw bytes message.
-func (c *Client) NamedMutateRaw(ctx context.Context, name string, m interface{}, variables map[string]interface{}, options ...Option) ([]byte, error) {
+func (c *Client) NamedMutateRaw(ctx context.Context, name string, m any, variables map[string]any, options ...Option) ([]byte, error) {
 	return c.doRaw(ctx, mutationOperation, m, variables, append(options, OperationName(name))...)
 }
 
-// buildAndRequest the common method that builds and send graphql request
-func (c *Client) buildAndRequest(ctx context.Context, op operationType, v interface{}, variables map[string]interface{}, options ...Option) ([]byte, *http.Response, io.Reader, Errors) {
+// buildQueryAndOptions the common method to build query and options
+func (c *Client) buildQueryAndOptions(op operationType, v any, variables map[string]any, options ...Option) (string, *constructOptionsOutput, error) {
 	var query string
 	var err error
 	var optionOutput *constructOptionsOutput
@@ -110,17 +110,18 @@ func (c *Client) buildAndRequest(ctx context.Context, op operationType, v interf
 		query, optionOutput, err = constructQuery(v, variables, options...)
 	case mutationOperation:
 		query, optionOutput, err = constructMutation(v, variables, options...)
+	default:
+		err = fmt.Errorf("invalid operation type: %v", op)
 	}
 
 	if err != nil {
-		return nil, nil, nil, Errors{newError(ErrGraphQLEncode, err)}
+		return "", nil, Errors{newError(ErrGraphQLEncode, err)}
 	}
-
-	return c.request(ctx, query, variables, optionOutput)
+	return query, optionOutput, nil
 }
 
 // Request the common method that send graphql request
-func (c *Client) request(ctx context.Context, query string, variables map[string]interface{}, options *constructOptionsOutput) ([]byte, *http.Response, io.Reader, Errors) {
+func (c *Client) request(ctx context.Context, query string, variables map[string]any, options *constructOptionsOutput) ([]byte, []byte, *http.Response, io.Reader, Errors) {
 	in := GraphQLRequestPayload{
 		Query:     query,
 		Variables: variables,
@@ -133,7 +134,7 @@ func (c *Client) request(ctx context.Context, query string, variables map[string
 	var buf bytes.Buffer
 	err := json.NewEncoder(&buf).Encode(in)
 	if err != nil {
-		return nil, nil, nil, Errors{newError(ErrGraphQLEncode, err)}
+		return nil, nil, nil, nil, Errors{newError(ErrGraphQLEncode, err)}
 	}
 
 	reqReader := bytes.NewReader(buf.Bytes())
@@ -143,7 +144,7 @@ func (c *Client) request(ctx context.Context, query string, variables map[string
 		if c.debug {
 			e = e.withRequest(request, reqReader)
 		}
-		return nil, nil, nil, Errors{e}
+		return nil, nil, nil, nil, Errors{e}
 	}
 	request.Header.Add("Content-Type", "application/json")
 
@@ -162,7 +163,7 @@ func (c *Client) request(ctx context.Context, query string, variables map[string
 		if c.debug {
 			e = e.withRequest(request, reqReader)
 		}
-		return nil, nil, nil, Errors{e}
+		return nil, nil, nil, nil, Errors{e}
 	}
 	defer resp.Body.Close()
 
@@ -171,25 +172,29 @@ func (c *Client) request(ctx context.Context, query string, variables map[string
 	if resp.Header.Get("Content-Encoding") == "gzip" {
 		gr, err := gzip.NewReader(r)
 		if err != nil {
-			return nil, nil, nil, Errors{newError(ErrJsonDecode, fmt.Errorf("problem trying to create gzip reader: %w", err))}
+			return nil, nil, nil, nil, Errors{newError(ErrJsonDecode, fmt.Errorf("problem trying to create gzip reader: %w", err))}
 		}
 		defer gr.Close()
 		r = gr
 	}
 
 	if resp.StatusCode != http.StatusOK {
-		body, _ := io.ReadAll(resp.Body)
-		err := newError(ErrRequestError, fmt.Errorf("%v; body: %q", resp.Status, body))
+		b, _ := io.ReadAll(resp.Body)
+		err := newError(ErrRequestError, NetworkError{
+			statusCode: resp.StatusCode,
+			body:       string(b),
+		})
 
 		if c.debug {
 			err = err.withRequest(request, reqReader)
 		}
-		return nil, nil, nil, Errors{err}
+		return nil, nil, nil, nil, Errors{err}
 	}
 
 	var out struct {
-		Data   *json.RawMessage
-		Errors Errors
+		Data       *json.RawMessage
+		Extensions *json.RawMessage
+		Errors     Errors
 	}
 
 	// copy the response reader for debugging
@@ -197,7 +202,7 @@ func (c *Client) request(ctx context.Context, query string, variables map[string
 	if c.debug {
 		body, err := io.ReadAll(resp.Body)
 		if err != nil {
-			return nil, nil, nil, Errors{newError(ErrJsonDecode, err)}
+			return nil, nil, nil, nil, Errors{newError(ErrJsonDecode, err)}
 		}
 		respReader = bytes.NewReader(body)
 		r = io.NopCloser(respReader)
@@ -215,7 +220,7 @@ func (c *Client) request(ctx context.Context, query string, variables map[string
 			we = we.withRequest(request, reqReader).
 				withResponse(resp, respReader)
 		}
-		return nil, nil, nil, Errors{we}
+		return nil, nil, nil, nil, Errors{we}
 	}
 
 	var rawData []byte
@@ -223,6 +228,11 @@ func (c *Client) request(ctx context.Context, query string, variables map[string
 		rawData = []byte(*out.Data)
 	}
 
+	var extensions []byte
+	if out.Extensions != nil && len(*out.Extensions) > 0 {
+		extensions = []byte(*out.Extensions)
+	}
+
 	if len(out.Errors) > 0 {
 		if c.debug && (out.Errors[0].Extensions == nil || out.Errors[0].Extensions["request"] == nil) {
 			out.Errors[0] = out.Errors[0].
@@ -230,56 +240,82 @@ func (c *Client) request(ctx context.Context, query string, variables map[string
 				withResponse(resp, respReader)
 		}
 
-		return rawData, resp, respReader, out.Errors
+		return rawData, extensions, resp, respReader, out.Errors
 	}
 
-	return rawData, resp, respReader, nil
+	return rawData, extensions, resp, respReader, nil
 }
 
 // do executes a single GraphQL operation.
 // return raw message and error
-func (c *Client) doRaw(ctx context.Context, op operationType, v interface{}, variables map[string]interface{}, options ...Option) ([]byte, error) {
-	data, _, _, err := c.buildAndRequest(ctx, op, v, variables, options...)
-	if len(err) > 0 {
-		return data, err
+func (c *Client) doRaw(ctx context.Context, op operationType, v any, variables map[string]any, options ...Option) ([]byte, error) {
+	query, optionsOutput, err := c.buildQueryAndOptions(op, v, variables, options...)
+	if err != nil {
+		return nil, err
 	}
+	data, _, _, _, errs := c.request(ctx, query, variables, optionsOutput)
+	if len(errs) > 0 {
+		return data, errs
+	}
+
 	return data, nil
 }
 
 // do executes a single GraphQL operation and unmarshal json.
-func (c *Client) do(ctx context.Context, op operationType, v interface{}, variables map[string]interface{}, options ...Option) error {
-	data, resp, respBuf, errs := c.buildAndRequest(ctx, op, v, variables, options...)
-	return c.processResponse(v, data, resp, respBuf, errs)
+func (c *Client) do(ctx context.Context, op operationType, v any, variables map[string]any, options ...Option) error {
+	query, optionsOutput, err := c.buildQueryAndOptions(op, v, variables, options...)
+	if err != nil {
+		return err
+	}
+	data, extData, resp, respBuf, errs := c.request(ctx, query, variables, optionsOutput)
+
+	return c.processResponse(v, data, optionsOutput.extensions, extData, resp, respBuf, errs)
 }
 
 // Executes a pre-built query and unmarshals the response into v. Unlike the Query method you have to specify in the query the
 // fields that you want to receive as they are not inferred from v. This method is useful if you need to build the query dynamically.
-func (c *Client) Exec(ctx context.Context, query string, v interface{}, variables map[string]interface{}, options ...Option) error {
+func (c *Client) Exec(ctx context.Context, query string, v any, variables map[string]any, options ...Option) error {
 	optionsOutput, err := constructOptions(options)
 	if err != nil {
 		return err
 	}
 
-	data, resp, respBuf, errs := c.request(ctx, query, variables, optionsOutput)
-	return c.processResponse(v, data, resp, respBuf, errs)
+	data, extData, resp, respBuf, errs := c.request(ctx, query, variables, optionsOutput)
+	return c.processResponse(v, data, optionsOutput.extensions, extData, resp, respBuf, errs)
 }
 
 // Executes a pre-built query and returns the raw json message. Unlike the Query method you have to specify in the query the
 // fields that you want to receive as they are not inferred from the interface. This method is useful if you need to build the query dynamically.
-func (c *Client) ExecRaw(ctx context.Context, query string, variables map[string]interface{}, options ...Option) ([]byte, error) {
+func (c *Client) ExecRaw(ctx context.Context, query string, variables map[string]any, options ...Option) ([]byte, error) {
 	optionsOutput, err := constructOptions(options)
 	if err != nil {
 		return nil, err
 	}
 
-	data, _, _, errs := c.request(ctx, query, variables, optionsOutput)
+	data, _, _, _, errs := c.request(ctx, query, variables, optionsOutput)
 	if len(errs) > 0 {
 		return data, errs
 	}
 	return data, nil
 }
 
-func (c *Client) processResponse(v interface{}, data []byte, resp *http.Response, respBuf io.Reader, errs Errors) error {
+// ExecRawWithExtensions execute a pre-built query and returns the raw json message and a map with extensions (values also as raw json objects). Unlike the
+// Query method you have to specify in the query the fields that you want to receive as they are not inferred from the interface. This method
+// is useful if you need to build the query dynamically.
+func (c *Client) ExecRawWithExtensions(ctx context.Context, query string, variables map[string]any, options ...Option) ([]byte, []byte, error) {
+	optionsOutput, err := constructOptions(options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	data, ext, _, _, errs := c.request(ctx, query, variables, optionsOutput)
+	if len(errs) > 0 {
+		return data, ext, errs
+	}
+	return data, ext, nil
+}
+
+func (c *Client) processResponse(v any, data []byte, extensions any, rawExtensions []byte, resp *http.Response, respBuf io.Reader, errs Errors) error {
 	if len(data) > 0 {
 		err := jsonutil.UnmarshalGraphQL(data, v)
 		if err != nil {
@@ -291,6 +327,14 @@ func (c *Client) processResponse(v interface{}, data []byte, resp *http.Response
 		}
 	}
 
+	if len(rawExtensions) > 0 && extensions != nil {
+		err := json.Unmarshal(rawExtensions, extensions)
+		if err != nil {
+			we := newError(ErrGraphQLExtensionsDecode, err)
+			errs = append(errs, we)
+		}
+	}
+
 	if len(errs) > 0 {
 		return errs
 	}
@@ -326,13 +370,13 @@ func (c *Client) WithDebug(debug bool) *Client {
 type Errors []Error
 
 type Error struct {
-	Message    string                 `json:"message"`
-	Extensions map[string]interface{} `json:"extensions"`
+	Message    string         `json:"message"`
+	Extensions map[string]any `json:"extensions"`
 	Locations  []struct {
 		Line   int `json:"line"`
 		Column int `json:"column"`
 	} `json:"locations"`
-	Path []interface{} `json:"path"`
+	Path []any `json:"path"`
 	err  error
 }
 
@@ -364,42 +408,59 @@ func (e Errors) Unwrap() []error {
 	return errs
 }
 
-func (e Error) getInternalExtension() map[string]interface{} {
+func (e Error) getInternalExtension() map[string]any {
 	if e.Extensions == nil {
-		return make(map[string]interface{})
+		return make(map[string]any)
 	}
 
 	if ex, ok := e.Extensions["internal"]; ok {
-		return ex.(map[string]interface{})
+		return ex.(map[string]any)
 	}
 
-	return make(map[string]interface{})
+	return make(map[string]any)
 }
 
 func newError(code string, err error) Error {
 	return Error{
 		Message: err.Error(),
-		Extensions: map[string]interface{}{
+		Extensions: map[string]any{
 			"code": code,
 		},
 		err: err,
 	}
 }
 
+type NetworkError struct {
+	body       string
+	statusCode int
+}
+
+func (e NetworkError) Error() string {
+	return fmt.Sprintf("%d %s", e.statusCode, http.StatusText(e.statusCode))
+}
+
+func (e NetworkError) Body() string {
+	return e.body
+}
+
+func (e NetworkError) StatusCode() int {
+	return e.statusCode
+}
+
 func (e Error) withRequest(req *http.Request, bodyReader io.Reader) Error {
 	internal := e.getInternalExtension()
 	bodyBytes, err := io.ReadAll(bodyReader)
 	if err != nil {
 		internal["error"] = err
 	} else {
-		internal["request"] = map[string]interface{}{
+		internal["request"] = map[string]any{
 			"headers": req.Header,
 			"body":    string(bodyBytes),
 		}
 	}
 
 	if e.Extensions == nil {
-		e.Extensions = make(map[string]interface{})
+		e.Extensions = make(map[string]any)
 	}
 	e.Extensions["internal"] = internal
 	return e
@@ -407,16 +468,20 @@ func (e Error) withRequest(req *http.Request, bodyReader io.Reader) Error {
 
 func (e Error) withResponse(res *http.Response, bodyReader io.Reader) Error {
 	internal := e.getInternalExtension()
-	bodyBytes, err := io.ReadAll(bodyReader)
-	if err != nil {
-		internal["error"] = err
-	} else {
-		internal["response"] = map[string]interface{}{
-			"headers": res.Header,
-			"body":    string(bodyBytes),
-		}
+
+	response := map[string]any{
+		"headers": res.Header,
 	}
 
+	if bodyReader != nil {
+		bodyBytes, err := io.ReadAll(bodyReader)
+		if err != nil {
+			internal["error"] = err
+		} else {
+			response["body"] = string(bodyBytes)
+		}
+	}
+	internal["response"] = response
 	e.Extensions["internal"] = internal
 	return e
 }
@@ -427,7 +492,7 @@ func (e Error) withResponse(res *http.Response, bodyReader io.Reader) Error {
 // The implementation is created on top of the JSON tokenizer available
 // in "encoding/json".Decoder.
 // This function is re-exported from the internal package
-func UnmarshalGraphQL(data []byte, v interface{}) error {
+func UnmarshalGraphQL(data []byte, v any) error {
 	return jsonutil.UnmarshalGraphQL(data, v)
 }
 
@@ -438,9 +503,10 @@ const (
 	mutationOperation
 	// subscriptionOperation // Unused.
 
-	ErrRequestError  = "request_error"
-	ErrJsonEncode    = "json_encode_error"
-	ErrJsonDecode    = "json_decode_error"
-	ErrGraphQLEncode = "graphql_encode_error"
-	ErrGraphQLDecode = "graphql_decode_error"
+	ErrRequestError            = "request_error"
+	ErrJsonEncode              = "json_encode_error"
+	ErrJsonDecode              = "json_decode_error"
+	ErrGraphQLEncode           = "graphql_encode_error"
+	ErrGraphQLDecode           = "graphql_decode_error"
+	ErrGraphQLExtensionsDecode = "graphql_extensions_decode_error"
 )
diff --git a/graphql_test.go b/graphql_test.go
index 131b591..25b9771 100644
--- a/graphql_test.go
+++ b/graphql_test.go
@@ -217,7 +217,7 @@ func TestClient_Query_errorStatusCode(t *testing.T) {
 	if err == nil {
 		t.Fatal("got error: nil, want: non-nil")
 	}
-	if got, want := err.Error(), `Message: 500 Internal Server Error; body: "important message\n", Locations: [], Extensions: map[code:request_error], Path: []`; got != want {
+	if got, want := err.Error(), `Message: 500 Internal Server Error, Locations: [], Extensions: map[code:request_error], Path: []`; got != want {
 		t.Errorf("got error: %v, want: %v", got, want)
 	}
 	if q.User.Name != "" {
@@ -242,7 +242,7 @@ func TestClient_Query_errorStatusCode(t *testing.T) {
 		t.Errorf("the error type should be graphql.Errors")
 	}
 	gqlErr = err.(graphql.Errors)
-	if got, want := gqlErr[0].Message, `500 Internal Server Error; body: "important message\n"`; got != want {
+	if got, want := gqlErr[0].Message, `500 Internal Server Error`; got != want {
 		t.Errorf("got error: %v, want: %v", got, want)
 	}
 	if got, want := gqlErr[0].Extensions["code"], graphql.ErrRequestError; got != want {
@@ -472,9 +472,99 @@ func TestClient_Exec_QueryRaw(t *testing.T) {
 	}
 }
 
+func TestClient_BindExtensions(t *testing.T) {
+	mux := http.NewServeMux()
+	mux.HandleFunc("/graphql", func(w http.ResponseWriter, req *http.Request) {
+		body := mustRead(req.Body)
+		if got, want := body, `{"query":"{user{id,name}}"}`+"\n"; got != want {
+			t.Errorf("got body: %v, want %v", got, want)
+		}
+		w.Header().Set("Content-Type", "application/json")
+		mustWrite(w, `{"data": {"user": {"name": "Gopher"}}, "extensions": {"id": 1, "domain": "users"}}`)
+	})
+	client := graphql.NewClient("/graphql", &http.Client{Transport: localRoundTripper{handler: mux}})
+
+	var q struct {
+		User struct {
+			ID   string `graphql:"id"`
+			Name string `graphql:"name"`
+		}
+	}
+
+	var ext struct {
+		ID     int    `json:"id"`
+		Domain string `json:"domain"`
+	}
+
+	err := client.Query(context.Background(), &q, map[string]interface{}{})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if got, want := q.User.Name, "Gopher"; got != want {
+		t.Fatalf("got q.User.Name: %q, want: %q", got, want)
+	}
+
+	err = client.Query(context.Background(), &q, map[string]interface{}{}, graphql.BindExtensions(&ext))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if got, want := q.User.Name, "Gopher"; got != want {
+		t.Fatalf("got q.User.Name: %q, want: %q", got, want)
+	}
+
+	if got, want := ext.ID, 1; got != want {
+		t.Errorf("got ext.ID: %q, want: %q", got, want)
+	}
+	if got, want := ext.Domain, "users"; got != want {
+		t.Errorf("got ext.Domain: %q, want: %q", got, want)
+	}
+}
+
+// Test exec pre-built query, return raw json string and map
+// with extensions
+func TestClient_Exec_QueryRawWithExtensions(t *testing.T) {
+	mux := http.NewServeMux()
+	mux.HandleFunc("/graphql", func(w http.ResponseWriter, req *http.Request) {
+		body := mustRead(req.Body)
+		if got, want := body, `{"query":"{user{id,name}}"}`+"\n"; got != want {
+			t.Errorf("got body: %v, want %v", got, want)
+		}
+		w.Header().Set("Content-Type", "application/json")
+		mustWrite(w, `{"data": {"user": {"name": "Gopher"}}, "extensions": {"id": 1, "domain": "users"}}`)
+	})
+	client := graphql.NewClient("/graphql", &http.Client{Transport: localRoundTripper{handler: mux}})
+
+	var ext struct {
+		ID     int    `json:"id"`
+		Domain string `json:"domain"`
+	}
+
+	_, extensions, err := client.ExecRawWithExtensions(context.Background(), "{user{id,name}}", map[string]interface{}{})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if got := extensions; got == nil {
+		t.Errorf("got nil extensions: %q, want: non-nil", got)
+	}
+
+	err = json.Unmarshal(extensions, &ext)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if got, want := ext.ID, 1; got != want {
+		t.Errorf("got ext.ID: %q, want: %q", got, want)
+	}
+	if got, want := ext.Domain, "users"; got != want {
+		t.Errorf("got ext.Domain: %q, want: %q", got, want)
+	}
+}
+
 // Issue: https://github.com/hasura/go-graphql-client/issues/139
 func TestUnmarshalGraphQL_unionSlice(t *testing.T) {
-
 	expectedQuery := `query($cursor0: String, $searchQuery: String!) {
 		search(type: ISSUE, query: $searchQuery, first: 100, after: $cursor0) {
 			pageInfo {
@@ -602,6 +692,139 @@ func TestUnmarshalGraphQL_unionSlice(t *testing.T) {
 	}
 }
 
+// Issue: https://github.com/hasura/go-graphql-client/issues/152
+func TestUnmarshalGraphQL_shopifyAdminAPI(t *testing.T) {
+
+	type testQuery struct {
+		Orders struct {
+			Edges []struct {
+				Cursor string
+				Node   struct {
+					ID                       string
+					Test                     bool
+					Name                     string
+					Email                    *string
+					DisplayFinancialStatus   string
+					DisplayFulfillmentStatus string
+					ReturnStatus             string
+					Note                     string
+					ClientIP                 string
+					Closed                   bool
+					ClosedAt                 *time.Time
+					CancelledAt              *time.Time
+					CustomAttributes         []struct {
+						Key   string
+						Value string
+					}
+					Customer struct {
+						Email string
+					}
+				}
+			}
+			PageInfo struct {
+				EndCursor   string
+				HasNextPage bool
+			}
+		} `graphql:"orders(first: $first, after: $after)"`
+	}
+
+	mux := http.NewServeMux()
+	mux.HandleFunc("/graphql", func(w http.ResponseWriter, req *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+		mustWrite(w, `{
+			"data": {
+				"orders": {
+					"edges": [
+						{
+							"cursor": "x==",
+							"node": {
+								"id": "gid://shopify/Order/x",
+								"test": false,
+								"name": "#1004",
+								"email": null,
+								"displayFinancialStatus": "PAID",
+								"displayFulfillmentStatus": "UNFULFILLED",
+								"returnStatus": "NO_RETURN",
+								"note": "unfulfilled",
+								"clientIp": "x",
+								"closed": false,
+								"closedAt": null,
+								"cancelledAt": null,
+								"customAttributes": [],
+								"customer": null
+							}
+						},
+						{
+							"cursor": "x==",
+							"node": {
+								"id": "gid://shopify/Order/x",
+								"test": false,
+								"name": "#1005",
+								"email": null,
+								"displayFinancialStatus": "REFUNDED",
+								"displayFulfillmentStatus": "FULFILLED",
+								"returnStatus": "NO_RETURN",
+								"note": "fulfilled then refunded (not returned)",
+								"clientIp": "x",
+								"closed": true,
+								"closedAt": "2024-08-29T02:07:04Z",
+								"cancelledAt": null,
+								"customAttributes": [],
+								"customer": null
+							}
+						},
+						{
+							"cursor": "x==",
+							"node": {
+								"id": "gid://shopify/Order/x",
+								"test": false,
+								"name": "#1006",
+								"email": null,
+								"displayFinancialStatus": "PAID",
+								"displayFulfillmentStatus": "FULFILLED",
+								"returnStatus": "IN_PROGRESS",
+								"note": "fulfulled and return in progress",
+								"clientIp": "x",
+								"closed": false,
+								"closedAt": null,
+								"cancelledAt": null,
+								"customAttributes": [],
+								"customer": null
+							}
+						}
+					],
+					"pageInfo": {
+						"endCursor": "x==",
+						"hasNextPage": false
+					}
+				}
+			},
+			"extensions": {
+				"cost": {
+					"requestedQueryCost": 8,
+					"actualQueryCost": 8,
+					"throttleStatus": {
+						"maximumAvailable": 2000.0,
+						"currentlyAvailable": 1992,
+						"restoreRate": 100.0
+					}
+				}
+			}
+		}`)
+	})
+	client := graphql.NewClient("/graphql", &http.Client{Transport: localRoundTripper{handler: mux}})
+
+	var got testQuery
+
+	err := client.Query(context.Background(), &got, map[string]interface{}{
+		"first": 10,
+		"after": "test",
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
 // localRoundTripper is an http.RoundTripper that executes HTTP transactions
 // by using handler directly, instead of going over an HTTP connection.
 type localRoundTripper struct {
diff --git a/option.go b/option.go
index 88ac865..b89d7f9 100644
--- a/option.go
+++ b/option.go
@@ -4,8 +4,6 @@ package graphql
 type OptionType string
 
 const (
-	// optionTypeOperationName is private because it's option is built-in and unique
-	optionTypeOperationName      OptionType = "operation_name"
 	OptionTypeOperationDirective OptionType = "operation_directive"
 )
 
@@ -15,8 +13,6 @@ type Option interface {
 	// Type returns the supported type of the renderer
 	// available types: operation_name and operation_directive
 	Type() OptionType
-	// String returns the query component string
-	String() string
 }
 
 // operationNameOption represents the operation name render component
@@ -25,7 +21,7 @@ type operationNameOption struct {
 }
 
 func (ono operationNameOption) Type() OptionType {
-	return optionTypeOperationName
+	return "operation_name"
 }
 
 func (ono operationNameOption) String() string {
@@ -36,3 +32,17 @@ func (ono operationNameOption) String() string {
 func OperationName(name string) Option {
 	return operationNameOption{name}
 }
+
+// bind the struct pointer to decode extensions from response
+type bindExtensionsOption struct {
+	value any
+}
+
+func (ono bindExtensionsOption) Type() OptionType {
+	return "bind_extensions"
+}
+
+// BindExtensions bind the struct pointer to decode extensions from json response
+func BindExtensions(value any) Option {
+	return bindExtensionsOption{value: value}
+}
diff --git a/query.go b/query.go
index dfd6edc..0954cce 100644
--- a/query.go
+++ b/query.go
@@ -16,6 +16,7 @@ import (
 type constructOptionsOutput struct {
 	operationName       string
 	operationDirectives []string
+	extensions          any
 }
 
 func (coo constructOptionsOutput) OperationDirectivesString() string {
@@ -30,13 +31,21 @@ func constructOptions(options []Option) (*constructOptionsOutput, error) {
 	output := &constructOptionsOutput{}
 
 	for _, option := range options {
-		switch option.Type() {
-		case optionTypeOperationName:
-			output.operationName = option.String()
-		case OptionTypeOperationDirective:
-			output.operationDirectives = append(output.operationDirectives, option.String())
+		switch opt := option.(type) {
+		case operationNameOption:
+			output.operationName = opt.name
+		case bindExtensionsOption:
+			output.extensions = opt.value
 		default:
-			return nil, fmt.Errorf("invalid query option type: %s", option.Type())
+			if opt.Type() != OptionTypeOperationDirective {
+				return nil, fmt.Errorf("invalid query option type: %s", option.Type())
+			}
+			if d, ok := option.(fmt.Stringer); ok {
+				output.operationDirectives = append(output.operationDirectives, d.String())
+			} else {
+				return nil, fmt.Errorf("please implement the fmt.Stringer interface for %s option", OptionTypeOperationDirective)
+			}
+
 		}
 	}
 
@@ -234,7 +243,7 @@ func writeQuery(w io.Writer, t reflect.Type, v reflect.Value, inline bool) error
 		}
 	case reflect.Struct:
 		// If the type implements json.Unmarshaler, it's a scalar. Don't expand it.
-		if reflect.PtrTo(t).Implements(jsonUnmarshaler) {
+		if reflect.PointerTo(t).Implements(jsonUnmarshaler) {
 			return nil
 		}
 		if t.AssignableTo(idType) {
diff --git a/query_test.go b/query_test.go
index e73de3a..6263772 100644
--- a/query_test.go
+++ b/query_test.go
@@ -342,13 +342,15 @@ func TestConstructQuery(t *testing.T) {
 			want: `{viewer{login,databaseId}}`,
 		},
 	}
-	for _, tc := range tests {
-		got, err := ConstructQuery(tc.inV, tc.inVariables, tc.options...)
-		if err != nil {
-			t.Error(err)
-		} else if got != tc.want {
-			t.Errorf("\ngot:  %q\nwant: %q\n", got, tc.want)
-		}
+	for i, tc := range tests {
+		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+			got, err := ConstructQuery(tc.inV, tc.inVariables, tc.options...)
+			if err != nil {
+				t.Error(err)
+			} else if got != tc.want {
+				t.Errorf("\ngot:  %q\nwant: %q\n", got, tc.want)
+			}
+		})
 	}
 }