Skip to content

Commit 11dd853

Browse files
committed
refactor: simplify body handling in request/response message decoding
1 parent cb69ab2 commit 11dd853

File tree

6 files changed

+67
-80
lines changed

6 files changed

+67
-80
lines changed

packages/standard-server-peer/src/client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import type { StandardRequest, StandardResponse } from '@orpc/standard-server'
33
import type { EventIteratorPayload } from './codec'
44
import type { EncodedMessage, EncodedMessageSendFn } from './types'
55
import { AsyncIdQueue, isAsyncIteratorObject, SequentialIdGenerator } from '@orpc/shared'
6-
import { decodeResponseMessage, encodeRequestMessage, isEventIteratorHeaders, MessageType } from './codec'
6+
import { isEventIteratorHeaders } from '@orpc/standard-server'
7+
import { decodeResponseMessage, encodeRequestMessage, MessageType } from './codec'
78
import { resolveEventIterator, toEventIterator } from './event-iterator'
89

910
export interface ClientPeerCloseOptions extends AsyncIdQueueCloseOptions {

packages/standard-server-peer/src/codec.test.ts

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { StandardHeaders } from '@orpc/standard-server'
2-
import { decodeRequestMessage, decodeResponseMessage, encodeRequestMessage, encodeResponseMessage, isEventIteratorHeaders, MessageType } from './codec'
2+
import { decodeRequestMessage, decodeResponseMessage, encodeRequestMessage, encodeResponseMessage, MessageType } from './codec'
33

44
const MB10Headers: StandardHeaders = {}
55

@@ -317,7 +317,7 @@ describe('encode/decode request message', () => {
317317
body: blob,
318318
})
319319

320-
expect(message).toBeInstanceOf(ArrayBuffer)
320+
expect(message).toBeTypeOf('string')
321321

322322
const [id, type, payload] = await decodeRequestMessage(message)
323323

@@ -410,7 +410,7 @@ describe('encode/decode request message', () => {
410410
body: file,
411411
})
412412

413-
expect(message).toBeInstanceOf(ArrayBuffer)
413+
expect(message).toBeTypeOf('string')
414414

415415
const [id, type, payload] = await decodeRequestMessage(message)
416416

@@ -835,22 +835,3 @@ describe('encode/decode response message', () => {
835835
expect(await (payload as any).body.text()).toBe(json)
836836
})
837837
})
838-
839-
it('isEventIteratorHeaders', () => {
840-
expect(isEventIteratorHeaders({
841-
'content-type': 'text/event-stream',
842-
})).toBe(true)
843-
844-
expect(isEventIteratorHeaders({
845-
'content-type': 'text/event-stream',
846-
'content-disposition': '',
847-
})).toBe(false)
848-
849-
expect(isEventIteratorHeaders({
850-
'content-type': 'text/plain',
851-
})).toBe(false)
852-
853-
expect(isEventIteratorHeaders({
854-
'content-disposition': 'attachment; filename="test.pdf"',
855-
})).toBe(false)
856-
})

packages/standard-server-peer/src/codec.ts

Lines changed: 36 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export async function encodeRequestMessage<T extends keyof RequestMessageMap>(
114114

115115
const request = payload as RequestMessageMap[MessageType.REQUEST]
116116

117-
const { body: processedBody, headers: processedHeaders } = await prepareBodyAndHeadersForSerialization(
117+
const { body: processedBody, headers: processedHeaders } = await serializeBodyAndHeaders(
118118
request.body,
119119
request.headers,
120120
)
@@ -139,7 +139,7 @@ export async function encodeRequestMessage<T extends keyof RequestMessageMap>(
139139
}
140140

141141
export async function decodeRequestMessage(raw: EncodedMessage): Promise<DecodedRequestMessage> {
142-
const { json: message, blobData } = await decodeRawMessage(raw)
142+
const { json: message, buffer } = await decodeRawMessage(raw)
143143

144144
const id: string = message.i
145145
const type: MessageType = message.t
@@ -157,26 +157,7 @@ export async function decodeRequestMessage(raw: EncodedMessage): Promise<Decoded
157157
const payload = message.p as SerializedRequestPayload
158158

159159
const headers = payload.h ?? {}
160-
let body: StandardBody = payload.b
161-
162-
const contentType = flattenHeader(headers['content-type'])
163-
164-
if (blobData) {
165-
const contentDisposition = flattenHeader(headers['content-disposition'])
166-
167-
if (contentDisposition === undefined && contentType?.startsWith('multipart/form-data')) {
168-
const tempRes = new Response(blobData, { headers: { 'content-type': contentType } })
169-
body = await tempRes.formData()
170-
}
171-
else {
172-
// if the body is a file, contentDisposition must be defined
173-
const filename = getFilenameFromContentDisposition(contentDisposition!) ?? 'blob'
174-
body = new File([blobData], filename, { type: contentType })
175-
}
176-
}
177-
else if (contentType?.startsWith('application/x-www-form-urlencoded') && typeof body === 'string') {
178-
body = new URLSearchParams(body)
179-
}
160+
const body = await deserializeBody(headers, payload.b, buffer)
180161

181162
return [id, MessageType.REQUEST, { url: new URL(payload.u, 'orpc:/'), headers, method: payload.m ?? 'POST', body }]
182163
}
@@ -201,7 +182,7 @@ export async function encodeResponseMessage<T extends keyof ResponseMessageMap>(
201182
}
202183

203184
const response = payload as StandardResponse
204-
const { body: processedBody, headers: processedHeaders } = await prepareBodyAndHeadersForSerialization(
185+
const { body: processedBody, headers: processedHeaders } = await serializeBodyAndHeaders(
205186
response.body,
206187
response.headers,
207188
)
@@ -225,7 +206,7 @@ export async function encodeResponseMessage<T extends keyof ResponseMessageMap>(
225206
}
226207

227208
export async function decodeResponseMessage(raw: EncodedMessage): Promise<DecodedResponseMessage> {
228-
const { json: message, blobData } = await decodeRawMessage(raw)
209+
const { json: message, buffer } = await decodeRawMessage(raw)
229210

230211
const id: string = message.i
231212
const type: MessageType | undefined = message.t
@@ -243,32 +224,16 @@ export async function decodeResponseMessage(raw: EncodedMessage): Promise<Decode
243224
const payload = message.p as SerializedResponsePayload
244225

245226
const headers = payload.h ?? {}
246-
let body: StandardBody = payload.b
247-
248-
const contentType = flattenHeader(headers['content-type'])
249-
250-
if (blobData) {
251-
const contentDisposition = flattenHeader(headers['content-disposition'])
252-
253-
// Handle FormData specifically
254-
if (contentDisposition === undefined && contentType?.startsWith('multipart/form-data')) {
255-
const tempRes = new Response(blobData, { headers: { 'content-type': contentType } })
256-
body = await tempRes.formData()
257-
}
258-
else {
259-
// if the body is a file, contentDisposition must be defined
260-
const filename = getFilenameFromContentDisposition(contentDisposition!) ?? 'blob'
261-
body = new File([blobData], filename, { type: contentType })
262-
}
263-
}
264-
else if (contentType?.startsWith('application/x-www-form-urlencoded') && typeof body === 'string') {
265-
body = new URLSearchParams(body)
266-
}
227+
const body = await deserializeBody(headers, payload.b, buffer)
267228

268229
return [id, MessageType.RESPONSE, { status: payload.s ?? 200, headers, body }]
269230
}
270231

271-
async function prepareBodyAndHeadersForSerialization(
232+
/**
233+
* Helper to deal with body and headers
234+
*/
235+
236+
async function serializeBodyAndHeaders(
272237
body: StandardBody,
273238
originalHeaders: StandardHeaders | undefined,
274239
): Promise<{ body: StandardBody | Blob | string | undefined, headers: StandardHeaders }> {
@@ -307,8 +272,25 @@ async function prepareBodyAndHeadersForSerialization(
307272
return { body, headers }
308273
}
309274

310-
export function isEventIteratorHeaders(headers: StandardHeaders): boolean {
311-
return Boolean(flattenHeader(headers['content-type'])?.startsWith('text/event-stream') && headers['content-disposition'] === undefined)
275+
async function deserializeBody(headers: StandardHeaders, body: unknown, buffer: Uint8Array | undefined): Promise<StandardBody> {
276+
const contentType = flattenHeader(headers['content-type'])
277+
const contentDisposition = flattenHeader(headers['content-disposition'])
278+
279+
if (typeof contentDisposition === 'string') {
280+
const filename = getFilenameFromContentDisposition(contentDisposition) ?? 'blob'
281+
return new File(buffer === undefined ? [] : [buffer], filename, { type: contentType })
282+
}
283+
284+
if (contentType?.startsWith('multipart/form-data')) {
285+
const tempRes = new Response(buffer, { headers: { 'content-type': contentType } })
286+
return tempRes.formData()
287+
}
288+
289+
if (contentType?.startsWith('application/x-www-form-urlencoded') && typeof body === 'string') {
290+
return new URLSearchParams(body)
291+
}
292+
293+
return body
312294
}
313295

314296
/**
@@ -318,21 +300,21 @@ export function isEventIteratorHeaders(headers: StandardHeaders): boolean {
318300
*/
319301
const JSON_AND_BINARY_DELIMITER = 0xFF
320302

321-
async function encodeRawMessage(data: object, blobData?: Blob): Promise<EncodedMessage> {
303+
async function encodeRawMessage(data: object, blob?: Blob): Promise<EncodedMessage> {
322304
const json = stringifyJSON(data)
323305

324-
if (blobData === undefined) {
306+
if (blob === undefined || blob.size === 0) {
325307
return json
326308
}
327309

328310
return new Blob([
329311
new TextEncoder().encode(json),
330312
new Uint8Array([JSON_AND_BINARY_DELIMITER]),
331-
blobData,
313+
blob,
332314
]).arrayBuffer()
333315
}
334316

335-
async function decodeRawMessage(raw: EncodedMessage): Promise<{ json: any, blobData?: Uint8Array }> {
317+
async function decodeRawMessage(raw: EncodedMessage): Promise<{ json: any, buffer?: Uint8Array }> {
336318
if (typeof raw === 'string') {
337319
return { json: JSON.parse(raw) }
338320
}
@@ -347,10 +329,10 @@ async function decodeRawMessage(raw: EncodedMessage): Promise<{ json: any, blobD
347329
}
348330

349331
const jsonPart = new TextDecoder().decode(buffer.subarray(0, delimiterIndex))
350-
const blobData = buffer.subarray(delimiterIndex + 1)
332+
const bufferPart = buffer.subarray(delimiterIndex + 1)
351333

352334
return {
353335
json: JSON.parse(jsonPart),
354-
blobData,
336+
buffer: bufferPart,
355337
}
356338
}

packages/standard-server-peer/src/server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import type { StandardRequest, StandardResponse } from '@orpc/standard-server'
33
import type { EventIteratorPayload } from './codec'
44
import type { EncodedMessage, EncodedMessageSendFn } from './types'
55
import { AsyncIdQueue, isAsyncIteratorObject } from '@orpc/shared'
6-
import { experimental_HibernationEventIterator } from '@orpc/standard-server'
7-
import { decodeRequestMessage, encodeResponseMessage, isEventIteratorHeaders, MessageType } from './codec'
6+
import { experimental_HibernationEventIterator, isEventIteratorHeaders } from '@orpc/standard-server'
7+
import { decodeRequestMessage, encodeResponseMessage, MessageType } from './codec'
88
import { resolveEventIterator, toEventIterator } from './event-iterator'
99

1010
export interface ServerPeerCloseOptions extends AsyncIdQueueCloseOptions {

packages/standard-server/src/utils.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { StandardLazyResponse } from './types'
22
import * as SharedModule from '@orpc/shared'
3-
import { flattenHeader, generateContentDisposition, getFilenameFromContentDisposition, mergeStandardHeaders, replicateStandardLazyResponse } from './utils'
3+
import { flattenHeader, generateContentDisposition, getFilenameFromContentDisposition, isEventIteratorHeaders, mergeStandardHeaders, replicateStandardLazyResponse } from './utils'
44

55
const replicateAsyncIteratorSpy = vi.spyOn(SharedModule, 'replicateAsyncIterator')
66

@@ -122,3 +122,22 @@ describe('replicateStandardLazyResponse', () => {
122122
expect(replicateAsyncIteratorSpy).toHaveBeenCalledWith(iterator, 3)
123123
})
124124
})
125+
126+
it('isEventIteratorHeaders', () => {
127+
expect(isEventIteratorHeaders({
128+
'content-type': 'text/event-stream',
129+
})).toBe(true)
130+
131+
expect(isEventIteratorHeaders({
132+
'content-type': 'text/event-stream',
133+
'content-disposition': '',
134+
})).toBe(false)
135+
136+
expect(isEventIteratorHeaders({
137+
'content-type': 'text/plain',
138+
})).toBe(false)
139+
140+
expect(isEventIteratorHeaders({
141+
'content-disposition': 'attachment; filename="test.pdf"',
142+
})).toBe(false)
143+
})

packages/standard-server/src/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,7 @@ export function replicateStandardLazyResponse(
9191

9292
return replicated
9393
}
94+
95+
export function isEventIteratorHeaders(headers: StandardHeaders): boolean {
96+
return Boolean(flattenHeader(headers['content-type'])?.startsWith('text/event-stream') && flattenHeader(headers['content-disposition']) === undefined)
97+
}

0 commit comments

Comments
 (0)