Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/fastify-actionable-version-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/fastify': patch
---

Registering `clerkPlugin` on Fastify <5 now throws an actionable error that tells you exactly how to resolve the mismatch β€” either upgrade to `fastify@^5` or pin `@clerk/fastify@^1` to stay on Fastify 4 LTS β€” instead of the generic `FST_ERR_PLUGIN_VERSION_MISMATCH` you would previously see during registration.
2 changes: 2 additions & 0 deletions packages/fastify/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
- Node.js `>=20.9.0` or later
- An existing Clerk application. [Create your account for free](https://dashboard.clerk.com/sign-up?utm_source=github&utm_medium=clerk_fastify).

> **Still on Fastify 4 (LTS)?** Pin `@clerk/fastify@^1`. The current major requires Fastify 5; registering it on Fastify 4 throws at plugin registration time with an actionable error explaining your options.

### Installation

The fastest way to get started with Clerk is by following the [Fastify Quickstart](https://clerk.com/docs/quickstarts/fastify?utm_source=github&utm_medium=clerk_fastify).
Expand Down
27 changes: 27 additions & 0 deletions packages/fastify/src/__tests__/clerkPlugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,31 @@ describe('clerkPlugin()', () => {
expect(fastify.decorateRequest).toHaveBeenCalledWith('clerk', null);
expect(doneFn).toHaveBeenCalled();
});

test.each(['4.28.0', '3.29.5', '2.0.0'])(
'throws an actionable error when registered on fastify@%s',
version => {
const doneFn = vi.fn();
const fastify = createFastifyInstanceMock({ version });

expect(() => {
clerkPlugin(fastify, {}, doneFn);
}).toThrowError(new RegExp(`requires fastify@>=5 but is being registered on fastify@${version.replace(/\./g, '\\.')}`));
expect(() => {
clerkPlugin(fastify, {}, doneFn);
}).toThrowError(/pin @clerk\/fastify@\^1/);
expect(doneFn).not.toHaveBeenCalled();
},
);

test.each(['5.0.0', '5.8.5', '6.0.0-alpha.1'])(
'does not throw on supported fastify@%s',
version => {
const doneFn = vi.fn();
const fastify = createFastifyInstanceMock({ version });

expect(() => clerkPlugin(fastify, {}, doneFn)).not.toThrow();
expect(doneFn).toHaveBeenCalled();
},
);
});
18 changes: 17 additions & 1 deletion packages/fastify/src/clerkPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
import type { FastifyInstance, FastifyPluginCallback } from 'fastify';
import fp from 'fastify-plugin';

import { incompatibleFastifyVersion } from './errors';
import type { ClerkFastifyOptions } from './types';
import { ALLOWED_HOOKS } from './types';
import { withClerkMiddleware } from './withClerkMiddleware';

const FASTIFY_MAJOR_MIN = 5;

// Extracts the major version from a Fastify version string like "5.8.5"
// without introducing a `semver` dependency for a single integer compare.
const parseFastifyMajor = (version: string): number => {
const match = /^(\d+)/.exec(version);
return match ? Number.parseInt(match[1]!, 10) : 0;
};

const plugin: FastifyPluginCallback<ClerkFastifyOptions> = (
instance: FastifyInstance,
opts: ClerkFastifyOptions,
done,
) => {
// Throw a Clerk-branded, actionable error instead of fastify-plugin's
// stock FST_ERR_PLUGIN_VERSION_MISMATCH, which doesn't tell the user
// what to do (upgrade fastify or pin @clerk/fastify@^1).
if (parseFastifyMajor(instance.version) < FASTIFY_MAJOR_MIN) {
throw new Error(incompatibleFastifyVersion(instance.version));
}

instance.decorateRequest('auth', null);
instance.decorateRequest('clerk', null as any);

Expand All @@ -26,5 +43,4 @@ const plugin: FastifyPluginCallback<ClerkFastifyOptions> = (

export const clerkPlugin = fp(plugin, {
name: '@clerk/fastify',
fastify: '5.x',
});
11 changes: 11 additions & 0 deletions packages/fastify/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,14 @@ import { clerkPlugin } from '@clerk/fastify';
const server: FastifyInstance = Fastify({ logger: true });
server.register(clerkPlugin);
`);

export const incompatibleFastifyVersion = (foundVersion: string) =>
createErrorMessage(`@clerk/fastify requires fastify@>=5 but is being registered on fastify@${foundVersion}.

To resolve this, either:
- upgrade your fastify dependency to ^5, or
- pin @clerk/fastify@^1 to keep using fastify@4 (LTS):

npm install @clerk/fastify@^1
# or: pnpm add @clerk/fastify@^1
`);
Comment on lines +20 to +29

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion | 🟠 Major | ⚑ Quick win

Add explicit return type annotation for the exported function.

The exported function incompatibleFastifyVersion lacks an explicit return type. As per coding guidelines, all exported functions (especially public APIs) should define explicit return types for better type safety and API clarity.

πŸ“ Proposed fix
-export const incompatibleFastifyVersion = (foundVersion: string) =>
+export const incompatibleFastifyVersion = (foundVersion: string): string =>
   createErrorMessage(`@clerk/fastify requires fastify@>=5 but is being registered on fastify@${foundVersion}.
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const incompatibleFastifyVersion = (foundVersion: string) =>
createErrorMessage(`@clerk/fastify requires fastify@>=5 but is being registered on fastify@${foundVersion}.
To resolve this, either:
- upgrade your fastify dependency to ^5, or
- pin @clerk/fastify@^1 to keep using fastify@4 (LTS):
npm install @clerk/fastify@^1
# or: pnpm add @clerk/fastify@^1
`);
export const incompatibleFastifyVersion = (foundVersion: string): string =>
createErrorMessage(`@clerk/fastify requires fastify@>=5 but is being registered on fastify@${foundVersion}.
To resolve this, either:
- upgrade your fastify dependency to ^5, or
- pin `@clerk/fastify`@^1 to keep using fastify@4 (LTS):
npm install `@clerk/fastify`@^1
# or: pnpm add `@clerk/fastify`@^1
`);
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/fastify/src/errors.ts` around lines 20 - 29, The exported function
incompatibleFastifyVersion currently has no explicit return type; update its
signature to include the correct return type (the type returned by
createErrorMessage, e.g., string or ErrorMessageType used across the module) so
the public API has an explicit annotation; locate the incompatibleFastifyVersion
declaration and add the appropriate return type annotation matching
createErrorMessage's return (or the module's convention) and ensure export
remains unchanged.

Source: Coding guidelines

3 changes: 2 additions & 1 deletion packages/fastify/src/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import type { FastifyInstance } from 'fastify';
import { vi } from 'vitest';

// expose decorated reply methods as methods in fastify instance
export function createFastifyInstanceMock() {
export function createFastifyInstanceMock(overrides: { version?: string } = {}) {
const fastify = {
version: overrides.version ?? '5.8.5',
decorateReply: vi.fn((name: string, fn) => {
// @ts-expect-error - TS7053
fastify[name] = fn;
Expand Down