Skip to content

Commit 88d3331

Browse files
committed
Allow fetchHandler to be overriden on a per-request basis
1 parent 50b7b11 commit 88d3331

File tree

6 files changed

+109
-3
lines changed

6 files changed

+109
-3
lines changed

.changeset/swift-kings-kneel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@fetchkit/ffetch': patch
3+
---
4+
5+
Allow fetchHandler to be overriden on a per-request basis

docs/api.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,14 @@ const client = createClient({
209209
// Or use node-fetch/undici in Node.js
210210
import nodeFetch from 'node-fetch'
211211
const clientNode = createClient({ fetchHandler: nodeFetch })
212+
213+
// Per-request fetchHandler override (useful for testing)
214+
const client = createClient({ retries: 0 })
215+
const mockFetch = () => Promise.resolve(
216+
new Response(JSON.stringify({ test: 'data' }), { status: 200 })
217+
)
218+
await client('https://example.com', { fetchHandler: mockFetch }) // Uses mockFetch
219+
await client('https://example.com') // Uses global fetch
212220
```
213221

214222
## Circuit Breaker Hooks

docs/examples.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ const data = await response.json()
127127

128128
### Injecting a Mock Fetch for Unit Tests
129129

130+
You can provide a mock fetch handler at the client level or override it per-request:
131+
130132
```typescript
131133
import createClient from '@fetchkit/ffetch'
132134

@@ -136,10 +138,28 @@ function mockFetch(url, options) {
136138
)
137139
}
138140

141+
// Option 1: Client-level fetchHandler (all requests use this)
139142
const client = createClient({ fetchHandler: mockFetch })
140143
const response = await client('https://api.example.com/test')
141144
const data = await response.json()
142145
// data: { ok: true, url: 'https://api.example.com/test' }
146+
147+
// Option 2: Per-request fetchHandler (useful for different mocks per test)
148+
const client2 = createClient({ retries: 0 })
149+
150+
// First request with specific mock
151+
const mockUser = () => Promise.resolve(
152+
new Response(JSON.stringify({ id: 1, name: 'Alice' }), { status: 200 })
153+
)
154+
const userResponse = await client2('/api/user', { fetchHandler: mockUser })
155+
// Returns: { id: 1, name: 'Alice' }
156+
157+
// Second request with different mock
158+
const mockPosts = () => Promise.resolve(
159+
new Response(JSON.stringify([{ id: 1, title: 'Hello' }]), { status: 200 })
160+
)
161+
const postsResponse = await client2('/api/posts', { fetchHandler: mockPosts })
162+
// Returns: [{ id: 1, title: 'Hello' }]
143163
```
144164

145165
## Advanced Patterns

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ export function createClient(opts: FFetchOptions = {}): FFetch {
232232
signal: combinedSignal,
233233
})
234234
try {
235-
const handler = fetchHandler ?? fetch
235+
const handler = init.fetchHandler ?? fetchHandler ?? fetch
236236
const response = await handler(reqWithSignal)
237237
lastResponse = response
238238
if (

test/client.fetchHandler.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,77 @@ describe('ffetch fetchHandler option', () => {
7373
expect(global.fetch).toHaveBeenCalled()
7474
global.fetch = originalFetch
7575
})
76+
77+
it('allows per-request fetchHandler override', async () => {
78+
const clientFetchSpy = vi.fn(mockFetch)
79+
const requestFetchSpy = vi.fn(() =>
80+
Promise.resolve(
81+
new Response(JSON.stringify({ from: 'request' }), { status: 200 })
82+
)
83+
)
84+
85+
const client = createClient({ fetchHandler: clientFetchSpy })
86+
const res = await client('https://example.com', {
87+
fetchHandler: requestFetchSpy,
88+
})
89+
const json = await res.json()
90+
91+
expect(clientFetchSpy).not.toHaveBeenCalled()
92+
expect(requestFetchSpy).toHaveBeenCalled()
93+
expect(json.from).toBe('request')
94+
})
95+
96+
it('uses client fetchHandler when no per-request override is provided', async () => {
97+
const clientFetchSpy = vi.fn(() =>
98+
Promise.resolve(
99+
new Response(JSON.stringify({ from: 'client' }), { status: 200 })
100+
)
101+
)
102+
103+
const client = createClient({ fetchHandler: clientFetchSpy })
104+
const res = await client('https://example.com')
105+
const json = await res.json()
106+
107+
expect(clientFetchSpy).toHaveBeenCalled()
108+
expect(json.from).toBe('client')
109+
})
110+
111+
it('supports per-request fetchHandler without client-level handler', async () => {
112+
const requestFetchSpy = vi.fn(() =>
113+
Promise.resolve(
114+
new Response(JSON.stringify({ from: 'request-only' }), { status: 200 })
115+
)
116+
)
117+
118+
const client = createClient({ retries: 0 })
119+
const res = await client('https://example.com', {
120+
fetchHandler: requestFetchSpy,
121+
})
122+
const json = await res.json()
123+
124+
expect(requestFetchSpy).toHaveBeenCalled()
125+
expect(json.from).toBe('request-only')
126+
})
127+
128+
it('allows different fetchHandlers for different requests on same client', async () => {
129+
const fetch1 = vi.fn(() =>
130+
Promise.resolve(new Response(JSON.stringify({ id: 1 }), { status: 200 }))
131+
)
132+
const fetch2 = vi.fn(() =>
133+
Promise.resolve(new Response(JSON.stringify({ id: 2 }), { status: 200 }))
134+
)
135+
136+
const client = createClient({ retries: 0 })
137+
138+
const res1 = await client('https://test1.com', { fetchHandler: fetch1 })
139+
const json1 = await res1.json()
140+
141+
const res2 = await client('https://test2.com', { fetchHandler: fetch2 })
142+
const json2 = await res2.json()
143+
144+
expect(fetch1).toHaveBeenCalledTimes(1)
145+
expect(fetch2).toHaveBeenCalledTimes(1)
146+
expect(json1.id).toBe(1)
147+
expect(json2.id).toBe(2)
148+
})
76149
})

0 commit comments

Comments
 (0)