Cannot use ClerkMiddleware and input together
I encountered following issue when using Clerk Middleware and jstack's input validation.
X [ERROR] [API Error] TypeError: This ReadableStream is disturbed (has already been read from), and cannot be used as a body.
at new ClerkRequest (file:///E:/intonix/application-backup-v3/node_modules/@clerk/backend/src/tokens/clerkRequest.ts:29:5)
The main cause to the problem is clerk reading the stream after it's already been read.
When I debug it I found that this code trying to read it.
node_modules/wrangler/templates/middleware/middleware-ensure-req-body-drained.ts
var drainBody = /* @__PURE__ */ __name(async (request, env4, _ctx, middlewareCtx) => {
try {
return await middlewareCtx.next(request, env4);
} finally {
try {
if (request.body !== null && !request.bodyUsed && request) {
const reader = request.body.getReader();
while (!(await reader.read()).done) {
}
}
} catch (e) {
console.error("Failed to drain the unused request body.", e);
}
}
}, "drainBody");
var middleware_ensure_req_body_drained_default = drainBody;
Just reporting the bug. As I am now directly using the await c.req.json() and manually parsing it. :)
Please lmk these things to help:
- jstack version
- which clerk middleware exactly youve used
- your middleware implementation you expect to work
thanks for raising the issue
Jstack Version : [email protected] Clerk Version : @hono/[email protected]
Here is the middleware implementation.
const authMiddleware = j.middleware(async ({ c, next }) => {
const { NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, CLERK_SECRET_KEY } = env(c) as {
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: string;
CLERK_SECRET_KEY: string;
};
await honoClerkMiddleware({
publishableKey: NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
secretKey: CLERK_SECRET_KEY,
})(c, async () => {
const auth = getAuth(c);
if (!auth?.userId) {
c.json({ message: "You are not logged in." }, 401);
return;
}
await next({ auth });
});
});
When in a procedure i have input with a schema it throws this error. But as I remove the input and directly use the procedure it works. The error stack trace says it's clerk fault.
#58 covers this in more detail as to why you are getting errors.
Its because zod is consuming the body before clerk middleware can, so its erroring as contaminated/consumed/read whatever you want to call it. This is most commonly and likely a security thing. There is a workaround for this, but its a tough setup and requires you to modify how you call anything on the client side that requires the ClerkMiddleware for securing authenticated procedures.
This is how I did the middlewares, given i don't need authMiddleware to run on some paths but need access to clerk data i made a separate procedure, but it works very well.
// other jstack imports and stuff for db
import { createClerkClient, verifyToken } from "@clerk/backend";
import { env } from "hono/adapter";
import { getCookie } from "hono/cookie";
import { HTTPException } from "hono/http-exception";
const clerkMiddleware = j.middleware(async ({ c, next }) => {
const { CLERK_SECRET_KEY, CLERK_PUBLISHABLE_KEY, CLERK_JWT_KEY } = env(c);
const cookies = getCookie(c);
const sessionCookie = cookies["__session"];
const authorization = c.req.header("Authorization")?.replace("Bearer ", "");
const token = sessionCookie || authorization;
const client = createClerkClient({
secretKey: CLERK_SECRET_KEY,
publishableKey: CLERK_PUBLISHABLE_KEY,
});
if (!token) {
throw new HTTPException(401, {
message: "Unauthorized, no session token. Please sign in.",
});
}
try {
const verifiedToken = await verifyToken(token, {
jwtKey: CLERK_JWT_KEY,
secretKey: CLERK_SECRET_KEY,
clockSkewInMs: 120000,
authorizedParties: [
"http://localhost:3000",
"https://jconetjobs.cloud",
"http://localhost:8000",
"https://api.jconetjobs.cloud",
],
});
return await next({ client, verifiedToken });
} catch (error) {
console.log(error);
throw new HTTPException(500, {
message: "Something went wrong please try again.",
});
}
});
export const withClerk = publicProcedure.use(clerkMiddleware);
const authMiddleware = j.middleware(async ({ c, ctx, next }) => {
const { db, client, verifiedToken } = ctx as InferMiddlewareOutput<
typeof dbMiddleware
> &
InferMiddlewareOutput<typeof clerkMiddleware>;
const session = await client.sessions.getSession(verifiedToken.sid);
if (!session || !session.userId) {
throw new HTTPException(401, {
message: "Unauthorized, sign in to continue.",
});
}
const user = await db.user.findUnique({
where: {
extId: session.userId,
},
select: {
id: true,
username: true,
extId: true,
},
});
if (!user) {
throw new HTTPException(404, {
message: "User not recognized, sign up to continue.",
});
}
return await next({ user });
});
export const privateProcedure = withClerk.use(authMiddleware);
This is how its called from the frontend
import { client } from "@/lib/client";
import { useAuth } from "@clerk/nextjs";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
// rest of imports
// rest of consts needed
const queryClient = useQueryClient();
const {
data,
isLoading: queryLoading,
error,
isError,
} = useQuery({
queryKey: ["personal-info-query"],
queryFn: async () => {
const res = await client.auth.getPersonalInformation.$get(
undefined,
{
headers: {
Authorization: `Bearer ${await getToken()}`,
},
}
);
return await res.json();
},
});
const { mutate, isPending: mutatePending } = useMutation({
mutationKey: ["personal-info-upate"],
mutationFn: async ({
dob,
sex,
country,
}: {
dob: string;
sex: string;
country: string;
}) => {
await client.auth.updatePersonalInformation.$post(
{
country,
sex,
dob,
},
{
headers: {
Authorization: `Bearer ${await getToken()}`,
},
}
);
},
onError: (error: HTTPException) => {
toast.error(error.message);
},
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: ["personal-info-query"],
});
},
});
// rest of file...
You can see a working version of this here and view the code here
Thank You for the solution. Now it is working. Cheers 👍