Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Add stricter check for "use server" exports #62821

Merged
merged 2 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions errors/invalid-use-server-value.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
title: 'Invalid "use server" Value'
---

## Why This Error Occurred

This error occurs when a `"use server"` file exports a value that is not an async function. It might happen when you unintentionally export something like a configuration object, an arbitrary value, or missed the `async` keyword in the exported function declaration.

These functions are required to be defined as async, because `"use server"` marks them as [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations) and they can be invoked directly from the client through a network request.

Examples of incorrect code:

```js
'use server'

// ❌ This is incorrect: only async functions are allowed.
export const value = 1

// ❌ This is incorrect: missed the `async` keyword.
export function getServerData() {
return '...'
}
```

Correct code:

```js
'use server'

// ✅ This is correct: an async function is exported.
export async function getServerData() {
return '...'
}
```

## Possible Ways to Fix It

Check all exported values in the `"use server"` file (including `export *`) and make sure that they are all defined as async functions.

## Useful Links

- [Server Actions and Mutations - Next.js](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations)
- ['use server' directive - React](https://react.dev/reference/react/use-server)
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,22 @@ export function ensureServerEntryExports(actions: any[]) {
const action = actions[i]
if (typeof action !== 'function') {
throw new Error(
`A "use server" file can only export async functions, found ${typeof action}.`
`A "use server" file can only export async functions, found ${typeof action}.\nRead more: https://nextjs.org/docs/messages/invalid-use-server-value`
)
}

if (action.constructor.name !== 'AsyncFunction') {
const actionName: string = action.name || ''

// Note: if it's from library code with `async` being transpiled to returning a promise,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bit is actually quite annoying for library authors tho, any plan where this will get improved in the future?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. It also breaks the use of Zod for validating the input of Server Actions (this worked before):

"use server";

import { db } from "./db";
import { z } from "zod";

export const getUserName = z
  .function()
  .args(z.object({ userId: z.string() }))
  .implement(async ({ userId }) => {
    const user = await db.users.query({ where: { id: userId } });
    return user.name;
  });

// it would be annoying. But users can still wrap it in an async function to work around it.
throw new Error(
`A "use server" file can only export async functions.${
// If the function has a name, we'll make the error message more specific.
actionName
? ` Found "${actionName}" that is not an async function.`
: ''
}\nRead more: https://nextjs.org/docs/messages/invalid-use-server-value`
)
}
}
Expand Down
69 changes: 68 additions & 1 deletion test/e2e/app-dir/actions/app-action.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable jest/no-standalone-expect */
import { createNextDescribe } from 'e2e-utils'
import { check, waitFor } from 'next-test-utils'
import { check, waitFor, getRedboxSource, hasRedbox } from 'next-test-utils'
import type { Request, Response, Route } from 'playwright'
import fs from 'fs-extra'
import { join } from 'path'
Expand Down Expand Up @@ -508,6 +508,73 @@ createNextDescribe(
}

if (isNextDev) {
describe('"use server" export values', () => {
it('should error when exporting non async functions at build time', async () => {
const filePath = 'app/server/actions.js'
const origContent = await next.readFile(filePath)

try {
const browser = await next.browser('/server')

const cnt = await browser.elementByCss('h1').text()
expect(cnt).toBe('0')

// This can be caught by SWC directly
await next.patchFile(
filePath,
origContent + '\n\nexport const foo = 1'
)

expect(await hasRedbox(browser)).toBe(true)
expect(await getRedboxSource(browser)).toContain(
'Only async functions are allowed to be exported in a "use server" file.'
)
} finally {
await next.patchFile(filePath, origContent)
}
})

it('should error when exporting non async functions during runtime', async () => {
const logs: string[] = []
next.on('stdout', (log) => {
logs.push(log)
})
next.on('stderr', (log) => {
logs.push(log)
})

const filePath = 'app/server/actions.js'
const origContent = await next.readFile(filePath)

try {
const browser = await next.browser('/server')

const cnt = await browser.elementByCss('h1').text()
expect(cnt).toBe('0')

// This requires the runtime to catch
await next.patchFile(
filePath,
origContent + '\n\nconst f = () => {}\nexport { f }'
)

await check(
() =>
logs.some((log) =>
log.includes(
'Error: A "use server" file can only export async functions. Found "f" that is not an async function.'
)
)
? 'true'
: '',
'true'
)
} finally {
await next.patchFile(filePath, origContent)
}
})
})

describe('HMR', () => {
it('should support updating the action', async () => {
const filePath = 'app/server/actions-3.js'
Expand Down
Loading