1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 | 'use strict';
|
15 |
|
16 | const pkg = require('../package.json');
|
17 | const http = require('http');
|
18 | const https = require('https');
|
19 | const stream = require('stream');
|
20 | const { CloudantV1, CouchdbSessionAuthenticator } = require('@ibm-cloud/cloudant');
|
21 | const { IamAuthenticator, NoAuthAuthenticator } = require('ibm-cloud-sdk-core');
|
22 | const retryPlugin = require('retry-axios');
|
23 |
|
24 | const userAgent = 'couchbackup-cloudant/' + pkg.version + ' (Node.js ' +
|
25 | process.version + ')';
|
26 |
|
27 |
|
28 |
|
29 |
|
30 | class ResponseWriteable extends stream.Writable {
|
31 | constructor(options) {
|
32 | super(options);
|
33 | this.data = [];
|
34 | }
|
35 |
|
36 | _write(chunk, encoding, callback) {
|
37 | this.data.push(chunk);
|
38 | callback();
|
39 | }
|
40 |
|
41 | stringBody() {
|
42 | return Buffer.concat(this.data).toString();
|
43 | }
|
44 | }
|
45 |
|
46 |
|
47 |
|
48 |
|
49 | const errorHelper = async function(err) {
|
50 | let method;
|
51 | let requestUrl;
|
52 | if (err.response) {
|
53 | if (err.response.config.url) {
|
54 | requestUrl = err.response.config.url;
|
55 | method = err.response.config.method;
|
56 | }
|
57 |
|
58 | let errorMsg = `${err.response.status} ${err.response.statusText || ''}: ` +
|
59 | `${method} ${requestUrl}`;
|
60 | if (err.response.data) {
|
61 |
|
62 | if (err.response.headers['content-type'] === 'application/json') {
|
63 | if (!err.response.data.error && err.response.data.pipe) {
|
64 |
|
65 |
|
66 |
|
67 | const p = new Promise((resolve, reject) => {
|
68 | const errorBody = new ResponseWriteable();
|
69 | err.response.data.pipe(errorBody)
|
70 | .on('finish', () => { resolve(JSON.parse(errorBody.stringBody())); })
|
71 | .on('error', () => { reject(err); });
|
72 | });
|
73 |
|
74 | err.response.data = await p;
|
75 | }
|
76 |
|
77 | if (err.response.data.error) {
|
78 |
|
79 | errorMsg += ` - Error: ${err.response.data.error}`;
|
80 | if (err.response.data.reason) {
|
81 | errorMsg += `, Reason: ${err.response.data.reason}`;
|
82 | }
|
83 | }
|
84 | } else {
|
85 | errorMsg += err.response.data;
|
86 | }
|
87 |
|
88 |
|
89 |
|
90 | err.response.data.errors = [{ message: errorMsg }];
|
91 | }
|
92 | } else if (err.request) {
|
93 | if (!err.message.includes(err.config.url)) {
|
94 |
|
95 |
|
96 | err.message = `${err.message}: ${err.config.method} ${err.config.url}`;
|
97 | }
|
98 | }
|
99 | return Promise.reject(err);
|
100 | };
|
101 |
|
102 | module.exports = {
|
103 | client: function(rawUrl, opts) {
|
104 | const url = new URL(rawUrl);
|
105 | const protocol = (url.protocol.match(/^https/)) ? https : http;
|
106 | const keepAliveAgent = new protocol.Agent({
|
107 | keepAlive: true,
|
108 | keepAliveMsecs: 30000,
|
109 | maxSockets: opts.parallelism
|
110 | });
|
111 |
|
112 |
|
113 | const actUrl = new URL(url.pathname.substr(0, url.pathname.lastIndexOf('/')), url.origin);
|
114 | const dbName = url.pathname.substr(url.pathname.lastIndexOf('/') + 1);
|
115 | let authenticator;
|
116 |
|
117 | if (opts.iamApiKey) {
|
118 | const iamAuthOpts = { apikey: opts.iamApiKey };
|
119 | if (opts.iamTokenUrl) {
|
120 | iamAuthOpts.url = opts.iamTokenUrl;
|
121 | }
|
122 | authenticator = new IamAuthenticator(iamAuthOpts);
|
123 | } else if (url.username) {
|
124 | authenticator = new CouchdbSessionAuthenticator({
|
125 | username: url.username,
|
126 | password: url.password
|
127 | });
|
128 | } else {
|
129 | authenticator = new NoAuthAuthenticator();
|
130 | }
|
131 | const serviceOpts = {
|
132 | authenticator: authenticator,
|
133 | timeout: opts.requestTimeout,
|
134 |
|
135 | maxContentLength: -1
|
136 | };
|
137 | if (url.protocol === 'https') {
|
138 | serviceOpts.httpsAgent = keepAliveAgent;
|
139 | } else {
|
140 | serviceOpts.httpAgent = keepAliveAgent;
|
141 | }
|
142 | const service = new CloudantV1(serviceOpts);
|
143 |
|
144 | const maxRetries = 2;
|
145 | service.getHttpClient().defaults.raxConfig = {
|
146 |
|
147 | retry: maxRetries,
|
148 |
|
149 | noResponseRetries: maxRetries,
|
150 | backoffType: 'exponential',
|
151 | httpMethodsToRetry: ['GET', 'HEAD', 'POST'],
|
152 | statusCodesToRetry: [
|
153 | [429, 429],
|
154 | [500, 599]
|
155 | ],
|
156 | shouldRetry: err => {
|
157 | const cfg = retryPlugin.getConfig(err);
|
158 |
|
159 | if (cfg.currentRetryAttempt >= maxRetries) {
|
160 | return false;
|
161 | } else {
|
162 | return retryPlugin.shouldRetryRequest(err);
|
163 | }
|
164 | },
|
165 | instance: service.getHttpClient()
|
166 | };
|
167 | retryPlugin.attach(service.getHttpClient());
|
168 |
|
169 | service.setServiceUrl(actUrl.toString());
|
170 | if (authenticator instanceof CouchdbSessionAuthenticator) {
|
171 |
|
172 |
|
173 |
|
174 | authenticator.tokenManager.requestWrapperInstance.compressRequestData = false;
|
175 | }
|
176 | if (authenticator.tokenManager && authenticator.tokenManager.requestWrapperInstance) {
|
177 | authenticator.tokenManager.requestWrapperInstance.axiosInstance.interceptors.response.use(null, errorHelper);
|
178 | }
|
179 |
|
180 | service.getHttpClient().interceptors.response.use(null, errorHelper);
|
181 |
|
182 |
|
183 | service.getHttpClient().interceptors.request.use(function(requestConfig) {
|
184 | requestConfig.headers['User-Agent'] = userAgent;
|
185 | return requestConfig;
|
186 | }, null);
|
187 |
|
188 | return { service: service, db: dbName, url: actUrl.toString() };
|
189 | }
|
190 | };
|