Proposal: response.base64()
What problem are you trying to solve?
Problem Statement
Currently, when working with the Fetch API, converting response data to base64 encoding requires multiple steps and introduces complexity, especially when handling binary data. Developers often need to:
- Read the response as a blob or array buffer
- Convert it to base64 using additional utilities
- Handle potential encoding issues
This process is error-prone and requires additional code that could be standardized.
What solutions exist today?
Existing Solutions
Currently, developers typically handle this in one of these ways:
- Using FileReader:
const response = await fetch(url);
const blob = await response.blob();
const reader = new FileReader();
reader.readAsDataURL(blob);
await new Promise(resolve => reader.onload = resolve);
const base64 = reader.result.split(',')[1];
- Using array buffers and manual conversion:
const response = await fetch(url);
const buffer = await response.arrayBuffer();
const bytes = new Uint8Array(buffer);
const binary = bytes.reduce((data, byte) => data + String.fromCharCode(byte), '');
const base64 = btoa(binary);
Both approaches have drawbacks:
- Verbose and complex code
- Performance overhead from multiple conversions
- Inconsistent handling across different types of content
- Memory inefficient due to multiple data copies
How would you solve it?
Proposed Solution
Add a new method base64() to the Response prototype:
interface Response {
base64(): Promise<string>;
}
Example usage:
const response = await fetch('https://example.com/image.png');
const base64Data = await response.base64();
Implementation Details
The method would:
- Efficiently read the response body
- Convert it directly to base64 encoding
- Return a Promise that resolves with the base64 string
Benefits
- Simplicity: Single method call instead of complex conversion chains
- Performance: Native implementation can optimize the conversion process
- Memory efficiency: Avoid unnecessary intermediate copies
- Standardization: Consistent behavior across browsers
- Error handling: Built-in handling of encoding edge cases
Compatibility
The method name base64() is not currently used in the Response prototype, making it safe to add. The method would return a Promise to maintain consistency with other Response body reading methods.
Additional Considerations
Security
- The method should respect same-origin policies
- Large responses should be handled efficiently to prevent memory issues
- Consider adding optional parameters for handling different encodings
Edge Cases
- Handle empty responses
- Consider adding options for URL-safe base64 encoding
- Define behavior for streaming responses
Performance Impact
- Native implementation can optimize the conversion process
- Consider chunked processing for large responses
- Potential for WebAssembly acceleration
Anything else?
Open Questions
- Should there be a size limit for automatic base64 conversion?
- Should streaming be supported for large responses?
- Should there be options for different base64 variants (standard, URL-safe)?
I think now it'd be bytes.toBase64() in your 2nd example there?
related https://github.com/tc39/proposal-arraybuffer-base64
Having await response.base64() (where it takes the same options that ArrayBuffer.prototype.toBase64` does) seems fine
Yes, was following proposal-arraybuffer-base64 but again we would still need to do a new Uint8Array(await response.arrayBuffer()).toBase64() rather response.base64() is convenient and intuitive.
(await response.bytes()).toBase64() is pretty short. If a lot of code ends up doing that maybe we should provide a shortcut, but let's wait a couple of years for the other APIs to become more readily available to see if there's actually such a trend.
@annevk await response.bytes() just gives you a buffer, you'd need new Uint8Array(await response.arrayBuffer()).toBase64() which is a bit longer.
No, it gives you a view (of type Uint8Array). arrayBuffer() gives you a buffer.
oh! in that case yeah it seems short enough :-)
Ah! Okies.
but let's wait a couple of years for the other APIs to become more readily available
+1
I don't think we should even encourage ppl to use base64 when we have better containers to handle bytes. base64 takes up more RAM and size and other overhead by converting data to and from bytes -> string -> bytes
Base64 encoding typically increases the size of the original data by about 33% because it converts every 3 bytes of binary data into 4 bytes of encoded text. This overhead occurs because Base64 uses a set of 64 characters to represent the data, which requires more space than the original binary format.
if the intent was to show a picture with <img> doing something like:
const response = await fetch(url); // with credentials or other METHOD other than GET
const blob = await response.blob();
const reader = new FileReader();
reader.readAsDataURL(blob);
await new Promise(resolve => reader.onload = resolve);
const base64 = reader.result
const image = new Image()
img.src = base64
document.body.append(img)
then it has to waste process of converting bytes to base64 (only so that you can assign img.src with a base64 url only for browser to have to convert that img.src url back into bytes again.
a cleaner solution would be to do:
const response = await fetch(url); // with credentials or other METHOD other than GET
const blob = await response.blob();
const image = URL.createObjectURL(blob)
img.src = base64
document.body.append(img)
then browser can read the bytes directly from the blob without having to do: bytes -> base64 string -> bytes
your proposal is good, but it lacks reasoning for why we even need base64 in the first place.