Skip to content

Commit 9186271

Browse files
committed
feat(api): reject reversed array ranges (RI-8219)
ARGETRANGE / ARSCAN require start <= end. Match that contract at the API boundary: validate the order in the service and return 400 if reversed, instead of silently normalizing. Aligns with how every other Redis range command behaves and removes ambiguity about how a reversed reply would be ordered or paginated.
1 parent f65e26d commit 9186271

6 files changed

Lines changed: 24 additions & 20 deletions

File tree

redisinsight/api/.tscheck.rec.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,7 @@
370370
"TS2322": 2
371371
},
372372
"src/modules/browser/utils/clusterCursor.ts": {
373+
"TS2488": 1,
373374
"TS7005": 1,
374375
"TS7034": 1
375376
},
@@ -1206,9 +1207,6 @@
12061207
"src/modules/redis-sentinel/redis-sentinel.analytics.ts": {
12071208
"TS18048": 1
12081209
},
1209-
"src/modules/redis/client/ioredis/cluster.ioredis.client.ts": {
1210-
"TS2488": 1
1211-
},
12121210
"src/modules/redis/client/ioredis/ioredis.client.ts": {
12131211
"TS2345": 2,
12141212
"TS2532": 1

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export default {
8080
INCORRECT_CLUSTER_CURSOR_FORMAT: 'Incorrect cluster cursor format.',
8181
ARRAY_RANGE_TOO_LARGE: (max: number) =>
8282
`Requested range exceeds the maximum of ${numberWithSpaces(max)} elements per call. Narrow the range and try again.`,
83+
ARRAY_RANGE_REVERSED: 'Start index must be less than or equal to end index.',
8384
REMOVING_MULTIPLE_ELEMENTS_NOT_SUPPORT: () =>
8485
'Removing multiple elements is available for Redis databases v. 6.2 or later.',
8586
SCAN_PER_KEY_TYPE_NOT_SUPPORT: () =>

redisinsight/api/src/modules/browser/array/array.service.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,11 @@ describe('ArrayService', () => {
212212
).rejects.toThrow(BadRequestException);
213213
});
214214

215-
it('should reject when reversed range exceeds the 1M cap', async () => {
215+
it('should reject reversed ranges (start > end)', async () => {
216216
await expect(
217217
service.getRange(mockBrowserClientMetadata, {
218218
...mockGetArrayRangeDto,
219-
start: String(ARRAY_RANGE_MAX_ELEMENTS),
219+
start: '5',
220220
end: '0',
221221
}),
222222
).rejects.toThrow(BadRequestException);
@@ -378,11 +378,11 @@ describe('ArrayService', () => {
378378
).rejects.toThrow(BadRequestException);
379379
});
380380

381-
it('should reject when reversed range exceeds the 1M cap', async () => {
381+
it('should reject reversed ranges (start > end)', async () => {
382382
await expect(
383383
service.scan(mockBrowserClientMetadata, {
384384
...mockGetArrayScanDto,
385-
start: String(ARRAY_RANGE_MAX_ELEMENTS),
385+
start: '5',
386386
end: '0',
387387
}),
388388
).rejects.toThrow(BadRequestException);

redisinsight/api/src/modules/browser/array/array.service.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,18 @@ export class ArrayService {
8484
}
8585

8686
// Inputs are validated as canonical decimal strings ≤ 2^64-1, so BigInt()
87-
// is safe. Ranges are reversible (start > end).
88-
private assertRangeWithinCap(start: string, end: string): void {
87+
// is safe. The contract is start ≤ end — matches every Redis range
88+
// command and avoids any ambiguity about how a reversed reply would be
89+
// ordered or paginated.
90+
private assertValidRange(start: string, end: string): void {
8991
const startBig = BigInt(start);
9092
const endBig = BigInt(end);
91-
const span =
92-
startBig > endBig
93-
? startBig - endBig + BigInt(1)
94-
: endBig - startBig + BigInt(1);
9593

94+
if (startBig > endBig) {
95+
throw new BadRequestException(ERROR_MESSAGES.ARRAY_RANGE_REVERSED);
96+
}
97+
98+
const span = endBig - startBig + BigInt(1);
9699
if (span > BigInt(ARRAY_RANGE_MAX_ELEMENTS)) {
97100
throw new BadRequestException(
98101
ERROR_MESSAGES.ARRAY_RANGE_TOO_LARGE(ARRAY_RANGE_MAX_ELEMENTS),
@@ -108,7 +111,7 @@ export class ArrayService {
108111
this.logger.debug('Getting array range.', clientMetadata);
109112
const { keyName, start, end } = dto;
110113

111-
this.assertRangeWithinCap(start, end);
114+
this.assertValidRange(start, end);
112115

113116
const client =
114117
await this.databaseClientFactory.getOrCreateClient(clientMetadata);
@@ -183,10 +186,11 @@ export class ArrayService {
183186
const { keyName, start, end, limit } = dto;
184187

185188
// ARSCAN skips empty slots in the response but still walks the index
186-
// range server-side (O(|end-start|+1)). Apply the same span cap as
187-
// ARGETRANGE so an unbounded range cannot tie up Redis even when LIMIT
188-
// is omitted; LIMIT remains a complementary result-set cap.
189-
this.assertRangeWithinCap(start, end);
189+
// range server-side (O(|end-start|+1)). Apply the same range checks
190+
// as ARGETRANGE so an unbounded or malformed range cannot tie up
191+
// Redis even when LIMIT is omitted; LIMIT remains a complementary
192+
// result-set cap.
193+
this.assertValidRange(start, end);
190194

191195
const client =
192196
await this.databaseClientFactory.getOrCreateClient(clientMetadata);

redisinsight/api/src/modules/browser/array/dto/get.array-range.dto.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class GetArrayRangeDto extends KeyDto {
1515
@ApiProperty({
1616
description:
1717
'End index of the range (inclusive). Unsigned 64-bit integer as string. ' +
18-
'If start > end, the range is returned reversed.',
18+
'Must be greater than or equal to start.',
1919
type: String,
2020
example: '99',
2121
})

redisinsight/api/src/modules/browser/array/dto/get.array-scan.dto.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ export class GetArrayScanDto extends KeyDto {
1616

1717
@ApiProperty({
1818
description:
19-
'End index of the range (inclusive). Unsigned 64-bit integer as string.',
19+
'End index of the range (inclusive). Unsigned 64-bit integer as string. ' +
20+
'Must be greater than or equal to start.',
2021
type: String,
2122
example: '99',
2223
})

0 commit comments

Comments
 (0)