Skip to content

Commit 5d55b0e

Browse files
committed
Add custom fetch function support and improve documentation
- Add ability to pass custom fetch function via object parameters - Client-side: findFetchInArgs() extracts custom fetch from arguments - Server-side: injectFetchInArgs() provides global fetch by default - User-provided fetch functions always take precedence - Add comprehensive AbortController/AbortSignal documentation - Add custom fetch usage examples and guide - Bump version to 2.1.0
1 parent 92e68f0 commit 5d55b0e

File tree

6 files changed

+168
-5
lines changed

6 files changed

+168
-5
lines changed

README.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,131 @@ export async function failingFunction({}) {
154154
}
155155
```
156156

157+
## Aborting requests with AbortSignal
158+
159+
You can abort ongoing server actions using `AbortController` and `AbortSignal`. Pass an abort signal in your arguments (either directly or as a field in an object parameter), and it will be used to:
160+
161+
- **Client-side**: Abort the fetch request
162+
- **Server-side**: Allow server functions to respond to request cancellations
163+
164+
```ts
165+
// pages/api/server-actions.js
166+
"poor man's use server";
167+
168+
export async function longRunningTask(signal: AbortSignal) {
169+
for (let i = 0; i < 10; i++) {
170+
// Check if the request was aborted
171+
if (signal.aborted) {
172+
throw new Error('Task aborted: ' + signal.reason);
173+
}
174+
await sleep(1000);
175+
}
176+
return { completed: true };
177+
}
178+
179+
// Also works with async generators
180+
export async function* streamData({ signal }: { signal: AbortSignal }) {
181+
for (let i = 0; i < 20; i++) {
182+
if (signal.aborted) {
183+
throw new Error('Stream aborted: ' + signal.reason);
184+
}
185+
await sleep(500);
186+
yield { count: i };
187+
}
188+
}
189+
```
190+
191+
Client usage:
192+
193+
```tsx
194+
// pages/index.tsx
195+
import { longRunningTask, streamData } from './api/server-actions';
196+
197+
export default function Page() {
198+
const handleAbortableTask = async () => {
199+
const controller = new AbortController();
200+
201+
// Abort after 2 seconds
202+
setTimeout(() => controller.abort('Timeout'), 2000);
203+
204+
try {
205+
await longRunningTask(controller.signal);
206+
} catch (error) {
207+
console.log(error); // "Task aborted: Timeout"
208+
}
209+
};
210+
211+
const handleAbortableStream = async () => {
212+
const controller = new AbortController();
213+
const generator = streamData({ signal: controller.signal });
214+
215+
for await (const { count } of generator) {
216+
console.log(count);
217+
if (count > 5) {
218+
controller.abort('User stopped stream');
219+
break;
220+
}
221+
}
222+
};
223+
224+
return <div>...</div>;
225+
}
226+
```
227+
228+
**Note**: You can pass `AbortSignal` or `AbortController` directly as arguments, or as fields within object parameters. The server function will receive the request's abort signal, allowing it to detect when the client cancels the request (e.g., when navigating away).
229+
230+
## Custom fetch function
231+
232+
You can provide a custom `fetch` function to your server actions by including it as a `fetch` field in an object parameter. This is useful for adding custom headers, authentication, or using a custom fetch implementation.
233+
234+
```ts
235+
// pages/api/server-actions.js
236+
"poor man's use server";
237+
238+
export async function fetchExternalData({ url, fetch }) {
239+
// The fetch parameter will be either:
240+
// - The custom fetch function passed from the client
241+
// - The global fetch function (injected automatically on the server)
242+
const response = await fetch(url);
243+
return response.json();
244+
}
245+
```
246+
247+
Client usage with custom fetch:
248+
249+
```tsx
250+
// pages/index.tsx
251+
import { fetchExternalData } from './api/server-actions';
252+
253+
export default function Page() {
254+
const handleFetch = async () => {
255+
// Define a custom fetch with authentication
256+
const customFetch = (url, options) => {
257+
return fetch(url, {
258+
...options,
259+
headers: {
260+
...options?.headers,
261+
'Authorization': 'Bearer my-token',
262+
},
263+
});
264+
};
265+
266+
const data = await fetchExternalData({
267+
url: 'https://api.example.com/data',
268+
fetch: customFetch
269+
});
270+
console.log(data);
271+
};
272+
273+
return <div>...</div>;
274+
}
275+
```
276+
277+
**How it works**:
278+
- **Client-side**: If you provide a `fetch` field in an object parameter, it will be used for the RPC call instead of the global `fetch`
279+
- **Server-side**: The global `fetch` function is automatically injected into object parameters (unless you already provided your own)
280+
- User-provided fetch functions always take precedence over the injected one
281+
157282
## How it works
158283

159284
The plugin will replace the content of files inside `pages/api` with `"poor man's use server"` at the top to make the exported functions callable from the browser.

server-actions-for-next-pages/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# server-actions-for-next-pages
22

3+
## 2.1.0
4+
5+
### Minor Changes
6+
7+
- Add custom fetch function support - you can now pass a `fetch` field in object parameters to customize fetch behavior (e.g., add authentication headers)
8+
- Client-side: Custom fetch functions can be provided and will be used for RPC calls instead of global fetch
9+
- Server-side: Global fetch is automatically injected into object parameters unless already provided by the user
10+
- Add comprehensive documentation for AbortController/AbortSignal support in README
11+
- Add documentation for custom fetch functionality with usage examples
12+
313
## 2.0.0
414

515
### Major Changes

server-actions-for-next-pages/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "server-actions-for-next-pages",
3-
"version": "2.0.0",
3+
"version": "2.1.0",
44
"description": "Use Next.js server actions in your pages directory",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

server-actions-for-next-pages/src/browser.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { JsonRpcRequest } from './jsonRpc';
22
import { EventSourceParserStream } from 'eventsource-parser/stream';
3-
import { registerAbortControllerSerializers, findAbortSignalInArgs } from './superjson-setup';
3+
import { registerAbortControllerSerializers, findAbortSignalInArgs, findFetchInArgs } from './superjson-setup';
44

55
type NextRpcCall = (...params: any[]) => any;
66

@@ -46,8 +46,9 @@ export function createRpcFetcher(
4646
const superjson = await import('superjson');
4747
registerAbortControllerSerializers(superjson.default);
4848
const abortSignal = findAbortSignalInArgs(args);
49+
const customFetch = findFetchInArgs(args) || fetch;
4950
const { json, meta } = superjson.serialize(args);
50-
const res = await fetch(url, {
51+
const res = await customFetch(url, {
5152
method: 'POST',
5253
body: JSON.stringify(
5354
{
@@ -90,8 +91,9 @@ export function createRpcFetcher(
9091
const superjson = await import('superjson');
9192
registerAbortControllerSerializers(superjson.default);
9293
const abortSignal = findAbortSignalInArgs(args);
94+
const customFetch = findFetchInArgs(args) || fetch;
9395
const { json, meta } = superjson.serialize(args);
94-
const res = await fetch(url, {
96+
const res = await customFetch(url, {
9597
method: 'POST',
9698
body: JSON.stringify(
9799
{

server-actions-for-next-pages/src/server.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { NextApiHandler } from 'next';
22
import { JsonRpcResponse } from './jsonRpc';
33
import { NextRequest, NextResponse } from 'next/server';
44
import { getEdgeContext, getRequestAbortSignal } from './context-internal';
5-
import { registerAbortControllerSerializers, replaceAbortSignalsInArgs } from './superjson-setup';
5+
import { registerAbortControllerSerializers, replaceAbortSignalsInArgs, injectFetchInArgs } from './superjson-setup';
66
// @ts-ignore
77
import type SuperJSON from 'superjson';
88

@@ -105,6 +105,9 @@ export function createRpcHandler(
105105
meta: argsMeta,
106106
}) as any[];
107107

108+
// Inject global fetch into args (won't override if user provided their own)
109+
args = injectFetchInArgs(args, fetch);
110+
108111
const requestAbortSignal = getRequestAbortSignal();
109112
if (requestAbortSignal) {
110113
args = replaceAbortSignalsInArgs(args, requestAbortSignal);

server-actions-for-next-pages/src/superjson-setup.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,26 @@ export function replaceAbortSignalsInArgs(args: any[], newSignal: AbortSignal):
7878
return arg;
7979
});
8080
}
81+
82+
export function findFetchInArgs(args: any[]): typeof fetch | undefined {
83+
for (const arg of args) {
84+
if (arg && typeof arg === 'object' && !Array.isArray(arg)) {
85+
if (typeof arg.fetch === 'function') {
86+
return arg.fetch;
87+
}
88+
}
89+
}
90+
return undefined;
91+
}
92+
93+
export function injectFetchInArgs(args: any[], fetchFn: typeof fetch): any[] {
94+
return args.map((arg) => {
95+
if (arg && typeof arg === 'object' && !Array.isArray(arg)) {
96+
// Only inject if fetch is not already present
97+
if (!('fetch' in arg)) {
98+
return { ...arg, fetch: fetchFn };
99+
}
100+
}
101+
return arg;
102+
});
103+
}

0 commit comments

Comments
 (0)