diff --git a/packages/standard-server-node/src/body.test.ts b/packages/standard-server-node/src/body.test.ts index f79b6e3ec..59d45971c 100644 --- a/packages/standard-server-node/src/body.test.ts +++ b/packages/standard-server-node/src/body.test.ts @@ -16,6 +16,29 @@ beforeEach(() => { vi.clearAllMocks() }) +function createChunkedRequest(contentType: string, chunks: Buffer[]): IncomingMessage { + const request = Readable.from(chunks) as IncomingMessage + request.headers = { + 'content-type': contentType, + } + return request +} + +function splitBufferInsideCharacter(text: string, splitCharacter: string): Buffer[] { + const buffer = Buffer.from(text) + const splitBytes = Buffer.from(splitCharacter) + const splitIndex = buffer.indexOf(splitBytes) + + if (splitIndex === -1) { + throw new Error(`split character not found: ${splitCharacter}`) + } + + return [ + buffer.subarray(0, splitIndex + 1), + buffer.subarray(splitIndex + 1), + ] +} + describe('toStandardBody', () => { it('undefined', async () => { let standardBody: StandardBody = {} as any @@ -51,6 +74,42 @@ describe('toStandardBody', () => { expect(standardBody).toEqual({ foo: 'bar' }) }) + it('json with utf-8 characters split across chunk boundaries', async () => { + const original = { + json: { + text: '滚滚长江东逝水', + }, + } + + const chunks = splitBufferInsideCharacter(JSON.stringify(original), '江') + const request = createChunkedRequest('application/json', chunks) + + const standardBody = await toStandardBody(request) + + expect(standardBody).toEqual(original) + }) + + it('text with utf-8 characters split across chunk boundaries', async () => { + const original = '海内存知己,天涯若比邻' + const chunks = splitBufferInsideCharacter(original, '存') + const request = createChunkedRequest('text/plain', chunks) + + const standardBody = await toStandardBody(request) + + expect(standardBody).toBe(original) + }) + + it('text with utf-8 characters split across chunk boundaries end with incomplete utf8', async () => { + const original = '海内存知己,天涯若比邻' + const chunks = splitBufferInsideCharacter(original, '存') + const incompleteUtf8 = Buffer.from([230, 181]) + const request = createChunkedRequest('text/plain', [...chunks, incompleteUtf8]) + + const standardBody = await toStandardBody(request) + + expect(standardBody).toBe(`${original}�`) + }) + it('json but empty body', async () => { let standardBody: StandardBody = {} as any diff --git a/packages/standard-server-node/src/body.ts b/packages/standard-server-node/src/body.ts index ee9cfcec3..2cb2582ff 100644 --- a/packages/standard-server-node/src/body.ts +++ b/packages/standard-server-node/src/body.ts @@ -1,7 +1,7 @@ import type { StandardBody, StandardHeaders } from '@orpc/standard-server' -import type { Buffer } from 'node:buffer' import type { ToEventIteratorOptions, ToEventStreamOptions } from './event-iterator' import type { NodeHttpRequest } from './types' +import { Buffer } from 'node:buffer' import { Readable } from 'node:stream' import { isAsyncIteratorObject, parseEmptyableJSON, runWithSpan, stringifyJSON } from '@orpc/shared' import { flattenHeader, generateContentDisposition, getFilenameFromContentDisposition } from '@orpc/standard-server' @@ -114,13 +114,16 @@ function _streamToFormData(stream: Readable, contentType: string): Promise { - let string = '' + const decoder = new TextDecoder() + let text = '' for await (const chunk of stream) { - string += chunk.toString() + text += decoder.decode(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk), { stream: true }) } - return string + text += decoder.decode() + + return text } async function _streamToFile(stream: Readable, fileName: string, contentType: string): Promise {