Skip to content

Commit 4be764f

Browse files
authored
fix(nextjs): return 401 for unauthenticated server action requests (#7820)
1 parent 9328cf8 commit 4be764f

File tree

4 files changed

+65
-0
lines changed

4 files changed

+65
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/nextjs': major
3+
---
4+
5+
Return 401 instead of 404 for unauthenticated server action requests in `auth.protect()`

packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,31 @@ describe('clerkMiddleware(params)', () => {
518518
expect((await clerkClient()).authenticateRequest).toBeCalled();
519519
});
520520

521+
it('returns 401 when protect is called, the user is signed out, and the request is a server action', async () => {
522+
const req = mockRequest({
523+
url: '/protected',
524+
headers: new Headers({
525+
'next-url': '/protected',
526+
'next-action': '1',
527+
}),
528+
appendDevBrowserCookie: true,
529+
});
530+
531+
authenticateRequestMock.mockResolvedValueOnce({
532+
publishableKey,
533+
status: AuthStatus.SignedOut,
534+
headers: new Headers(),
535+
toAuth: () => ({ tokenType: TokenType.SessionToken, userId: null }),
536+
});
537+
538+
const resp = await clerkMiddleware(async auth => {
539+
await auth.protect();
540+
})(req, {} as NextFetchEvent);
541+
542+
expect(resp?.status).toEqual(401);
543+
expect((await clerkClient()).authenticateRequest).toBeCalled();
544+
});
545+
521546
it('throws an unauthorized error when protect is called and the machine auth token is invalid', async () => {
522547
const req = mockRequest({
523548
url: '/protected',

packages/nextjs/src/server/protect.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ export function createProtect(opts: {
126126
// TODO: Handle runtime values. What happens if runtime values are set in middleware and in ClerkProvider as well?
127127
return redirectToSignIn();
128128
}
129+
if (isServerActionRequest(request)) {
130+
return unauthorized();
131+
}
129132
return notFound();
130133
};
131134

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
title: '`auth.protect()` returns 401 instead of 404 for unauthenticated server actions'
3+
packages: ['nextjs']
4+
matcher: 'auth\.protect\('
5+
matcherFlags: 'm'
6+
category: 'breaking'
7+
---
8+
9+
`auth.protect()` in `clerkMiddleware()` now returns a `401 Unauthorized` response instead of a `404 Not Found` when an unauthenticated request is made from a server action.
10+
11+
Previously, unauthenticated server action requests would receive a `404` response, which made it difficult for client-side code to distinguish between "not found" and "not authenticated." The new behavior returns `401`, which is the semantically correct HTTP status code for unauthenticated requests.
12+
13+
### Who is affected
14+
15+
If your application uses `auth.protect()` inside `clerkMiddleware()` and you have server actions that may be called by unauthenticated users, the HTTP status code returned will change from `404` to `401`.
16+
17+
### What to do
18+
19+
If you have client-side error handling that checks for `404` responses from server actions when the user is signed out, update it to handle `401` instead:
20+
21+
```diff
22+
try {
23+
await myServerAction();
24+
} catch (error) {
25+
- if (error.status === 404) {
26+
+ if (error.status === 401) {
27+
// Handle unauthenticated user
28+
}
29+
}
30+
```
31+
32+
No changes are required if you are not explicitly checking the HTTP status code in your error handling.

0 commit comments

Comments
 (0)