Repository: elastic/elasticsearch-js
Pull request #1415 (2021-03-10 10:57:09)

fix(TransportRequestPromise): add finally method to TransportRequestP…
Closes: #1393

elasticmachine: Since this is a community submitted pull request, a Jenkins build has not been kicked off automatically. Can an Elastic organization member please verify the contents of this patch and then kick off a build manually?


Patch:
diff --git a/.gitignore b/.gitignore
index 4e5d6608..6de1b46b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -60,3 +60,4 @@ test/benchmarks/macro/fixtures/*
 .cache
 
 test/bundlers/**/bundle.js
+test/bundlers/parcel-test/.parcel-cache
diff --git a/docs/advanced-config.asciidoc b/docs/advanced-config.asciidoc
index 001c7743..e5a64ea1 100644
--- a/docs/advanced-config.asciidoc
+++ b/docs/advanced-config.asciidoc
@@ -4,45 +4,11 @@
 If you need to customize the client behavior heavily, you are in the right 
 place! The client enables you to customize the following internals:
 
-* `Transport` class
 * `ConnectionPool` class
 * `Connection` class
 * `Serializer` class
 
-
-[discrete]
-==== `Transport`
-
-This class is responsible for performing the request to {es} and handling 
-errors, it also handles the sniffing.
-
-[source,js]
-----
-const { Client, Transport } = require('@elastic/elasticsearch')
-
-class MyTransport extends Transport {
-  request (params, options, callback) {
-    // your code
-  }
-}
-
-const client = new Client({
-    Transport: MyTransport
-})
-----
-
-Sometimes you need to inject a small snippet of your code and then continue to 
-use the usual client code. In such cases, call `super.method`:
-
-[source,js]
-----
-class MyTransport extends Transport {
-  request (params, options, callback) {
-    // your code
-    return super.request(params, options, callback)
-  }
-}
-----
+NOTE: For information about the `Transport` class, refer to <<transport>>.
 
 
 [discrete]
diff --git a/docs/basic-config.asciidoc b/docs/basic-config.asciidoc
index a6a4b41d..38ed7c1d 100644
--- a/docs/basic-config.asciidoc
+++ b/docs/basic-config.asciidoc
@@ -244,4 +244,8 @@ const client = new Client({
 })
 ----
 
+|`disablePrototypePoisoningProtection`
+|`boolean`, `'proto'`, `'constructor'` - By the default the client will protect you against prototype poisoning attacks. Read https://web.archive.org/web/20200319091159/https://hueniverse.com/square-brackets-are-the-enemy-ff5b9fd8a3e8?gi=184a27ee2a08[this article] to learn more. If needed you can disable prototype poisoning protection entirely or one of the two checks. Read the `secure-json-parse` https://github.com/fastify/secure-json-parse[documentation] to learn more. +
+_Default:_ `false`
+
 |===
\ No newline at end of file
diff --git a/docs/configuration.asciidoc b/docs/configuration.asciidoc
index cca46ece..2cd2114a 100644
--- a/docs/configuration.asciidoc
+++ b/docs/configuration.asciidoc
@@ -9,3 +9,4 @@ section, you can see the possible options that you can use to configure it.
 * <<advanced-config>>
 * <<child>>
 * <<extend>>
+* <<client-testing>>
diff --git a/docs/index.asciidoc b/docs/index.asciidoc
index cc70ef5a..7df4ba84 100644
--- a/docs/index.asciidoc
+++ b/docs/index.asciidoc
@@ -11,10 +11,12 @@ include::basic-config.asciidoc[]
 include::advanced-config.asciidoc[]
 include::child.asciidoc[]
 include::extend.asciidoc[]
+include::testing.asciidoc[]
+include::integrations.asciidoc[]
+include::observability.asciidoc[]
+include::transport.asciidoc[]
+include::typescript.asciidoc[]
 include::reference.asciidoc[]
 include::breaking-changes.asciidoc[]
-include::observability.asciidoc[]
 include::helpers.asciidoc[]
-include::typescript.asciidoc[]
-include::testing.asciidoc[]
 include::examples/index.asciidoc[]
diff --git a/docs/integrations.asciidoc b/docs/integrations.asciidoc
new file mode 100644
index 00000000..84f854ab
--- /dev/null
+++ b/docs/integrations.asciidoc
@@ -0,0 +1,8 @@
+[[integrations]]
+== Integrations
+
+The Client offers the following integration options for you:
+
+* <<observability>>
+* <<transport>>
+* <<typescript>>
\ No newline at end of file
diff --git a/docs/observability.asciidoc b/docs/observability.asciidoc
index 2d4a3666..66c45fb4 100644
--- a/docs/observability.asciidoc
+++ b/docs/observability.asciidoc
@@ -1,19 +1,19 @@
 [[observability]]
-== Observability
+=== Observability
 
 The client does not provide a default logger, but instead it offers an event 
 emitter interfaces to hook into internal events, such as `request` and 
 `response`.
 
-Correlating those events can be quite hard, especially if your applications have 
-a large codebase with many events happening at the same time.
+Correlating those events can be hard, especially if your applications have a 
+large codebase with many events happening at the same time.
 
 To help you with this, the client offers you a correlation id system and other 
 features. Let's see them in action.
 
 
 [discrete]
-=== Events
+==== Events
 
 The client is an event emitter, this means that you can listen for its event and 
 add additional logic to your code, without need to change the client internals 
@@ -105,7 +105,8 @@ client.on('resurrect', (err, result) => {
 
 |===
 
-The values of `result` in `serialization`, `request`, `deserialization`, `response` and `sniff` will be:
+The values of `result` in `serialization`, `request`, `deserialization`, 
+`response` and `sniff` are:
 
 [source,ts]
 ----
@@ -132,7 +133,7 @@ meta: {
 ----
 
 
-While the `result` value in `resurrect` will be:
+While the `result` value in `resurrect` is:
 
 [source,ts]
 ----
@@ -146,10 +147,13 @@ request: {
 ----
 
 [discrete]
-==== Events order
+===== Events order
 
-The event order is described in the following graph, in some edge cases, the order is not guaranteed.
-You can find in https://github.com/elastic/elasticsearch-js/blob/master/test/acceptance/events-order.test.js[`test/acceptance/events-order.test.js`] how the order changes based on the situation.
+The event order is described in the following graph, in some edge cases, the 
+order is not guaranteed.
+You can find in 
+https://github.com/elastic/elasticsearch-js/blob/master/test/acceptance/events-order.test.js[`test/acceptance/events-order.test.js`] 
+how the order changes based on the situation.
 
 [source]
 ----
@@ -170,11 +174,11 @@ serialization
 
 
 [discrete]
-=== Correlation id
+==== Correlation id
 
-Correlating events can be quite hard, especially if there are many events at the 
-same time. The client offers you an automatic (and configurable) system to help 
-you handle this problem.
+Correlating events can be hard, especially if there are many events at the same 
+time. The client offers you an automatic (and configurable) system to help you 
+handle this problem.
 
 [source,js]
 ----
@@ -204,8 +208,8 @@ client.search({
 ----
 
 
-By default the id is an incremental integer, but you can easily configure that 
-with the `generateRequestId` option:
+By default the id is an incremental integer, but you can configure it with the 
+`generateRequestId` option:
 
 [source,js]
 ----
@@ -238,7 +242,7 @@ client.search({
 
 
 [discrete]
-=== Context object
+==== Context object
 
 Sometimes, you might need to make some custom data available in your events, you 
 can do that via the `context` option of a request: 
@@ -275,8 +279,8 @@ client.search({
 ----
 
 The context object can also be configured as a global option in the client
-configuration. If you provide both, the two context object will be shallow merged,
-and the API level object will take precedece.
+configuration. If you provide both, the two context objects will be shallow 
+merged, and the API level object will take precedence.
 
 [source,js]
 ----
@@ -314,12 +318,12 @@ client.search({
 
 
 [discrete]
-=== Client name
+==== Client name
 
 If you are using multiple instances of the client or if you are using multiple 
 child clients _(which is the recommended way to have multiple instances of the 
 client)_, you might need to recognize which client you are using. The `name` 
-options will help you in this regard.
+options help you in this regard.
 
 [source,js]
 ----
@@ -368,13 +372,13 @@ child.search({
 
 
 [discrete]
-=== X-Opaque-Id support
+==== X-Opaque-Id support
 
-To improve the overall observability, the client offers an easy way to configure 
-the `X-Opaque-Id` header. If you set the `X-Opaque-Id` in a specific request, 
-this will allow you to discover this identifier in the 
+To improve observability, the client offers an easy way to configure the 
+`X-Opaque-Id` header. If you set the `X-Opaque-Id` in a specific request, this 
+allows you to discover this identifier in the 
 https://www.elastic.co/guide/en/elasticsearch/reference/master/logging.html#deprecation-logging[deprecation logs], 
-help you with https://www.elastic.co/guide/en/elasticsearch/reference/master/index-modules-slowlog.html#_identifying_search_slow_log_origin[identifying search slow log origin] 
+helps you with https://www.elastic.co/guide/en/elasticsearch/reference/master/index-modules-slowlog.html#_identifying_search_slow_log_origin[identifying search slow log origin] 
 as well as https://www.elastic.co/guide/en/elasticsearch/reference/master/tasks.html#_identifying_running_tasks[identifying running tasks].
 
 The `X-Opaque-Id` should be configured in each request, for doing that you can 
diff --git a/docs/transport.asciidoc b/docs/transport.asciidoc
new file mode 100644
index 00000000..f4fc722c
--- /dev/null
+++ b/docs/transport.asciidoc
@@ -0,0 +1,34 @@
+[[transport]]
+=== Transport
+
+This class is responsible for performing the request to {es} and handling 
+errors, it also handles sniffing.
+
+[source,js]
+----
+const { Client, Transport } = require('@elastic/elasticsearch')
+
+class MyTransport extends Transport {
+  request (params, options, callback) {
+    // your code
+  }
+}
+
+const client = new Client({
+    Transport: MyTransport
+})
+----
+
+Sometimes you need to inject a small snippet of your code and then continue to 
+use the usual client code. In such cases, call `super.method`:
+
+[source,js]
+----
+class MyTransport extends Transport {
+  request (params, options, callback) {
+    // your code
+    return super.request(params, options, callback)
+  }
+}
+----
+
diff --git a/docs/typescript.asciidoc b/docs/typescript.asciidoc
index ffccc49c..a2b29e6c 100644
--- a/docs/typescript.asciidoc
+++ b/docs/typescript.asciidoc
@@ -1,17 +1,23 @@
 [[typescript]]
-== TypeScript support
+=== TypeScript support
 
 The client offers a first-class support for TypeScript, since it ships the type 
 definitions for every exposed API.
 
-NOTE: If you are using TypeScript you will be required to use _snake_case_ style 
-to define the API parameters instead of _camelCase_. 
+NOTE: If you are using TypeScript you need to use _snake_case_ style to define 
+the API parameters instead of _camelCase_. 
 
-By default event API uses https://www.typescriptlang.org/docs/handbook/generics.html[generics] to specify the requests and response bodies and the `meta.context`. Currently we can't provide those definitions, but we are working to improve this situation.
+By default event API uses 
+https://www.typescriptlang.org/docs/handbook/generics.html[generics] to specify 
+the requests and response bodies and the `meta.context`. Currently, we can't 
+provide those definitions, but we are working to improve this situation.
 
-You can find a partial definition of the request types by importing `RequestParams`, which is used by default in the client and accepts a body (when needed) as a generic to provide a better specification.
+You can find a partial definition of the request types by importing 
+`RequestParams`, which is used by default in the client and accepts a body (when 
+needed) as a generic to provide a better specification.
 
-The body defaults to `RequestBody` and `RequestNDBody`, which are defined as follows:
+The body defaults to `RequestBody` and `RequestNDBody`, which are defined as 
+follows:
 
 [source,ts]
 ----
@@ -39,7 +45,7 @@ You don't have to specify all the generics, but the order must be respected.
 
 
 [discrete]
-=== A complete example
+==== A complete example
 
 [source,ts]
 ----
diff --git a/index.d.ts b/index.d.ts
index 00462979..9f7c87ff 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -114,7 +114,8 @@ interface ClientOptions {
     // TODO: remove username and password here in 8
     username?: string;
     password?: string;
-  }
+  };
+  disablePrototypePoisoningProtection?: boolean | 'proto' | 'constructor';
 }
 
 declare class Client {
diff --git a/index.js b/index.js
index 76b517a1..02851266 100644
--- a/index.js
+++ b/index.js
@@ -123,7 +123,8 @@ class Client extends ESAPI {
         opaqueIdPrefix: null,
         context: null,
         proxy: null,
-        enableMetaHeader: true
+        enableMetaHeader: true,
+        disablePrototypePoisoningProtection: false
       }, opts)
 
     this[kInitialOptions] = options
@@ -140,7 +141,9 @@ class Client extends ESAPI {
       this[kEventEmitter] = options[kChild].eventEmitter
     } else {
       this[kEventEmitter] = new EventEmitter()
-      this.serializer = new options.Serializer()
+      this.serializer = new options.Serializer({
+        disablePrototypePoisoningProtection: options.disablePrototypePoisoningProtection
+      })
       this.connectionPool = new options.ConnectionPool({
         pingTimeout: options.pingTimeout,
         resurrectStrategy: options.resurrectStrategy,
diff --git a/lib/Helpers.js b/lib/Helpers.js
index b5fc9e91..184e256b 100644
--- a/lib/Helpers.js
+++ b/lib/Helpers.js
@@ -421,7 +421,7 @@ class Helpers {
    */
   bulk (options, reqOptions = {}) {
     const client = this[kClient]
-    const { serialize, deserialize } = client.serializer
+    const { serializer } = client
     if (this[kMetaHeader] !== null) {
       reqOptions.headers = reqOptions.headers || {}
       reqOptions.headers['x-elastic-client-meta'] = this[kMetaHeader] + ',h=bp'
@@ -505,19 +505,19 @@ class Helpers {
           ? Object.keys(action[0])[0]
           : Object.keys(action)[0]
         if (operation === 'index' || operation === 'create') {
-          actionBody = serialize(action)
-          payloadBody = typeof chunk === 'string' ? chunk : serialize(chunk)
+          actionBody = serializer.serialize(action)
+          payloadBody = typeof chunk === 'string' ? chunk : serializer.serialize(chunk)
           chunkBytes += Buffer.byteLength(actionBody) + Buffer.byteLength(payloadBody)
           bulkBody.push(actionBody, payloadBody)
         } else if (operation === 'update') {
-          actionBody = serialize(action[0])
+          actionBody = serializer.serialize(action[0])
           payloadBody = typeof chunk === 'string'
             ? `{"doc":${chunk}}`
-            : serialize({ doc: chunk, ...action[1] })
+            : serializer.serialize({ doc: chunk, ...action[1] })
           chunkBytes += Buffer.byteLength(actionBody) + Buffer.byteLength(payloadBody)
           bulkBody.push(actionBody, payloadBody)
         } else if (operation === 'delete') {
-          actionBody = serialize(action)
+          actionBody = serializer.serialize(action)
           chunkBytes += Buffer.byteLength(actionBody)
           bulkBody.push(actionBody)
         } else {
@@ -669,13 +669,13 @@ class Helpers {
             return
           }
           for (let i = 0, len = bulkBody.length; i < len; i = i + 2) {
-            const operation = Object.keys(deserialize(bulkBody[i]))[0]
+            const operation = Object.keys(serializer.deserialize(bulkBody[i]))[0]
             onDrop({
               status: 429,
               error: null,
-              operation: deserialize(bulkBody[i]),
+              operation: serializer.deserialize(bulkBody[i]),
               document: operation !== 'delete'
-                ? deserialize(bulkBody[i + 1])
+                ? serializer.deserialize(bulkBody[i + 1])
                 /* istanbul ignore next */
                 : null,
               retried: isRetrying
@@ -716,9 +716,9 @@ class Helpers {
                 onDrop({
                   status: status,
                   error: action[operation].error,
-                  operation: deserialize(bulkBody[indexSlice]),
+                  operation: serializer.deserialize(bulkBody[indexSlice]),
                   document: operation !== 'delete'
-                    ? deserialize(bulkBody[indexSlice + 1])
+                    ? serializer.deserialize(bulkBody[indexSlice + 1])
                     : null,
                   retried: isRetrying
                 })
diff --git a/lib/Serializer.d.ts b/lib/Serializer.d.ts
index bf8453ab..7aad1d11 100644
--- a/lib/Serializer.d.ts
+++ b/lib/Serializer.d.ts
@@ -17,7 +17,12 @@
  * under the License.
  */
 
+export interface SerializerOptions {
+  disablePrototypePoisoningProtection: boolean | 'proto' | 'constructor'
+}
+
 export default class Serializer {
+  constructor (opts?: SerializerOptions)
   serialize(object: any): string;
   deserialize(json: string): any;
   ndserialize(array: any[]): string;
diff --git a/lib/Serializer.js b/lib/Serializer.js
index 8aa3d9e7..7797f1f0 100644
--- a/lib/Serializer.js
+++ b/lib/Serializer.js
@@ -23,8 +23,17 @@ const { stringify } = require('querystring')
 const debug = require('debug')('elasticsearch')
 const sjson = require('secure-json-parse')
 const { SerializationError, DeserializationError } = require('./errors')
+const kJsonOptions = Symbol('secure json parse options')
 
 class Serializer {
+  constructor (opts = {}) {
+    const disable = opts.disablePrototypePoisoningProtection
+    this[kJsonOptions] = {
+      protoAction: disable === true || disable === 'proto' ? 'ignore' : 'error',
+      constructorAction: disable === true || disable === 'constructor' ? 'ignore' : 'error'
+    }
+  }
+
   serialize (object) {
     debug('Serializing', object)
     let json
@@ -40,7 +49,7 @@ class Serializer {
     debug('Deserializing', json)
     let object
     try {
-      object = sjson.parse(json)
+      object = sjson.parse(json, this[kJsonOptions])
     } catch (err) {
       throw new DeserializationError(err.message, json)
     }
diff --git a/lib/Transport.d.ts b/lib/Transport.d.ts
index 979393a6..3464e2ba 100644
--- a/lib/Transport.d.ts
+++ b/lib/Transport.d.ts
@@ -120,6 +120,7 @@ export interface TransportRequestCallback {
 
 export interface TransportRequestPromise<T> extends Promise<T> {
   abort: () => void;
+  finally(onFinally?: (() => void) | undefined | null): Promise<T>;
 }
 
 export interface TransportGetConnectionOptions {
diff --git a/lib/Transport.js b/lib/Transport.js
index a61ce0d3..c808c51b 100644
--- a/lib/Transport.js
+++ b/lib/Transport.js
@@ -163,6 +163,9 @@ class Transport {
         request.abort()
         debug('Aborting request', params)
         return this
+      },
+      finally (onFinally) {
+        return p.finally(onFinally)
       }
     }
 


Test patch:
diff --git a/docs/testing.asciidoc b/docs/testing.asciidoc
index 9de2ebeb..af9fcf25 100644
--- a/docs/testing.asciidoc
+++ b/docs/testing.asciidoc
@@ -1,56 +1,58 @@
 [[client-testing]]
-== Testing
+=== Testing
 
 Testing is one of the most important parts of developing an application.
 The client is very flexible when it comes to testing and is compatible with
 most testing frameworks (such as https://www.npmjs.com/package/ava[`ava`],
 which is used in the examples below).
 
-If you are using this client, you are very likely working with Elasticsearch,
-and one of the first issues you will face is how to test your application.
-A perfectly valid solution is to use the real Elasticsearch instance for
-testing your application, but you would be doing an integration test,
-while you want a unit test.
-There are many ways to solve this problem, you could create the database
-with docker, or use an in-memory compatible one, but if you are writing
-unit tests that can be easily parallelized this will become quite uncomfortable.
-A different way of improving your testing experience while doing unit tests
-is to use a mock.
-
-The client is designed to be easy to extend and adapt to your needs.
-Thanks to its internal architecture it allows you to change some specific
-components while keeping the rest of it working as usual.
-Each Elasticsearch official client is composed of the following components:
-
-* `API layer`: every Elasticsearch API that you can call
-* `Transport`: a component that takes care of preparing a request before sending it and handling all the retry and sniffing strategies
-* `ConnectionPool`: Elasticsearch is a cluster and might have multiple nodes, the * `ConnectionPool` takes care of them
-* `Serializer`: A class with all the serialization strategies, from the basic JSON to the new line delimited JSON.
+If you are using this client, you are most likely working with {es}, and one of 
+the first issues you face is how to test your application. A perfectly valid 
+solution is to use the real {es} instance for testing your application, but you 
+would be doing an integration test, while you want a unit test. There are many 
+ways to solve this problem, you could create the database with Docker, or use an 
+in-memory compatible one, but if you are writing unit tests that can be easily 
+parallelized this becomes quite uncomfortable. A different way of improving your 
+testing experience while doing unit tests is to use a mock.
+
+The client is designed to be easy to extend and adapt to your needs. Thanks to 
+its internal architecture it allows you to change some specific components while 
+keeping the rest of it working as usual. Each {es} official client is composed 
+of the following components:
+
+* `API layer`: every {es} API that you can call.
+* `Transport`: a component that takes care of preparing a request before sending 
+  it and handling all the retry and sniffing strategies.
+* `ConnectionPool`: {es} is a cluster and might have multiple nodes, the 
+  `ConnectionPool` takes care of them.
+* `Serializer`: A class with all the serialization strategies, from the basic 
+  JSON to the new line delimited JSON.
 * `Connection`: The actual HTTP library.
 
-The best way to mock Elasticsearch with the official clients is to replace
-the `Connection` component since it has very few responsibilities and
-it does not interact with other internal components other than getting
-requests and returning responses.
+The best way to mock {es} with the official clients is to replace the 
+`Connection` component since it has very few responsibilities and it does not 
+interact with other internal components other than getting requests and 
+returning responses.
 
 
 [discrete]
-=== `@elastic/elasticsearch-mock`
+==== `@elastic/elasticsearch-mock`
 
-Writing each time a mock for your test can be annoying and error-prone,
-so we have built a simple yet powerful mocking library specifically designed
-for this client, and you can install it with the following command:
+Writing each time a mock for your test can be annoying and error-prone, so we 
+have built a simple yet powerful mocking library specifically designed for this 
+client, and you can install it with the following command:
 
 [source,sh]
 ----
 npm install @elastic/elasticsearch-mock --save-dev
 ----
 
-With this library you can easily create custom mocks for any request you can
-send to Elasticsearch. It offers a simple and intuitive API and it mocks only
-the HTTP layer, leaving the rest of the client working as usual.
+With this library you can create custom mocks for any request you can send to 
+{es}. It offers a simple and intuitive API and it mocks only the HTTP layer, 
+leaving the rest of the client working as usual.
 
-Before showing all of its features, and what you can do with it, let’s see an example:
+Before showing all of its features, and what you can do with it, let’s see an 
+example:
 
 [source,js]
 ----
@@ -73,16 +75,16 @@ mock.add({
 client.info(console.log)
 ----
 
-As you can see it works closely with the client itself, once you have created
-a new instance of the mock library you just need to call the mock.getConnection()
-method and pass its result to the Connection option of the client.
-From now on, every request will be handled by the mock library,  and the HTTP
-layer will never be touched. As a result, your test will be significantly faster
-and you will be able to easily parallelize them!
+As you can see it works closely with the client itself, once you have created a 
+new instance of the mock library you just need to call the mock.getConnection()
+method and pass its result to the Connection option of the client. From now on, 
+every request is handled by the mock library,  and the HTTP layer will never be 
+touched. As a result, your test is significantly faster and you are able to 
+easily parallelize them!
 
-The library allows you to write both “strict” and “loose” mocks, which means
-that you can write a mock that will handle a very specific request or be looser
-and handle a group of request, let’s see this in action:
+The library allows you to write both “strict” and “loose” mocks, which means 
+that you can write a mock that handles a very specific request or be looser and 
+handle a group of request, let’s see this in action:
 
 [source,js]
 ----
@@ -112,9 +114,9 @@ mock.add({
 })
 ----
 
-In the example above every search request will get the first response,
-while every search request that uses the query described in the second mock,
-will get the second response.
+In the example above, every search request gets the first response, while every 
+search request that uses the query described in the second mock gets the second 
+response.
 
 You can also specify dynamic paths:
 
@@ -150,5 +152,6 @@ mock.add({
 })
 ----
 
-We have seen how simple is mocking Elasticsearch and testing your application,
-you can find many more features and examples in the https://github.com/elastic/elasticsearch-js-mock[module documentation].
\ No newline at end of file
+We have seen how simple is mocking {es} and testing your application, you can 
+find many more features and examples in the 
+https://github.com/elastic/elasticsearch-js-mock[module documentation].
\ No newline at end of file
diff --git a/test/bundlers/parcel-test/package.json b/test/bundlers/parcel-test/package.json
index 3f02de1d..68444883 100644
--- a/test/bundlers/parcel-test/package.json
+++ b/test/bundlers/parcel-test/package.json
@@ -4,13 +4,16 @@
   "description": "",
   "main": "index.js",
   "scripts": {
-    "start": "node bundle.js",
-    "build": "parcel build index.js --target node --bundle-node-modules --out-file bundle.js --no-source-maps --out-dir ."
+    "start": "node index.js",
+    "build": "parcel build index.js --no-source-maps"
   },
   "keywords": [],
   "author": "",
   "license": "ISC",
+  "engines": {
+    "node": ">=12"
+  },
   "devDependencies": {
-    "parcel-bundler": "^1.12.4"
+    "parcel": "^2.0.0-beta.1"
   }
 }
diff --git a/test/types/client-options.test-d.ts b/test/types/client-options.test-d.ts
index b7578b96..5831d829 100644
--- a/test/types/client-options.test-d.ts
+++ b/test/types/client-options.test-d.ts
@@ -557,22 +557,6 @@ expectError<errors.ConfigurationError>(
   )
 }
 
-{
-  class CustomSerializer {
-    deserialize (str: string) {
-      return JSON.parse(str)
-    }
-  }
-
-  expectError<errors.ConfigurationError>(
-    // @ts-expect-error
-    new Client({
-      node: 'http://localhost:9200',
-      Serializer: CustomSerializer
-    })
-  )
-}
-
 /**
  * `Connection` option
  */
diff --git a/test/unit/api.test.js b/test/unit/api.test.js
index f0842222..b5a7fbb5 100644
--- a/test/unit/api.test.js
+++ b/test/unit/api.test.js
@@ -124,6 +124,33 @@ test('Error (promises)', t => {
   })
 })
 
+test('Finally method (promises)', t => {
+  t.plan(1)
+
+  function handler (req, res) {
+    res.setHeader('Content-Type', 'application/json;utf=8')
+    res.end(JSON.stringify({ hello: 'world' }))
+  }
+
+  buildServer(handler, ({ port }, server) => {
+    const client = new Client({
+      node: `http://localhost:${port}`
+    })
+
+    const request = client.search({
+      index: 'test',
+      q: 'foo:bar'
+    })
+
+    t.type(request.finally, 'function')
+
+    request
+      .finally(() => {
+        server.stop()
+      })
+  })
+})
+
 test('Abort method (callback)', t => {
   t.plan(3)
 
diff --git a/test/unit/client.test.js b/test/unit/client.test.js
index be61e1bf..a0a5c5d7 100644
--- a/test/unit/client.test.js
+++ b/test/unit/client.test.js
@@ -1364,3 +1364,60 @@ test('Meta header disabled', t => {
     t.error(err)
   })
 })
+
+test('Prototype poisoning protection enabled by default', t => {
+  t.plan(1)
+
+  class MockConnection extends Connection {
+    request (params, callback) {
+      const stream = intoStream('{"__proto__":{"foo":"bar"}}')
+      stream.statusCode = 200
+      stream.headers = {
+        'content-type': 'application/json;utf=8',
+        'content-length': '27',
+        connection: 'keep-alive',
+        date: new Date().toISOString()
+      }
+      process.nextTick(callback, null, stream)
+      return { abort () {} }
+    }
+  }
+
+  const client = new Client({
+    node: 'http://localhost:9200',
+    Connection: MockConnection
+  })
+
+  client.info((err, result) => {
+    t.true(err instanceof errors.DeserializationError)
+  })
+})
+
+test('Disable prototype poisoning protection', t => {
+  t.plan(1)
+
+  class MockConnection extends Connection {
+    request (params, callback) {
+      const stream = intoStream('{"__proto__":{"foo":"bar"}}')
+      stream.statusCode = 200
+      stream.headers = {
+        'content-type': 'application/json;utf=8',
+        'content-length': '27',
+        connection: 'keep-alive',
+        date: new Date().toISOString()
+      }
+      process.nextTick(callback, null, stream)
+      return { abort () {} }
+    }
+  }
+
+  const client = new Client({
+    node: 'http://localhost:9200',
+    Connection: MockConnection,
+    disablePrototypePoisoningProtection: true
+  })
+
+  client.info((err, result) => {
+    t.error(err)
+  })
+})
diff --git a/test/unit/serializer.test.js b/test/unit/serializer.test.js
index cce887e7..d0e84730 100644
--- a/test/unit/serializer.test.js
+++ b/test/unit/serializer.test.js
@@ -157,3 +157,75 @@ test('DeserializationError', t => {
     t.ok(err instanceof DeserializationError)
   }
 })
+
+test('prototype poisoning protection', t => {
+  t.plan(2)
+  const s = new Serializer()
+  try {
+    s.deserialize('{"__proto__":{"foo":"bar"}}')
+    t.fail('Should fail')
+  } catch (err) {
+    t.ok(err instanceof DeserializationError)
+  }
+
+  try {
+    s.deserialize('{"constructor":{"prototype":{"foo":"bar"}}}')
+    t.fail('Should fail')
+  } catch (err) {
+    t.ok(err instanceof DeserializationError)
+  }
+})
+
+test('disable prototype poisoning protection', t => {
+  t.plan(2)
+  const s = new Serializer({ disablePrototypePoisoningProtection: true })
+  try {
+    s.deserialize('{"__proto__":{"foo":"bar"}}')
+    t.pass('Should not fail')
+  } catch (err) {
+    t.fail(err)
+  }
+
+  try {
+    s.deserialize('{"constructor":{"prototype":{"foo":"bar"}}}')
+    t.pass('Should not fail')
+  } catch (err) {
+    t.fail(err)
+  }
+})
+
+test('disable prototype poisoning protection only for proto', t => {
+  t.plan(2)
+  const s = new Serializer({ disablePrototypePoisoningProtection: 'proto' })
+  try {
+    s.deserialize('{"__proto__":{"foo":"bar"}}')
+    t.pass('Should not fail')
+  } catch (err) {
+    t.fail(err)
+  }
+
+  try {
+    s.deserialize('{"constructor":{"prototype":{"foo":"bar"}}}')
+    t.fail('Should fail')
+  } catch (err) {
+    t.ok(err instanceof DeserializationError)
+  }
+})
+
+test('disable prototype poisoning protection only for constructor', t => {
+  t.plan(2)
+  const s = new Serializer({ disablePrototypePoisoningProtection: 'constructor' })
+  try {
+    s.deserialize('{"__proto__":{"foo":"bar"}}')
+    t.fail('Should fail')
+  } catch (err) {
+    t.ok(err instanceof DeserializationError)
+  }
+
+  try {
+    s.deserialize('{"constructor":{"prototype":{"foo":"bar"}}}')
+    t.pass('Should not fail')
+  } catch (err) {
+    t.fail(err)
+  }
+})