Skip to content

Commit 08a80e2

Browse files
NamesMTxzedxautofix-ci[bot]
authored
fix(parseResponse): should not include error responses in result (#4348)
* fix(client): improve parseResponse type inference for conditional responses Previously, parseResponse would infer union types for routes that return different response types based on conditions (e.g., success vs error responses). This was incorrect because parseResponse throws errors for non-OK responses and should only return the success response type. Changes: - Use Extract<> utility type to filter success status code responses from union types - Add fallback logic for responses without explicit success status codes - Update test cases to match the corrected behavior - Add comprehensive test case for conditional response type inference The fix ensures that parseResponse(client['conditional-route'].$get()) correctly infers only the success response type instead of a union of all possible response types. * ci: apply automated fixes * test(client): fix parsedResponse test issue and remove unused test case * fix: minor type error in current runtime version * chore: add more test cases * feat: add `FilterClientResponses` util * fix(parseResponse): should not include error responses in result * chore: refactor to type-only tests and ordering * chore: rename type util and add JSDOC describe --------- Co-authored-by: Zed tse <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 855e5b1 commit 08a80e2

File tree

3 files changed

+89
-5
lines changed

3 files changed

+89
-5
lines changed

src/client/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,18 @@ export type InferRequestOptionsType<T> = T extends (
136136
? NonNullable<R>
137137
: never
138138

139+
/**
140+
* Filter a ClientResponse type so it only includes responses of specific status codes.
141+
*/
142+
export type FilterClientResponseByStatusCode<
143+
T extends ClientResponse<any, any, any>,
144+
U extends number = StatusCode
145+
> = T extends ClientResponse<infer RT, infer RC, infer RF>
146+
? RC extends U
147+
? ClientResponse<RT, RC, RF>
148+
: never
149+
: never
150+
139151
type PathToChain<
140152
Path extends string,
141153
E extends Schema,

src/client/utils.test.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,20 @@ describe('parseResponse', async () => {
150150
const app = new Hono()
151151
.get('/text', (c) => c.text('hi'))
152152
.get('/json', (c) => c.json({ message: 'hi' }))
153-
.get('/404', (c) => c.notFound())
153+
.get('/might-error-json', (c) => {
154+
if (Math.random() > 0.5) {
155+
return c.json({ error: 'error' }, 500)
156+
}
157+
return c.json({ data: [{ id: 1 }, { id: 2 }] })
158+
})
159+
.get('/might-error-mixed-json-text', (c) => {
160+
if (Math.random() > 0.5) {
161+
return c.text('500 Internal Server Error', 500)
162+
}
163+
return c.json({ message: 'Success' })
164+
})
165+
.get('/200-explicit', (c) => c.text('OK', 200))
166+
.get('/404', (c) => c.text('404 Not Found', 404))
154167
.get('/500', (c) => c.text('500 Internal Server Error', 500))
155168
.get('/raw', (c) => {
156169
c.header('content-type', '')
@@ -174,6 +187,21 @@ describe('parseResponse', async () => {
174187
http.get('http://localhost/json', () => {
175188
return HttpResponse.json({ message: 'hi' })
176189
}),
190+
http.get('http://localhost/might-error-json', () => {
191+
if (Math.random() > 0.5) {
192+
return HttpResponse.json({ error: 'error' }, { status: 500 })
193+
}
194+
return HttpResponse.json({ data: [{ id: 1 }, { id: 2 }] })
195+
}),
196+
http.get('http://localhost/might-error-mixed-json-text', () => {
197+
if (Math.random() > 0.5) {
198+
return HttpResponse.text('500 Internal Server Error', { status: 500 })
199+
}
200+
return HttpResponse.json({ message: 'Success' })
201+
}),
202+
http.get('http://localhost/200-explicit', () => {
203+
return HttpResponse.text('OK', { status: 200 })
204+
}),
177205
http.get('http://localhost/404', () => {
178206
return HttpResponse.text('404 Not Found', { status: 404 })
179207
}),
@@ -195,7 +223,7 @@ describe('parseResponse', async () => {
195223
})
196224
}),
197225
http.get('http://localhost/rawBuffer', () => {
198-
return HttpResponse.arrayBuffer(new TextEncoder().encode('hono'), {
226+
return HttpResponse.arrayBuffer(new TextEncoder().encode('hono').buffer, {
199227
headers: {
200228
'content-type': 'x/custom-type',
201229
},
@@ -218,6 +246,11 @@ describe('parseResponse', async () => {
218246
expect(result).toBe('hi')
219247
type _verify = Expect<Equal<typeof result, 'hi'>>
220248
}),
249+
it('should auto parse text response - explicit 200', async () => {
250+
const result = await parseResponse(client['200-explicit'].$get())
251+
expect(result).toBe('OK')
252+
type _verify = Expect<Equal<typeof result, 'OK'>>
253+
}),
221254
it('should auto parse the json response - async fetch', async () => {
222255
const result = await parseResponse(client.json.$get())
223256
expect(result).toEqual({ message: 'hi' })
@@ -259,5 +292,29 @@ describe('parseResponse', async () => {
259292
parseResponse(client['noRoute'].$get())
260293
).rejects.toThrowErrorMatchingInlineSnapshot('[TypeError: fetch failed]')
261294
}),
295+
it('(type-only) should bypass error responses in the result type inference - simple 404', async () => {
296+
type ResultType = Awaited<
297+
ReturnType<typeof parseResponse<Awaited<ReturnType<(typeof client)['404']['$get']>>>>
298+
>
299+
type _verify = Expect<Equal<Awaited<ResultType>, undefined>>
300+
}),
301+
it('(type-only) should bypass error responses in the result type inference - conditional - json', async () => {
302+
type ResultType = Awaited<
303+
ReturnType<
304+
typeof parseResponse<Awaited<ReturnType<(typeof client)['might-error-json']['$get']>>>
305+
>
306+
>
307+
type _verify = Expect<Equal<ResultType, { data: { id: number }[] }>>
308+
}),
309+
it('(type-only) should bypass error responses in the result type inference - conditional - mixed json/text', async () => {
310+
type ResultType = Awaited<
311+
ReturnType<
312+
typeof parseResponse<
313+
Awaited<ReturnType<(typeof client)['might-error-mixed-json-text']['$get']>>
314+
>
315+
>
316+
>
317+
type _verify = Expect<Equal<ResultType, { message: string }>>
318+
}),
262319
])
263320
})

src/client/utils.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import type {
2+
ClientErrorStatusCode,
3+
ContentfulStatusCode,
4+
ServerErrorStatusCode,
5+
} from '../utils/http-status'
16
import { fetchRP, DetailedError } from './fetch-result-please'
2-
import type { ClientResponse, ObjectType } from './types'
7+
import type { ClientResponse, FilterClientResponseByStatusCode, ObjectType } from './types'
38

49
export { DetailedError }
510

@@ -87,13 +92,23 @@ export function deepMerge<T>(target: T, source: Record<string, unknown>): T {
8792
export async function parseResponse<T extends ClientResponse<any>>(
8893
fetchRes: T | Promise<T>
8994
): Promise<
90-
T extends ClientResponse<infer RT, infer _, infer RF>
95+
FilterClientResponseByStatusCode<
96+
T,
97+
Exclude<ContentfulStatusCode, ClientErrorStatusCode | ServerErrorStatusCode> // Filter out the error responses
98+
> extends never
99+
? // Filtered responses does not include any contentful responses, exit with undefined
100+
undefined
101+
: // Filtered responses includes contentful responses, proceed to infer the type
102+
FilterClientResponseByStatusCode<
103+
T,
104+
Exclude<ContentfulStatusCode, ClientErrorStatusCode | ServerErrorStatusCode>
105+
> extends ClientResponse<infer RT, infer _, infer RF>
91106
? RF extends 'json'
92107
? RT
93108
: RT extends string
94109
? RT
95110
: string
96-
: never
111+
: undefined
97112
> {
98113
return fetchRP(fetchRes)
99114
}

0 commit comments

Comments
 (0)