Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion lib/api_client/execute_request.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,12 @@ function execute_request(method, params, auth, api_url, callback, options = {})
request_options.headers['Content-Length'] = Buffer.byteLength(query_params);
}
handle_response = function (res) {
const { hide_sensitive = false } = config();
const { hide_sensitive = false, debug = false } = config();
const sanitizedOptions = { ...request_options };

// Capture X-Request-Id from response headers for debugging
const requestId = res.headers['x-request-id'];

if (hide_sensitive === true) {
if ("auth" in sanitizedOptions) { delete sanitizedOptions.auth; }
if ("Authorization" in sanitizedOptions.headers) { delete sanitizedOptions.headers.Authorization; }
Expand Down Expand Up @@ -108,6 +111,10 @@ function execute_request(method, params, auth, api_url, callback, options = {})

if (result.error) {
result.error.http_code = res.statusCode;
// Include request_id in errors when debug mode is enabled
if (debug && requestId) {
result.error.request_id = requestId;
}
} else {
if (res.headers["x-featureratelimit-limit"]) {
result.rate_limit_allowed = parseInt(res.headers["x-featureratelimit-limit"]);
Expand All @@ -118,6 +125,10 @@ function execute_request(method, params, auth, api_url, callback, options = {})
if (res.headers["x-featureratelimit-remaining"]) {
result.rate_limit_remaining = parseInt(res.headers["x-featureratelimit-remaining"]);
}
// Include request_id in success responses when debug mode is enabled
if (debug && requestId) {
result.request_id = requestId;
}
}

if (result.error) {
Expand All @@ -142,6 +153,10 @@ function execute_request(method, params, auth, api_url, callback, options = {})
query_params
}
};
// Include request_id in network errors when debug mode is enabled
if (debug && requestId) {
err_obj.error.request_id = requestId;
}
deferred.reject(err_obj.error);
if (typeof callback === "function") {
callback(err_obj);
Expand All @@ -156,6 +171,10 @@ function execute_request(method, params, auth, api_url, callback, options = {})
query_params
}
};
// Include request_id in unexpected status code errors when debug mode is enabled
if (debug && requestId) {
err_obj.error.request_id = requestId;
}
deferred.reject(err_obj.error);
if (typeof callback === "function") {
callback(err_obj);
Expand Down
195 changes: 195 additions & 0 deletions test/unit/debug_mode_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
const expect = require('expect.js');
const sinon = require('sinon');
const https = require('https');
const { EventEmitter } = require('events');
const cloudinary = require('../../lib/cloudinary');
const createTestConfig = require('../testUtils/createTestConfig');

describe('debug mode', function () {
let requestStub;
let mockResponse;

beforeEach(function () {
cloudinary.config(createTestConfig());

// Create a mock response that extends EventEmitter
mockResponse = new EventEmitter();
mockResponse.statusCode = 200;
mockResponse.headers = {
'x-request-id': 'test-request-id-12345678',
'x-featureratelimit-limit': '500',
'x-featureratelimit-reset': new Date().toUTCString(),
'x-featureratelimit-remaining': '499'
};

// Stub the https.request method
requestStub = sinon.stub(https, 'request').callsFake(function (options, callback) {
// Call the callback with our mock response
setTimeout(() => callback(mockResponse), 0);

// Return a mock request object
const mockRequest = new EventEmitter();
mockRequest.write = sinon.stub();
mockRequest.end = function () {
// Simulate response data after end() is called
setTimeout(() => {
mockResponse.emit('data', JSON.stringify({ status: 'ok' }));
mockResponse.emit('end');
}, 10);
};
mockRequest.setTimeout = sinon.stub();

return mockRequest;
});
});

afterEach(function () {
requestStub.restore();
});

describe('when debug mode is enabled', function () {
beforeEach(function () {
cloudinary.config({ debug: true });
});

it('should include request_id in successful responses', function (done) {
cloudinary.v2.api.ping()
.then((result) => {
expect(result.request_id).to.be('test-request-id-12345678');
expect(result.status).to.be('ok');
done();
})
.catch(done);
});

it('should include request_id in error responses', function (done) {
// Override the mock response to simulate an error
mockResponse.statusCode = 404;
requestStub.restore();

requestStub = sinon.stub(https, 'request').callsFake(function (options, callback) {
setTimeout(() => callback(mockResponse), 0);

const mockRequest = new EventEmitter();
mockRequest.write = sinon.stub();
mockRequest.end = function () {
setTimeout(() => {
mockResponse.emit('data', JSON.stringify({
error: {
message: 'Resource not found'
}
}));
mockResponse.emit('end');
}, 10);
};
mockRequest.setTimeout = sinon.stub();

return mockRequest;
});

cloudinary.v2.api.resource('nonexistent').catch((error) => {
expect(error.error.request_id).to.be('test-request-id-12345678');
expect(error.error.message).to.be('Resource not found');
expect(error.error.http_code).to.be(404);
done();
});
});

it('should include request_id even when X-Request-Id header has different casing', function (done) {
// Test case insensitivity (Node.js lowercases headers)
mockResponse.headers = {
'x-request-id': 'case-insensitive-id'
};

cloudinary.v2.api.ping()
.then((result) => {
expect(result.request_id).to.be('case-insensitive-id');
done();
})
.catch(done);
});
});

describe('when debug mode is disabled', function () {
beforeEach(function () {
cloudinary.config({ debug: false });
});

it('should NOT include request_id in successful responses', function (done) {
cloudinary.v2.api.ping()
.then((result) => {
expect(result.request_id).to.be(undefined);
expect(result.status).to.be('ok');
done();
})
.catch(done);
});

it('should NOT include request_id in error responses', function (done) {
mockResponse.statusCode = 404;
requestStub.restore();

requestStub = sinon.stub(https, 'request').callsFake(function (options, callback) {
setTimeout(() => callback(mockResponse), 0);

const mockRequest = new EventEmitter();
mockRequest.write = sinon.stub();
mockRequest.end = function () {
setTimeout(() => {
mockResponse.emit('data', JSON.stringify({
error: {
message: 'Resource not found'
}
}));
mockResponse.emit('end');
}, 10);
};
mockRequest.setTimeout = sinon.stub();

return mockRequest;
});

cloudinary.v2.api.resource('nonexistent').catch((error) => {
expect(error.error.request_id).to.be(undefined);
expect(error.error.message).to.be('Resource not found');
done();
});
});
});

describe('when X-Request-Id header is missing', function () {
beforeEach(function () {
cloudinary.config({ debug: true });
// Remove the x-request-id header
delete mockResponse.headers['x-request-id'];
});

it('should not break when request_id is missing from headers', function (done) {
cloudinary.v2.api.ping()
.then((result) => {
expect(result.request_id).to.be(undefined);
expect(result.status).to.be('ok');
done();
})
.catch(done);
});
});

describe('config option', function () {
it('should accept debug option in config', function () {
cloudinary.config({ debug: true });
expect(cloudinary.config('debug')).to.be(true);

cloudinary.config({ debug: false });
expect(cloudinary.config('debug')).to.be(false);
});

it('should be undefined when not explicitly set', function () {
const testConfig = createTestConfig();
delete testConfig.debug; // Ensure debug is not in the config
cloudinary.config(testConfig);
// When not set, it should be undefined (falsy)
expect(cloudinary.config('debug')).to.not.be.ok();
});
});
});
3 changes: 3 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ declare module 'cloudinary' {
provisioning_api_key?: string;
provisioning_api_secret?: string;
oauth_token?: string;
debug?: boolean;

[futureKey: string]: any;
}
Expand Down Expand Up @@ -633,6 +634,7 @@ declare module 'cloudinary' {
rate_limit_allowed?: number;
rate_limit_reset_at?: string;
rate_limit_remaining?: number;
request_id?: string;
}

export interface UploadApiResponse {
Expand Down Expand Up @@ -667,6 +669,7 @@ declare module 'cloudinary' {
message: string;
name: string;
http_code: number;
request_id?: string;

[futureKey: string]: any;
}
Expand Down