@cloudant/couchbackup
Version:
CouchBackup - command-line backup utility for Cloudant/CouchDB
151 lines (139 loc) • 6.19 kB
JavaScript
// Copyright © 2017, 2025 IBM Corp. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
const pkg = require('../package.json');
const { CloudantV1, CouchdbSessionAuthenticator } = require('@ibm-cloud/cloudant');
const { IamAuthenticator, NoAuthAuthenticator } = require('ibm-cloud-sdk-core');
const debug = require('debug')('couchbackup:request');
const userAgent = 'couchbackup-cloudant/' + pkg.version + ' (Node.js ' +
process.version + ')';
// An interceptor function to help augment error bodies with a little
// extra information so we can continue to use consistent messaging
// after the ugprade to @ibm-cloud/cloudant
function errorHelper(err) {
debug('Entering error helper interceptor');
let method;
let requestUrl;
if (err.response) {
debug('Error has a response');
if (err.response.config.url) {
debug('Getting request URL and method for error');
requestUrl = err.response.config.url;
method = err.response.config.method;
}
debug('Applying response error message with status, url, and method');
// Override the status text with an improved message
let errorMsg = `${err.response.status} ${method} ${requestUrl}`;
if (err.response.data) {
debug('Found response data');
// Check if we have a JSON response and try to get the error/reason
if (err.response.headers['content-type'] === 'application/json') {
debug('Response data is JSON');
// Append the 'errors' message if available
if (err.response.data.errors && err.response.data.errors.length > 0) {
const originalError = err.response.data.errors[0];
originalError.message = `${errorMsg} - Error: ${originalError.message}`;
}
} else {
errorMsg += err.response.data;
// Set a new message for use by the node-sdk-core
// We use the errors array because it gets processed
// ahead of all other service errors.
err.response.data.errors = [{ message: errorMsg }];
}
}
} else if (err.request) {
debug('Error did not include a response');
if (!err.message.includes(err.config.url)) {
debug('Augmenting request error message with URL and method');
// Augment the message with the URL and method
// but don't do it again if we already have the URL.
err.message = `${err.message}: ${err.config.method} ${err.config.url}`;
} else {
debug('Request error message already augmented');
}
}
return Promise.reject(err);
}
// Interceptor function to add the User-Agent header.
// An interceptor is used because setting UA in headers
// option during client initialization means it gets overwritten
// by the default value during a request.
// This interceptor is further along the chain and able to
// replace the default value.
function userAgentHelper(requestConfig) {
requestConfig.headers['User-Agent'] = userAgent;
return requestConfig;
}
function newSimpleClient(rawUrl, opts) {
const url = new URL(rawUrl);
// Split the URL to separate service from database
// Use origin as the "base" to remove auth elements
const actUrl = new URL(url.pathname.substring(0, url.pathname.lastIndexOf('/')), url.origin);
const dbName = decodeURIComponent(url.pathname.substring(url.pathname.lastIndexOf('/') + 1));
let authenticator;
// Default to cookieauth unless an IAM key is provided
if (opts.iamApiKey) {
const iamAuthOpts = { apikey: opts.iamApiKey };
if (opts.iamTokenUrl) {
iamAuthOpts.url = opts.iamTokenUrl;
}
authenticator = new IamAuthenticator(iamAuthOpts);
} else if (url.username) {
authenticator = new CouchdbSessionAuthenticator({
username: decodeURIComponent(url.username),
password: decodeURIComponent(url.password)
});
} else {
authenticator = new NoAuthAuthenticator();
}
const serviceOpts = {
authenticator,
timeout: opts.requestTimeout,
// Axios performance options
maxContentLength: -1
};
const service = new CloudantV1(serviceOpts);
service.setServiceUrl(actUrl.toString());
if (authenticator instanceof CouchdbSessionAuthenticator) {
// Awkward workaround for known Couch issue with compression on _session requests
// It is not feasible to disable compression on all requests with the amount of
// data this lib needs to move, so override the property in the tokenManager instance.
authenticator.tokenManager.requestWrapperInstance.compressRequestData = false;
}
return { service, dbName, actUrl };
}
function newClient(rawUrl, opts) {
const { service, dbName, actUrl } = newSimpleClient(rawUrl, opts);
const authenticator = service.getAuthenticator();
// Add interceptors
// Request interceptor to set the User-Agent header
// Response interceptor to put URLs in error messages
// Add for the token manager if present
if (authenticator.tokenManager && authenticator.tokenManager.requestWrapperInstance) {
authenticator.tokenManager.requestWrapperInstance.axiosInstance.interceptors.request.use(userAgentHelper, null);
authenticator.tokenManager.requestWrapperInstance.axiosInstance.interceptors.response.use(null, errorHelper);
}
// and add for the client
service.getHttpClient().interceptors.request.use(userAgentHelper, null);
service.getHttpClient().interceptors.response.use(null, errorHelper);
// Configure retries
// Note: this MUST happen last after all other interceptors have been registered
const maxRetries = 2; // for 3 total attempts
service.enableRetries({ maxRetries });
return { service, dbName, url: actUrl.toString() };
}
module.exports = {
newSimpleClient,
newClient
};