Skip to content

Commit fb42e8b

Browse files
committed
fix(render): strip NULL bytes from readStream output to prevent email truncation
1 parent 1360dd9 commit fb42e8b

2 files changed

Lines changed: 41 additions & 1 deletion

File tree

packages/render/src/node/read-stream.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,8 @@ export const readStream = async (
5454
});
5555
}
5656

57-
return result;
57+
// Strip NULL bytes (U+0000) that can appear when React's streaming renderer
58+
// produces chunks that split multi-byte UTF-8 characters at chunk boundaries.
59+
// The pretty() function already does this, but the default non-pretty path did not.
60+
return result.replaceAll('\0', '');
5861
};

packages/render/src/node/render-node.spec.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,43 @@ describe('render on node environments', () => {
150150
*
151151
* @see https://github.com/resend/react-email/issues/2353
152152
*/
153+
// Regression test for https://github.com/resend/react-email/issues/1667
154+
// and https://github.com/resend/react-email/issues/1932
155+
//
156+
// React's streaming renderer can produce chunks that split multi-byte UTF-8
157+
// characters at chunk boundaries, resulting in NULL bytes (U+0000) in the
158+
// rendered output. Email clients treat NULL as a string terminator, causing
159+
// emails to be truncated mid-content.
160+
it('rendered output contains no NULL bytes with multi-byte characters', async () => {
161+
const MultiByteTemplate = () => {
162+
const paragraphs = Array(30)
163+
.fill(null)
164+
.map((_, i) => (
165+
<p key={i}>
166+
段落{i}:日本語のテキストを含むメールテンプレートのテストです。
167+
Ärzte und Ünternehmen für Lösung und Grüße aus München.
168+
Тестовая компания приветствует вас в нашем сервисе.
169+
이메일 템플릿 테스트를 위한 한국어 텍스트입니다.
170+
</p>
171+
));
172+
173+
return (
174+
<div>
175+
<h1>テスト会社ABCははハハtestあ</h1>
176+
{paragraphs}
177+
</div>
178+
);
179+
};
180+
181+
const html = await render(<MultiByteTemplate />);
182+
183+
expect(html).not.toContain('\0');
184+
expect(html).toContain('テスト会社ABCははハハtestあ');
185+
expect(html).toContain('日本語のテキスト');
186+
expect(html).toContain('Ünternehmen');
187+
expect(html).toContain('Тестовая');
188+
});
189+
153190
it('renders large emails without hydration markers', async () => {
154191
const LargeEmailTemplate = () => {
155192
const largeContent = Array(100)

0 commit comments

Comments
 (0)