Skip to content

Commit

Permalink
🐛 fix: desensitize openai base url in the error response (#918)
Browse files Browse the repository at this point in the history
* 🐛 fix: desensitized openai base url

* 🎨 chore: improve code
  • Loading branch information
arvinxx authored Jan 3, 2024
1 parent d6b1dda commit ab0aeb7
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 2 deletions.
37 changes: 37 additions & 0 deletions src/app/api/openai/chat/createChatCompletion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ describe('createChatCompletion', () => {
expect(result).toBeInstanceOf(Response);
expect(result.status).toBe(577); // Your custom error status code
});

it('should return an cause response when OpenAI.APIError is thrown with cause', async () => {
// Arrange
const errorInfo = {
Expand Down Expand Up @@ -101,6 +102,42 @@ describe('createChatCompletion', () => {

const content = await result.json();
expect(content.body).toHaveProperty('endpoint');
expect(content.body.endpoint).toEqual('https://api.openai.com/v1');
expect(content.body.error).toEqual(errorInfo);
});

it('should return an cause response with desensitize Url', async () => {
// Arrange
const errorInfo = {
stack: 'abc',
cause: { message: 'api is undefined' },
};
const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});

openaiInstance = new OpenAI({
apiKey: 'test',
dangerouslyAllowBrowser: true,
baseURL: 'https://api.abc.com/v1',
});

vi.spyOn(openaiInstance.chat.completions, 'create').mockRejectedValue(apiError);

// Act
const result = await createChatCompletion({
openai: openaiInstance,
payload: {
messages: [{ content: 'Hello', role: 'user' }],
model: 'gpt-3.5-turbo',
temperature: 0,
},
});

// Assert
expect(result).toBeInstanceOf(Response);
expect(result.status).toBe(577); // Your custom error status code

const content = await result.json();
expect(content.body.endpoint).toEqual('https://api.***.com/v1');
expect(content.body.error).toEqual(errorInfo);
});

Expand Down
12 changes: 10 additions & 2 deletions src/app/api/openai/chat/createChatCompletion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ChatErrorType } from '@/types/fetch';
import { OpenAIChatStreamPayload } from '@/types/openai/chat';

import { createErrorResponse } from '../errorResponse';
import { desensitizeUrl } from './desensitizeUrl';

interface CreateChatCompletionOptions {
openai: OpenAI;
Expand All @@ -29,6 +30,13 @@ export const createChatCompletion = async ({ payload, openai }: CreateChatComple
const stream = OpenAIStream(response);
return new StreamingTextResponse(stream);
} catch (error) {
let desensitizedEndpoint = openai.baseURL;

// refs: https://github.com/lobehub/lobe-chat/issues/842
if (openai.baseURL !== 'https://api.openai.com/v1') {
desensitizedEndpoint = desensitizeUrl(openai.baseURL);
}

// Check if the error is an OpenAI APIError
if (error instanceof OpenAI.APIError) {
let errorResult: any;
Expand All @@ -51,7 +59,7 @@ export const createChatCompletion = async ({ payload, openai }: CreateChatComple
console.error(errorResult);

return createErrorResponse(ChatErrorType.OpenAIBizError, {
endpoint: openai.baseURL,
endpoint: desensitizedEndpoint,
error: errorResult,
});
}
Expand All @@ -61,7 +69,7 @@ export const createChatCompletion = async ({ payload, openai }: CreateChatComple

// return as a GatewayTimeout error
return createErrorResponse(ChatErrorType.InternalServerError, {
endpoint: openai.baseURL,
endpoint: desensitizedEndpoint,
error: JSON.stringify(error),
});
}
Expand Down
47 changes: 47 additions & 0 deletions src/app/api/openai/chat/desensitizeUrl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expect, it } from 'vitest';

import { desensitizeUrl } from './desensitizeUrl';

describe('desensitizeUrl', () => {
it('should desensitize a URL with a subdomain', () => {
const originalUrl = 'https://api.example.com/v1';
const result = desensitizeUrl(originalUrl);
expect(result).toBe('https://api.ex****le.com/v1');
});

it('should desensitize a URL without a subdomain', () => {
const originalUrl = 'https://example.com/v1';
const result = desensitizeUrl(originalUrl);
expect(result).toBe('https://ex****le.com/v1');
});

it('should desensitize a URL without a subdomain less then 5 chartarters', () => {
const originalUrl = 'https://abc.com/v1';
const result = desensitizeUrl(originalUrl);
expect(result).toBe('https://***.com/v1');
});

it('should desensitize a URL with multiple subdomains', () => {
const originalUrl = 'https://sub.api.example.com/v1';
const result = desensitizeUrl(originalUrl);
expect(result).toBe('https://sub.api.ex****le.com/v1');
});

it('should desensitize a URL with path and query parameters', () => {
const originalUrl = 'https://api.example.com/v1?query=123';
const result = desensitizeUrl(originalUrl);
expect(result).toBe('https://api.ex****le.com/v1?query=123');
});

it('should return the original URL if it is invalid', () => {
const originalUrl = 'invalidurl';
const result = desensitizeUrl(originalUrl);
expect(result).toBe(originalUrl);
});

it('should desensitize a URL with a port number', () => {
const originalUrl = 'https://api.example.com:8080/v1';
const result = desensitizeUrl(originalUrl);
expect(result).toBe('https://api.ex****le.com:****/v1');
});
});
34 changes: 34 additions & 0 deletions src/app/api/openai/chat/desensitizeUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export const desensitizeUrl = (url: string) => {
try {
const urlObj = new URL(url);
const hostnameParts = urlObj.hostname.split('.');
const port = urlObj.port;

// Desensitize domain only if there are at least two parts (example.com)
if (hostnameParts.length > 1) {
// Desensitize the second level domain (second part from the right)
// Special case for short domain names
const secondLevelDomainIndex = hostnameParts.length - 2;
if (hostnameParts[secondLevelDomainIndex].length < 5) {
hostnameParts[secondLevelDomainIndex] = '***';
} else {
hostnameParts[secondLevelDomainIndex] = hostnameParts[secondLevelDomainIndex].replace(
/^(.*?)(\w{2})(\w+)(\w{2})$/,
(_, prefix, start, middle, end) => `${prefix}${start}****${end}`,
);
}
}

// Join the hostname parts back together
const desensitizedHostname = hostnameParts.join('.');

// Desensitize port if present
const desensitizedPort = port ? ':****' : '';

// Reconstruct the URL with the desensitized parts
return `${urlObj.protocol}//${desensitizedHostname}${desensitizedPort}${urlObj.pathname}${urlObj.search}`;
} catch {
// If the URL is invalid, return the original URL
return url;
}
};

0 comments on commit ab0aeb7

Please sign in to comment.