Summary
The provided S3 resource library cannot return the raw bytes of a binary object. For any object whose content type isn't on Retool's internal "binary" list, the resource UTFâ8âdecodes the body server-side and returns it as a Body: string with IsBase64: false. Once that decode happens, the original bytes are unrecoverableâinvalid UTFâ8 sequences are replaced with U+FFFD and the returned string is shorter than the source object.
We need a way to get the raw bytesâideally as a streamâout of the S3 client without Retool performing any text decoding on them.
Reproduction
Read any binary S3 object whose Content-Type is not on Retool's binary-content-type list (e.g., application/zstd). The returned Body is a string, IsBase64 is false, and Body.length is less than
ContentLength due to UTFâ8 replacement characters embedded in place of invalid byte sequences. There is no way to reconstruct the original bytes from the value the resource returns.
Same result via download and readStreamRaw
downloadâ sameBody: stringwith the same UTFâ8 replacement behaviour.readStreamRawâ the streamed chunks are the JSON envelope of the sameBodyâŁstring. TheBodyfield of that JSON has already been UTFâ8âdecoded server-side (replacement chars embedded directly in the JSON string literal), so reassembling the stream yields the exact same lossy payload as the bufferedread.
In other words, the *StreamRaw variant does not stream the underlying S3 object bytesâit streams the JSON envelope around an already-decoded Body, so it can't be used as a workaround.
Request
Please add one (or more) of the following to the internal Retool API for the
S3 resource. Any of these would unblock us; option 1 is the cleanest:
-
A truly raw
*StreamRawvariant ofread/downloadthat yields the S3 object bytes themselves (not a JSON envelope around a decodedBody). Concretely, on the backend the AsyncIterable should be chunk-by-chunk passthrough of the S3GetObjectresponse stream, with no UTFâ8 decoding and no JSON wrapping. Returning the iterable directly. from a serverless function (as the*StreamRawcontract already describes) would then expose it to the frontend hook's.streamasAsyncIterable<Uint8Array>of file bytes. -
A per-call option to force base64 encoding, e.g.
s3.read({ fileKey, responseType: 'base64' })or{ binary: true }. SetIsBase64: trueand returnBodyas a base64 string. This matches the existing documented behaviour ofIsBase64for binary objects, just optâin instead of relying on content-type sniffing. -
Surface the raw
GetObjectCommandOutput.Bodystream from the underlying@aws-sdk/client-s3call as an opt-in (raw: trueor a newreadRawmethod). Anything that exposes the SDK's existing readable stream untouched would work.