From 4073d60b9759b42338724556dd732fbe6e0bd3d0 Mon Sep 17 00:00:00 2001 From: Tomas Della Vedova Date: Tue, 13 Jul 2021 09:47:45 +0200 Subject: [PATCH] Add api compatibility header support (#1478) --- docs/advanced-config.asciidoc | 16 ++++++++++- index.js | 4 +++ lib/Transport.js | 9 ++++--- test/unit/client.test.js | 50 +++++++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 4 deletions(-) diff --git a/docs/advanced-config.asciidoc b/docs/advanced-config.asciidoc index e5a64ea1f..1308b806a 100644 --- a/docs/advanced-config.asciidoc +++ b/docs/advanced-config.asciidoc @@ -83,4 +83,18 @@ class MySerializer extends Serializer { const client = new Client({ Serializer: MySerializer }) ----- \ No newline at end of file +---- + +[discrete] +==== Migrate to v8 + +The Node.js client can be configured to emit an HTTP header +``Accept: application/vnd.elasticsearch+json; compatible-with=7`` +which signals to Elasticsearch that the client is requesting +``7.x`` version of request and response bodies. This allows for +upgrading from 7.x to 8.x version of Elasticsearch without upgrading +everything at once. Elasticsearch should be upgraded first after +the compatibility header is configured and clients should be upgraded +second. +To enable to setting, configure the environment variable +``ELASTIC_CLIENT_APIVERSIONING`` to ``true``. diff --git a/index.js b/index.js index e36e6bee4..fc9e62ff3 100644 --- a/index.js +++ b/index.js @@ -116,6 +116,10 @@ class Client extends ESAPI { disablePrototypePoisoningProtection: false }, opts) + if (process.env.ELASTIC_CLIENT_APIVERSIONING === 'true') { + options.headers = Object.assign({ accept: 'application/vnd.elasticsearch+json; compatible-with=7' }, options.headers) + } + this[kInitialOptions] = options this[kExtensions] = [] this.name = options.name diff --git a/lib/Transport.js b/lib/Transport.js index dae838d1b..e99c1ccc9 100644 --- a/lib/Transport.js +++ b/lib/Transport.js @@ -38,6 +38,7 @@ const clientVersion = require('../package.json').version const userAgent = `elasticsearch-js/${clientVersion} (${os.platform()} ${os.release()}-${os.arch()}; Node.js ${process.version})` const MAX_BUFFER_LENGTH = buffer.constants.MAX_LENGTH const MAX_STRING_LENGTH = buffer.constants.MAX_STRING_LENGTH +const kApiVersioning = Symbol('api versioning') class Transport { constructor (opts) { @@ -64,6 +65,7 @@ class Transport { this.generateRequestId = opts.generateRequestId || generateRequestId() this.name = opts.name this.opaqueIdPrefix = opts.opaqueIdPrefix + this[kApiVersioning] = process.env.ELASTIC_CLIENT_APIVERSIONING === 'true' this.nodeFilter = opts.nodeFilter || defaultNodeFilter if (typeof opts.nodeSelector === 'function') { @@ -295,7 +297,8 @@ class Transport { // - the request is not a HEAD request // - the payload is not an empty string if (result.headers['content-type'] !== undefined && - result.headers['content-type'].indexOf('application/json') > -1 && + (result.headers['content-type'].indexOf('application/json') > -1 || + result.headers['content-type'].indexOf('application/vnd.elasticsearch+json') > -1) && isHead === false && payload !== '' ) { @@ -369,7 +372,7 @@ class Transport { } if (params.body !== '') { - headers['content-type'] = headers['content-type'] || 'application/json' + headers['content-type'] = headers['content-type'] || (this[kApiVersioning] ? 'application/vnd.elasticsearch+json; compatible-with=7' : 'application/json') } // handle ndjson body @@ -386,7 +389,7 @@ class Transport { params.body = params.bulkBody } if (params.body !== '') { - headers['content-type'] = headers['content-type'] || 'application/x-ndjson' + headers['content-type'] = headers['content-type'] || (this[kApiVersioning] ? 'application/vnd.elasticsearch+x-ndjson; compatible-with=7' : 'application/x-ndjson') } } diff --git a/test/unit/client.test.js b/test/unit/client.test.js index 95ae7cc12..8c65cf21d 100644 --- a/test/unit/client.test.js +++ b/test/unit/client.test.js @@ -1422,6 +1422,56 @@ test('Disable prototype poisoning protection', t => { }) }) +test('API compatibility header (json)', t => { + t.plan(4) + + function handler (req, res) { + t.equal(req.headers.accept, 'application/vnd.elasticsearch+json; compatible-with=7') + t.equal(req.headers['content-type'], 'application/vnd.elasticsearch+json; compatible-with=7') + res.setHeader('Content-Type', 'application/vnd.elasticsearch+json; compatible-with=7') + res.end(JSON.stringify({ hello: 'world' })) + } + + buildServer(handler, ({ port }, server) => { + process.env.ELASTIC_CLIENT_APIVERSIONING = 'true' + const client = new Client({ + node: `http://localhost:${port}` + }) + + client.index({ index: 'foo', body: {} }, (err, { body }) => { + t.error(err) + t.same(body, { hello: 'world' }) + server.stop() + delete process.env.ELASTIC_CLIENT_APIVERSIONING + }) + }) +}) + +test('API compatibility header (x-ndjson)', t => { + t.plan(4) + + function handler (req, res) { + t.equal(req.headers.accept, 'application/vnd.elasticsearch+json; compatible-with=7') + t.equal(req.headers['content-type'], 'application/vnd.elasticsearch+x-ndjson; compatible-with=7') + res.setHeader('Content-Type', 'application/vnd.elasticsearch+json; compatible-with=7') + res.end(JSON.stringify({ hello: 'world' })) + } + + buildServer(handler, ({ port }, server) => { + process.env.ELASTIC_CLIENT_APIVERSIONING = 'true' + const client = new Client({ + node: `http://localhost:${port}` + }) + + client.bulk({ index: 'foo', body: [{}, {}] }, (err, { body }) => { + t.error(err) + t.same(body, { hello: 'world' }) + server.stop() + delete process.env.ELASTIC_CLIENT_APIVERSIONING + }) + }) +}) + test('Bearer auth', t => { t.plan(3)