Skip to content

Commit dd72366

Browse files
committed
fix(api): add proper error messages for invalid cloud database endpoints
Validate publicEndpoint before calling split() to prevent null reference errors. Return user-friendly error messages instead of raw JavaScript errors. References: #RI-7778
1 parent 301013a commit dd72366

File tree

10 files changed

+122
-1
lines changed

10 files changed

+122
-1
lines changed

redisinsight/api/src/constants/custom-error-codes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export enum CustomErrorCodes {
4747
CloudJobNotFound = 11_113,
4848
CloudSubscriptionAlreadyExistsFree = 11_114,
4949
CloudDatabaseImportForbidden = 11_115,
50+
CloudDatabaseEndpointInvalid = 11_116,
5051

5152
// General database errors [11200, 11299]
5253
DatabaseAlreadyExists = 11_200,

redisinsight/api/src/constants/error-messages.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ export default {
144144
CLOUD_DATABASE_ALREADY_EXISTS_FREE: 'Free database already exists',
145145
CLOUD_DATABASE_IMPORT_FORBIDDEN:
146146
'Adding your Redis Cloud database to Redis Insight is disabled due to a setting restricting database connection management.',
147+
CLOUD_DATABASE_ENDPOINT_INVALID:
148+
'Database endpoint is not available or not fully provisioned yet.',
147149
CLOUD_PLAN_NOT_FOUND_FREE: 'Unable to find free cloud plan',
148150
CLOUD_SUBSCRIPTION_ALREADY_EXISTS_FREE: 'Free subscription already exists',
149151
COMMON_DEFAULT_IMPORT_ERROR: 'Unable to import default data',

redisinsight/api/src/modules/cloud/autodiscovery/cloud-autodiscovery.service.spec.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptio
2929
import { CloudUserCapiService } from 'src/modules/cloud/user/cloud-user.capi.service';
3030
import { CloudSubscriptionCapiService } from 'src/modules/cloud/subscription/cloud-subscription.capi.service';
3131
import { CloudDatabaseCapiService } from 'src/modules/cloud/database/cloud-database.capi.service';
32+
import ERROR_MESSAGES from 'src/constants/error-messages';
33+
import { CustomErrorCodes } from 'src/constants';
3234

3335
describe('CloudAutodiscoveryService', () => {
3436
let service: CloudAutodiscoveryService;
@@ -364,5 +366,62 @@ describe('CloudAutodiscoveryService', () => {
364366
mockImportCloudDatabaseResponseFixed,
365367
]);
366368
});
369+
it.each([
370+
{
371+
description: 'null',
372+
publicEndpoint: null,
373+
},
374+
{
375+
description: 'undefined',
376+
publicEndpoint: undefined,
377+
},
378+
{
379+
description: 'empty string',
380+
publicEndpoint: '',
381+
},
382+
{
383+
description: 'whitespace only',
384+
publicEndpoint: ' ',
385+
},
386+
{
387+
description: 'does not contain colon',
388+
publicEndpoint: 'hostname',
389+
},
390+
])(
391+
'should return error when publicEndpoint is $description',
392+
async ({ publicEndpoint }) => {
393+
cloudDatabaseCapiService.getDatabase.mockResolvedValueOnce({
394+
...mockCloudDatabase,
395+
publicEndpoint,
396+
status: CloudDatabaseStatus.Active,
397+
});
398+
399+
const result = await service.addRedisCloudDatabases(
400+
mockSessionMetadata,
401+
mockCloudCapiAuthDto,
402+
[mockImportCloudDatabaseDto],
403+
);
404+
405+
expect(result).toEqual([
406+
{
407+
...mockImportCloudDatabaseResponse,
408+
status: ActionStatus.Fail,
409+
message: ERROR_MESSAGES.CLOUD_DATABASE_ENDPOINT_INVALID,
410+
error: {
411+
message: ERROR_MESSAGES.CLOUD_DATABASE_ENDPOINT_INVALID,
412+
statusCode: 400,
413+
error: 'CloudDatabaseEndpointInvalid',
414+
errorCode: CustomErrorCodes.CloudDatabaseEndpointInvalid,
415+
},
416+
databaseDetails: {
417+
...mockCloudDatabase,
418+
publicEndpoint,
419+
status: CloudDatabaseStatus.Active,
420+
},
421+
},
422+
]);
423+
expect(databaseService.create).not.toHaveBeenCalled();
424+
},
425+
);
367426
});
368427
});

redisinsight/api/src/modules/cloud/autodiscovery/cloud-autodiscovery.service.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
Logger,
44
ServiceUnavailableException,
55
} from '@nestjs/common';
6+
import { CloudDatabaseEndpointInvalidException } from 'src/modules/cloud/job/exceptions';
67
import { uniqBy } from 'lodash';
78
import ERROR_MESSAGES from 'src/constants/error-messages';
89
import {
@@ -29,6 +30,7 @@ import {
2930
CloudDatabase,
3031
CloudDatabaseStatus,
3132
} from 'src/modules/cloud/database/models';
33+
import { isValidCloudDatabaseEndpoint } from 'src/modules/cloud/database/utils';
3234
import config from 'src/utils/config';
3335

3436
const cloudConfig = config.get('cloud');
@@ -222,6 +224,16 @@ export class CloudAutodiscoveryService {
222224
databaseDetails: database,
223225
};
224226
}
227+
if (!isValidCloudDatabaseEndpoint(publicEndpoint)) {
228+
const exception = new CloudDatabaseEndpointInvalidException();
229+
return {
230+
...dto,
231+
status: ActionStatus.Fail,
232+
message: exception.message,
233+
error: exception?.getResponse(),
234+
databaseDetails: database,
235+
};
236+
}
225237
const [host, port] = publicEndpoint.split(':');
226238

227239
await this.databaseService.create(sessionMetadata, {

redisinsight/api/src/modules/cloud/database/utils/cloud-data-converter.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ export function convertRedisCloudModuleName(name: string): string {
1717
return REDIS_CLOUD_MODULES_NAMES[name] ?? name;
1818
}
1919

20+
export function isValidCloudDatabaseEndpoint(
21+
publicEndpoint: string | null | undefined,
22+
): boolean {
23+
return (
24+
!!publicEndpoint && !!publicEndpoint.trim() && publicEndpoint.includes(':')
25+
);
26+
}
27+
2028
export const parseCloudDatabaseCapiResponse = (
2129
database: ICloudCapiDatabase,
2230
tags: ICloudCapiDatabaseTag[],
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {
2+
HttpException,
3+
HttpExceptionOptions,
4+
HttpStatus,
5+
} from '@nestjs/common';
6+
import ERROR_MESSAGES from 'src/constants/error-messages';
7+
import { CustomErrorCodes } from 'src/constants';
8+
9+
export class CloudDatabaseEndpointInvalidException extends HttpException {
10+
constructor(
11+
message = ERROR_MESSAGES.CLOUD_DATABASE_ENDPOINT_INVALID,
12+
options?: HttpExceptionOptions,
13+
) {
14+
const response = {
15+
message,
16+
statusCode: HttpStatus.BAD_REQUEST,
17+
error: 'CloudDatabaseEndpointInvalid',
18+
errorCode: CustomErrorCodes.CloudDatabaseEndpointInvalid,
19+
};
20+
21+
super(response, response.statusCode, options);
22+
}
23+
}

redisinsight/api/src/modules/cloud/job/exceptions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './cloud-database-already-exists-free.exception';
2+
export * from './cloud-database-endpoint-invalid.exception';
23
export * from './cloud-database-import-forbidden.exception';
34
export * from './cloud-database-in-failed-state.exception';
45
export * from './cloud-database-in-unexpected-state.exception';

redisinsight/api/src/modules/cloud/job/jobs/create-free-database.cloud-job.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { WaitForActiveDatabaseCloudJob } from 'src/modules/cloud/job/jobs/wait-f
1515
import { CloudJobName } from 'src/modules/cloud/job/constants';
1616
import { CloudJobStatus, CloudJobStep } from 'src/modules/cloud/job/models';
1717
import {
18+
CloudDatabaseEndpointInvalidException,
1819
CloudDatabaseImportForbiddenException,
1920
CloudJobUnexpectedErrorException,
2021
CloudTaskNoResourceIdException,
@@ -30,6 +31,7 @@ import { ClientContext, SessionMetadata } from 'src/common/models';
3031
import { DatabaseInfoService } from 'src/modules/database/database-info.service';
3132
import { FeatureService } from 'src/modules/feature/feature.service';
3233
import { KnownFeatures } from 'src/modules/feature/constants';
34+
import { isValidCloudDatabaseEndpoint } from 'src/modules/cloud/database/utils';
3335

3436
const cloudConfig = config.get('cloud');
3537

@@ -138,6 +140,10 @@ export class CreateFreeDatabaseCloudJob extends CloudJob {
138140

139141
const { publicEndpoint, name, password } = cloudDatabase;
140142

143+
if (!isValidCloudDatabaseEndpoint(publicEndpoint)) {
144+
throw new CloudDatabaseEndpointInvalidException();
145+
}
146+
141147
const [host, port] = publicEndpoint.split(':');
142148

143149
const database = await this.dependencies.databaseService.create(

redisinsight/api/src/modules/cloud/job/jobs/import-free-database.cloud-job.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ import { CloudCapiKeyService } from 'src/modules/cloud/capi-key/cloud-capi-key.s
1616
import { SessionMetadata } from 'src/common/models';
1717
import { KnownFeatures } from 'src/modules/feature/constants';
1818
import { FeatureService } from 'src/modules/feature/feature.service';
19-
import { CloudDatabaseImportForbiddenException } from 'src/modules/cloud/job/exceptions';
19+
import {
20+
CloudDatabaseEndpointInvalidException,
21+
CloudDatabaseImportForbiddenException,
22+
} from 'src/modules/cloud/job/exceptions';
23+
import { isValidCloudDatabaseEndpoint } from 'src/modules/cloud/database/utils';
2024

2125
const cloudConfig = config.get('cloud');
2226

@@ -82,6 +86,10 @@ export class ImportFreeDatabaseCloudJob extends CloudJob {
8286

8387
const { publicEndpoint, name, password } = cloudDatabase;
8488

89+
if (!isValidCloudDatabaseEndpoint(publicEndpoint)) {
90+
throw new CloudDatabaseEndpointInvalidException();
91+
}
92+
8593
const [host, port] = publicEndpoint.split(':');
8694

8795
const database = await this.dependencies.databaseService.create(

redisinsight/ui/src/constants/customErrorCodes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export enum CustomErrorCodes {
4747
CloudJobNotFound = 11_113,
4848
CloudSubscriptionAlreadyExistsFree = 11_114,
4949
CloudDatabaseImportForbidden = 11_115,
50+
CloudDatabaseEndpointInvalid = 11_116,
5051

5152
// General database errors [11200, 11299]
5253
DatabaseAlreadyExists = 11_200,

0 commit comments

Comments
 (0)