Skip to content

Commit 5269bd8

Browse files
fix: raw response handling (#5276)
1 parent bb410f8 commit 5269bd8

File tree

9 files changed

+122
-39
lines changed

9 files changed

+122
-39
lines changed

packages/react-start-client/src/tests/createServerFn.test-d.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { expectTypeOf, test } from 'vitest'
22
import { createServerFn } from '@tanstack/start-client-core'
33

4+
/*
5+
// disabled until we really support RSC
46
test.skip('createServerFn returns RSC', () => {
57
const fn = createServerFn().handler(() => ({
68
rscs: [
@@ -14,4 +16,40 @@ test.skip('createServerFn returns RSC', () => {
1416
rscs: readonly [ReadableStream, ReadableStream]
1517
}>
1618
>()
19+
})*/
20+
21+
test('createServerFn returns async array', () => {
22+
const result: Array<{ a: number }> = [{ a: 1 }]
23+
const serverFn = createServerFn({ method: 'GET' }).handler(async () => {
24+
return result
25+
})
26+
27+
expectTypeOf(serverFn()).toEqualTypeOf<Promise<Array<{ a: number }>>>()
28+
})
29+
30+
test('createServerFn returns sync array', () => {
31+
const result: Array<{ a: number }> = [{ a: 1 }]
32+
const serverFn = createServerFn({ method: 'GET' }).handler(() => {
33+
return result
34+
})
35+
36+
expectTypeOf(serverFn()).toEqualTypeOf<Promise<Array<{ a: number }>>>()
37+
})
38+
39+
test('createServerFn returns async union', () => {
40+
const result = '1' as string | number
41+
const serverFn = createServerFn({ method: 'GET' }).handler(async () => {
42+
return result
43+
})
44+
45+
expectTypeOf(serverFn()).toEqualTypeOf<Promise<string | number>>()
46+
})
47+
48+
test('createServerFn returns sync union', () => {
49+
const result = '1' as string | number
50+
const serverFn = createServerFn({ method: 'GET' }).handler(() => {
51+
return result
52+
})
53+
54+
expectTypeOf(serverFn()).toEqualTypeOf<Promise<string | number>>()
1755
})

packages/start-client-core/src/client-rpc/serverFnFetcher.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import {
77
import { fromCrossJSON, toJSONAsync } from 'seroval'
88
import invariant from 'tiny-invariant'
99
import { getDefaultSerovalPlugins } from '../getDefaultSerovalPlugins'
10-
import { TSS_FORMDATA_CONTEXT, X_TSS_SERIALIZED } from '../constants'
10+
import {
11+
TSS_FORMDATA_CONTEXT,
12+
X_TSS_RAW_RESPONSE,
13+
X_TSS_SERIALIZED,
14+
} from '../constants'
1115
import type { FunctionMiddlewareClientFnOptions } from '../createMiddleware'
1216
import type { Plugin as SerovalPlugin } from 'seroval'
1317

@@ -156,6 +160,9 @@ async function getResponse(fn: () => Promise<Response>) {
156160
}
157161
})()
158162

163+
if (response.headers.get(X_TSS_RAW_RESPONSE) === 'true') {
164+
return response
165+
}
159166
const contentType = response.headers.get('content-type')
160167
invariant(contentType, 'expected content-type header to be set')
161168
const serializedByStart = !!response.headers.get(X_TSS_SERIALIZED)

packages/start-client-core/src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export const TSS_SERVER_FUNCTION_FACTORY = Symbol.for(
55
)
66

77
export const X_TSS_SERIALIZED = 'x-tss-serialized'
8+
export const X_TSS_RAW_RESPONSE = 'x-tss-raw'
89
export {}

packages/start-client-core/src/createServerFn.ts

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import type {
1414
ResolveValidatorInput,
1515
ValidateSerializable,
1616
ValidateSerializableInput,
17-
ValidateSerializableInputResult,
1817
Validator,
1918
} from '@tanstack/router-core'
2019
import type { JsonResponse } from '@tanstack/router-core/ssr/client'
@@ -174,7 +173,7 @@ export async function executeMiddleware(
174173
env: 'client' | 'server',
175174
opts: ServerFnMiddlewareOptions,
176175
): Promise<ServerFnMiddlewareResult> {
177-
const globalMiddlewares = getStartOptions().functionMiddleware || []
176+
const globalMiddlewares = getStartOptions()?.functionMiddleware || []
178177
const flattenedMiddlewares = flattenMiddlewares([
179178
...globalMiddlewares,
180179
...middlewares,
@@ -243,10 +242,10 @@ export type CompiledFetcherFnOptions = {
243242
context?: any
244243
}
245244

246-
export type Fetcher<TRegister, TMiddlewares, TInputValidator, TResponse> =
245+
export type Fetcher<TMiddlewares, TInputValidator, TResponse> =
247246
undefined extends IntersectAllValidatorInputs<TMiddlewares, TInputValidator>
248-
? OptionalFetcher<TRegister, TMiddlewares, TInputValidator, TResponse>
249-
: RequiredFetcher<TRegister, TMiddlewares, TInputValidator, TResponse>
247+
? OptionalFetcher<TMiddlewares, TInputValidator, TResponse>
248+
: RequiredFetcher<TMiddlewares, TInputValidator, TResponse>
250249

251250
export interface FetcherBase {
252251
[TSS_SERVER_FUNCTION]: true
@@ -260,26 +259,18 @@ export interface FetcherBase {
260259
}) => Promise<unknown>
261260
}
262261

263-
export interface OptionalFetcher<
264-
TRegister,
265-
TMiddlewares,
266-
TInputValidator,
267-
TResponse,
268-
> extends FetcherBase {
262+
export interface OptionalFetcher<TMiddlewares, TInputValidator, TResponse>
263+
extends FetcherBase {
269264
(
270265
options?: OptionalFetcherDataOptions<TMiddlewares, TInputValidator>,
271-
): Promise<FetcherData<TRegister, TResponse>>
266+
): Promise<FetcherData<TResponse>>
272267
}
273268

274-
export interface RequiredFetcher<
275-
TRegister,
276-
TMiddlewares,
277-
TInputValidator,
278-
TResponse,
279-
> extends FetcherBase {
269+
export interface RequiredFetcher<TMiddlewares, TInputValidator, TResponse>
270+
extends FetcherBase {
280271
(
281272
opts: RequiredFetcherDataOptions<TMiddlewares, TInputValidator>,
282-
): Promise<FetcherData<TRegister, TResponse>>
273+
): Promise<FetcherData<TResponse>>
283274
}
284275

285276
export type FetcherBaseOptions = {
@@ -297,22 +288,23 @@ export interface RequiredFetcherDataOptions<TMiddlewares, TInputValidator>
297288
data: Expand<IntersectAllValidatorInputs<TMiddlewares, TInputValidator>>
298289
}
299290

300-
export type FetcherData<TRegister, TResponse> = TResponse extends Response
301-
? Response
302-
: TResponse extends JsonResponse<any>
303-
? ValidateSerializableInputResult<TRegister, ReturnType<TResponse['json']>>
304-
: ValidateSerializableInputResult<TRegister, TResponse>
305-
306291
export type RscStream<T> = {
307292
__cacheState: T
308293
}
309294

310295
export type Method = 'GET' | 'POST'
311296

297+
export type FetcherData<TResponse> =
298+
Awaited<TResponse> extends Response
299+
? Awaited<TResponse>
300+
: Awaited<TResponse> extends JsonResponse<any>
301+
? ReturnType<Awaited<TResponse>['json']>
302+
: Awaited<TResponse>
303+
312304
export type ServerFnReturnType<TRegister, TResponse> =
313-
| Response
314-
| Promise<ValidateSerializableInput<TRegister, TResponse>>
315-
| ValidateSerializableInput<TRegister, TResponse>
305+
Awaited<TResponse> extends Response
306+
? TResponse
307+
: ValidateSerializableInput<TRegister, TResponse>
316308

317309
export type ServerFn<
318310
TRegister,
@@ -521,7 +513,7 @@ export interface ServerFnHandler<
521513
TInputValidator,
522514
TNewResponse
523515
>,
524-
) => Fetcher<TRegister, TMiddlewares, TInputValidator, TNewResponse>
516+
) => Fetcher<TMiddlewares, TInputValidator, TNewResponse>
525517
}
526518

527519
export interface ServerFnBuilder<TRegister, TMethod extends Method = 'GET'>

packages/start-client-core/src/getDefaultSerovalPlugins.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { AnySerializationAdapter } from '@tanstack/router-core'
77

88
export function getDefaultSerovalPlugins() {
99
const start = getStartOptions()
10-
const adapters = start.serializationAdapters as
10+
const adapters = start?.serializationAdapters as
1111
| Array<AnySerializationAdapter>
1212
| undefined
1313
return [
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { getStartContext } from '@tanstack/start-storage-context'
22
import { createIsomorphicFn } from './createIsomorphicFn'
3+
import type { AnyStartInstanceOptions } from './createStart'
34

4-
export const getStartOptions = createIsomorphicFn()
5-
.client(() => window.__TSS_START_OPTIONS__!)
6-
.server(() => getStartContext().startOptions)
5+
export const getStartOptions: () => AnyStartInstanceOptions | undefined =
6+
createIsomorphicFn()
7+
.client(() => window.__TSS_START_OPTIONS__)
8+
.server(() => getStartContext().startOptions)

packages/start-client-core/src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export {
8585
TSS_FORMDATA_CONTEXT,
8686
TSS_SERVER_FUNCTION,
8787
X_TSS_SERIALIZED,
88+
X_TSS_RAW_RESPONSE,
8889
} from './constants'
8990

9091
export type * from './serverRoute'

packages/start-client-core/src/tests/createServerFn.test-d.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -317,11 +317,7 @@ test('createServerFn returns undefined', () => {
317317
test('createServerFn cannot return function', () => {
318318
expectTypeOf(createServerFn().handler<{ func: () => 'func' }>)
319319
.parameter(0)
320-
.returns.toEqualTypeOf<
321-
| Response
322-
| { func: 'Function is not serializable' }
323-
| Promise<{ func: 'Function is not serializable' }>
324-
>()
320+
.returns.toEqualTypeOf<{ func: 'Function is not serializable' }>()
325321
})
326322

327323
test('createServerFn cannot validate function', () => {
@@ -598,3 +594,47 @@ test('createServerFn fetcher itself is serializable', () => {
598594
const fn1 = createServerFn().handler(() => ({}))
599595
const fn2 = createServerFn().handler(() => fn1)
600596
})
597+
598+
test('createServerFn returns async Response', () => {
599+
const serverFn = createServerFn().handler(async () => {
600+
return new Response(new Blob([JSON.stringify({ a: 1 })]), {
601+
status: 200,
602+
headers: {
603+
'Content-Type': 'application/json',
604+
},
605+
})
606+
})
607+
608+
expectTypeOf(serverFn()).toEqualTypeOf<Promise<Response>>()
609+
})
610+
611+
test('createServerFn returns sync Response', () => {
612+
const serverFn = createServerFn().handler(() => {
613+
return new Response(new Blob([JSON.stringify({ a: 1 })]), {
614+
status: 200,
615+
headers: {
616+
'Content-Type': 'application/json',
617+
},
618+
})
619+
})
620+
621+
expectTypeOf(serverFn()).toEqualTypeOf<Promise<Response>>()
622+
})
623+
624+
test('createServerFn returns async array', () => {
625+
const result: Array<{ a: number }> = [{ a: 1 }]
626+
const serverFn = createServerFn({ method: 'GET' }).handler(async () => {
627+
return result
628+
})
629+
630+
expectTypeOf(serverFn()).toEqualTypeOf<Promise<Array<{ a: number }>>>()
631+
})
632+
633+
test('createServerFn returns sync array', () => {
634+
const result: Array<{ a: number }> = [{ a: 1 }]
635+
const serverFn = createServerFn({ method: 'GET' }).handler(() => {
636+
return result
637+
})
638+
639+
expectTypeOf(serverFn()).toEqualTypeOf<Promise<Array<{ a: number }>>>()
640+
})

packages/start-server-core/src/server-functions-handler.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { isNotFound } from '@tanstack/router-core'
22
import invariant from 'tiny-invariant'
33
import {
44
TSS_FORMDATA_CONTEXT,
5+
X_TSS_RAW_RESPONSE,
56
X_TSS_SERIALIZED,
67
getDefaultSerovalPlugins,
78
} from '@tanstack/start-client-core'
@@ -145,6 +146,7 @@ export const handleServerAction = async ({
145146
// Any time we get a Response back, we should just
146147
// return it immediately.
147148
if (result.result instanceof Response) {
149+
result.result.headers.set(X_TSS_RAW_RESPONSE, 'true')
148150
return result.result
149151
}
150152

0 commit comments

Comments
 (0)