Getting corrupted strings when using ElastiCache Redis 7.1.0 since library version 5
I'm using Amazon ElastiCache running Redis 7.1.0 (I do realize this is unsupported version, but thought that this is worth reporting anyway).
When I'm running the following script:
import redis from 'redis'
async function main() {
const client = redis.createClient({
url: 'redis://my-elasticache-cluster:6379'
})
await client.connect()
console.log('Connected to Redis')
const value = Array.from({ length: 1000 }, (_) => 'ä').join('')
await client.set('test-key', value, {
EX: 5
})
const result = await client.get('test-key')
console.log('Value got from Redis:', result)
console.log('Has replacement character', result && result.includes('�'))
await client.disconnect()
}
main().catch((err) => {
console.error('Unhandled error:', err)
process.exit(1)
})
I sometimes get the following output (about 50% of the time), scroll right to see the error:
Connected to Redis
Value got from Redis: äääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääää��ääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääää
Has replacement character true
If I install pnpm i redis@^4, this never happens. I could not replicate this with local redis (but did not test the same version, since I could not find 7.1 images easily).
Speculation - somewhere there is a mechanism where data is coming in as a separate byte chunks, and these incomplete byte chunks are being read as UTF-8 strings, even though they might be broken mid-codepoint and the conversion should wait until the bytes are retrieved in full?
Just as an FYI I can reproduce this also on Eliasticache 7.1.0. Maybe it's enough to put up a warning about it.
Hi @Kauhsa, thanks for flagging this. Im not sure when I will have the time to look into it though
Hi,
It looks like I’ve encountered the same issue and was able to identify the root cause, along with three unit tests that reproduce the problem.
From what I can tell, the issue occurs in packages/client/lib/RESP/decoder.ts when decoding strings containing UTF-8 characters that require multiple bytes for encoding.
If a multi-byte character is split across two separate chunks, it is not correctly reconstructed as a single character.
The methods #continueDecodeSimpleString and #continueDecodeStringWithLength appear to be the source of the issue, as they directly join the string representation of each chucks instead of conbining the buffers into a single one an then decoding it as a string.
From my tests, replacing:
return type === Buffer ?
Buffer.concat(chunks) :
chunks.join('');
by
const buffer = Buffer.concat(chunks);
return type === Buffer ? buffer : buffer.toString();
should resolve the issue.
The following unit tests can be used to reproduce the issue (packages/client/lib/RESP/decoder.spec.ts):
SimpleString case:
test("'é'", {
toWrite: Buffer.from('+é\r\n'),
replies: ['é']
});
BlobString case:
test("'é'", {
toWrite: Buffer.from('$2\r\né\r\n'),
replies: ['é']
});
VerbatimString case:
test("'é'", {
toWrite: Buffer.from('=6\r\ntxt:é\r\n'),
replies: ['é']
});
All three tests pass when the data is processed as a single chunk but fail when processed byte by byte.
We are seeing this in production as well.
We are also observing this in production as well. It was very obscure, but luckily this issue helped us narrow down why it happened.
Edit: As this affects our production, we see no other option other than to downgrade to v4.
We were bit this bug as well. @msebire did you make a Pull Request for this? It seems like you have a working solution.
We also ran into this issue and it was driving us crazy for weeks before I came across this issue. If you don't want to downgrade, this workaround solved the issue for us (inspired by @msebire 's explanation). You just use the withTypeMapping() option return strings as buffers instead of strings
So instead of this:
// content is a string
const content = await redis.hGet(key, filePath)
return content // intermittent corruption of multi-byte UTF-8 characters
do this:
// content is a Buffer
const content = await redis
.withTypeMapping({
[RESP_TYPES.BLOB_STRING]: Buffer,
})
.hGet(key, filePath)
return content?.toString("utf-8") || null // no corruption so far? 🤞