Skip to content
Draft
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
186 changes: 186 additions & 0 deletions src/apiClients.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { RequestId } from '@modelcontextprotocol/sdk/types.js';

import { getConfig } from './config.js';
import { log, shouldLogWhenLevelIsAtLeast } from './logging/log.js';
import { maskRequest, maskResponse } from './logging/secretMask.js';
import {
AxiosInterceptor,
AxiosResponseInterceptorConfig,
ErrorInterceptor,
getRequestInterceptorConfig,
getResponseInterceptorConfig,
RequestInterceptor,
RequestInterceptorConfig,
ResponseInterceptor,
ResponseInterceptorConfig,
} from './sdks/tableau/interceptors.js';
import { Server, userAgent } from './server.js';
import { isAxiosError } from './utils/axios.js';
import { getExceptionMessage } from './utils/getExceptionMessage.js';

export const getRequestInterceptor =
(server: Server, requestId: RequestId, logger: string): RequestInterceptor =>
(request) => {
request.headers['User-Agent'] = getUserAgent(server);
logRequest(server, request, requestId, logger);
return request;
};

export const getRequestErrorInterceptor =
(server: Server, requestId: RequestId, logger: string): ErrorInterceptor =>
(error, baseUrl) => {
if (!isAxiosError(error) || !error.request) {
log.error(server, `Request ${requestId} failed with error: ${getExceptionMessage(error)}`, {
logger,
requestId,
});
return;
}

const { request } = error;
logRequest(
server,
{
baseUrl,
...getRequestInterceptorConfig(request),
},
requestId,
logger,
);
};

export const getResponseInterceptor =
(server: Server, requestId: RequestId, logger: string): ResponseInterceptor =>
(response) => {
logResponse(server, response, requestId, logger);
return response;
};

export const getResponseErrorInterceptor =
(server: Server, requestId: RequestId, logger: string): ErrorInterceptor =>
(error, baseUrl) => {
if (!isAxiosError(error) || !error.response) {
log.error(
server,
`Response from request ${requestId} failed with error: ${getExceptionMessage(error)}`,
{ logger, requestId },
);
return;
}

// The type for the AxiosResponse headers is complex and not directly assignable to that of the Axios response interceptor's.
const { response } = error as { response: AxiosResponseInterceptorConfig };
logResponse(
server,
{
baseUrl,
...getResponseInterceptorConfig(response),
},
requestId,
logger,
);
};

function logRequest(
server: Server,
request: RequestInterceptorConfig,
requestId: RequestId,
logger: string,
): void {
const config = getConfig();
const maskedRequest = config.disableLogMasking ? request : maskRequest(request);
const url = new URL(
`${maskedRequest.baseUrl.replace(/\/$/, '')}/${maskedRequest.url?.replace(/^\//, '') ?? ''}`,
);
if (request.params && Object.keys(request.params).length > 0) {
url.search = new URLSearchParams(request.params).toString();
}

const messageObj = {
type: 'request',
requestId,
method: maskedRequest.method,
url: url.toString(),
...(shouldLogWhenLevelIsAtLeast('debug') && {
headers: maskedRequest.headers,
data: maskedRequest.data,
params: maskedRequest.params,
}),
} as const;

log.info(server, messageObj, { logger, requestId });
}

function logResponse(
server: Server,
response: ResponseInterceptorConfig,
requestId: RequestId,
logger: string,
): void {
const config = getConfig();
const maskedResponse = config.disableLogMasking ? response : maskResponse(response);
const url = new URL(
`${maskedResponse.baseUrl.replace(/\/$/, '')}/${maskedResponse.url?.replace(/^\//, '') ?? ''}`,
);
if (response.request?.params && Object.keys(response.request.params).length > 0) {
url.search = new URLSearchParams(response.request.params).toString();
}
const messageObj = {
type: 'response',
requestId,
url: url.toString(),
status: maskedResponse.status,
...(shouldLogWhenLevelIsAtLeast('debug') && {
headers: maskedResponse.headers,
data: maskedResponse.data,
}),
} as const;

log.info(server, messageObj, { logger, requestId });
}

function getUserAgent(server: Server): string {
const userAgentParts = [userAgent];
if (server.clientInfo) {
const { name, version } = server.clientInfo;
if (name) {
userAgentParts.push(version ? `(${name} ${version})` : `(${name})`);
}
}
return userAgentParts.join(' ');
}

export const addInterceptors = (
baseUrl: string,
axiosInterceptors: AxiosInterceptor,
requestInterceptors?: [RequestInterceptor, ErrorInterceptor?],
responseInterceptors?: [ResponseInterceptor, ErrorInterceptor?],
): void => {
axiosInterceptors.request.use(
(config) => {
requestInterceptors?.[0]({
baseUrl,
...getRequestInterceptorConfig(config),
});
return config;
},
(error) => {
requestInterceptors?.[1]?.(error, baseUrl);
return Promise.reject(error);
},
);

axiosInterceptors.response.use(
(response) => {
responseInterceptors?.[0]({
baseUrl,
...getResponseInterceptorConfig(response),
});
return response;
},
(error) => {
responseInterceptors?.[1]?.(error, baseUrl);
return Promise.reject(error);
},
);
};
20 changes: 10 additions & 10 deletions src/restApiInstance.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { getConfig } from './config.js';
import { log } from './logging/log.js';
import {
getRequestErrorInterceptor,
getRequestInterceptor,
getResponseErrorInterceptor,
getResponseInterceptor,
useRestApi,
} from './restApiInstance.js';
} from './apiClients.js';
import { getConfig } from './config.js';
import { log } from './logging/log.js';
import { useRestApi } from './restApiInstance.js';
import { AuthConfig } from './sdks/tableau/authConfig.js';
import { RestApi } from './sdks/tableau/restApi.js';
import { Server, userAgent } from './server.js';
Expand Down Expand Up @@ -55,7 +55,7 @@ describe('restApiInstance', () => {
describe('Request Interceptor', () => {
it('should add User-Agent header and log request', () => {
const server = new Server();
const interceptor = getRequestInterceptor(server, mockRequestId);
const interceptor = getRequestInterceptor(server, mockRequestId, 'rest-api');
const mockRequest = {
headers: {} as Record<string, string>,
method: 'GET',
Expand Down Expand Up @@ -85,7 +85,7 @@ describe('restApiInstance', () => {
describe('Response Interceptor', () => {
it('should log response', () => {
const server = new Server();
const interceptor = getResponseInterceptor(server, mockRequestId);
const interceptor = getResponseInterceptor(server, mockRequestId, 'rest-api');
const mockResponse = {
status: 200,
url: '/api/test',
Expand Down Expand Up @@ -116,7 +116,7 @@ describe('restApiInstance', () => {
describe('Error Handling', () => {
it('should handle request errors', () => {
const server = new Server();
const errorInterceptor = getRequestErrorInterceptor(server, mockRequestId);
const errorInterceptor = getRequestErrorInterceptor(server, mockRequestId, 'rest-api');
const mockError = {
request: {
method: 'GET',
Expand All @@ -140,7 +140,7 @@ describe('restApiInstance', () => {

it('should handle AxiosError request errors', () => {
const server = new Server();
const errorInterceptor = getRequestErrorInterceptor(server, mockRequestId);
const errorInterceptor = getRequestErrorInterceptor(server, mockRequestId, 'rest-api');
const mockError = {
isAxiosError: true,
request: {
Expand Down Expand Up @@ -172,7 +172,7 @@ describe('restApiInstance', () => {

it('should handle response errors', () => {
const server = new Server();
const errorInterceptor = getResponseErrorInterceptor(server, mockRequestId);
const errorInterceptor = getResponseErrorInterceptor(server, mockRequestId, 'rest-api');
const mockError = {
response: {
status: 500,
Expand All @@ -197,7 +197,7 @@ describe('restApiInstance', () => {

it('should handle AxiosError response errors', () => {
const server = new Server();
const errorInterceptor = getResponseErrorInterceptor(server, mockRequestId);
const errorInterceptor = getResponseErrorInterceptor(server, mockRequestId, 'rest-api');
const mockError = {
isAxiosError: true,
response: {
Expand Down
Loading
Loading