JsonWebTokenError: jwt malformed (Node / jsonwebtoken)
Filed under APIs, auth & CORS
The value passed to jwt.verify is not shaped like a JWT — usually the whole "Bearer ..." header, an undefined variable, or the wrong value entirely.
Fastest route
Give your AI this prompt
Copy it, replace the bracketed part with your error, and send it.
Preview the prompt
My Node auth code throws "JsonWebTokenError: jwt malformed" when verifying tokens. Here is the middleware or function that calls jwt.verify: [paste it]. The token is supposed to reach the server via [the Authorization header / a cookie / I'm not sure]. Please: 1) check whether the full "Bearer ..." header value is being passed to verify() instead of just the token, and fix the extraction, 2) add a presence check so a missing token returns a clean 401 instead of throwing, 3) add safe shape logging — type, length, and dot count only, never the token itself — so we can see what is actually arriving, 4) trace where the token should come from and check the frontend and backend agree on the header or cookie name, and that any environment variable involved is set in the failing environment. Constraints: never log or print a full token or the JWT secret, do not "fix" this by removing verification or accepting unsigned tokens, and make one change at a time with a test for each of the three cases: no token, garbage token, and a real token from login.
Fix it yourself
Follow the repair
- Split the Authorization header — never pass it whole. If your token travels in the standard header, the extraction should look like this:
Search your code forconst authHeader = req.headers.authorization; // "Bearer eyJhbGciOi..." const token = authHeader && authHeader.split(" ")[1];jwt.verify(and look at what is being passed. If it isreq.headers.authorizationdirectly, you have found the bug: verify was chewing on the word "Bearer" as if it were part of the token. - Check the token exists before you verify it. A missing token is not an error in your code; it is a request with no login attached, and it deserves a clean answer, not a stack trace:
This shape — extract, check presence, then verify inside a try/catch — turns every flavour of bad token into a tidy 401 and keeps the malformed error out of your logs for good.if (!token) { return res.status(401).json({ error: "No token provided" }); } try { const decoded = jwt.verify(token, process.env.JWT_SECRET); req.user = decoded; next(); } catch (err) { return res.status(401).json({ error: "Invalid or expired token" }); } - Look at the token's shape safely, without logging the token. When it still fails, inspect the shape only — never print a real token to logs or a chat, because a logged token is a working login for whoever reads it. Length, dot count, and the first few characters are enough:
A genuine JWT has exactly two dots and usually starts withconsole.log("token type:", typeof token, "length:", token?.length, "dots:", token?.split(".").length - 1, "starts:", token?.slice(0, 6));eyJ. Zero dots means it is not a JWT at all (an API key? a session ID? the word Bearer?). One long string starting with "Bearer" means step 1 was skipped somewhere. An undefined type means the value never arrived — go to the next step. - If the token is undefined, trace where it was supposed to come from. Follow the chain backwards: does the frontend actually send the header (check the request in the browser's network tab)? Is the backend reading the exact header or cookie name the frontend sets — names must match character for character? If the token comes from an environment variable, confirm that variable exists in the environment that is failing — set locally but missing on the hosting provider is the usual story, and the app must be restarted or redeployed after adding it.
- Check the secret is loaded, too — for the error that comes next. While you are in the environment settings, confirm
JWT_SECRET(or whichever variable your verify call reads) is actually set in this environment. An undefined secret does not cause "malformed" — it causes confusing failures right after you fix the token shape. One check now saves a second debugging round: verify the variable exists, do not print its value, and make sure the signing code and verifying code read the same variable name. - Test all three arrivals. With the fixes in: a request with no token should get a clean 401 "no token" response; a request with a garbage token should get a clean 401 invalid-token response; and a request with a real token from an actual login should sail through. If the third one now fails with "invalid signature" or "jwt expired", celebrate quietly — you have graduated to a different, more specific error, and your token extraction is working.
Why this happensOptional explanation
Verification has two stages, and this error happens at the door of stage one. The library first looks at what you passed and asks "is this even shaped like a JWT?" — a string with three dot-separated sections. Only if the shape passes does it go on to check the signature and expiry. "jwt malformed" is the shape check failing, which is why fixing it never involves your secret: the secret is never reached. (A wrong secret produces a different error entirely — "invalid signature" — and an expired token produces "jwt expired".)
AI-generated auth code walks into this in a few standard ways. The most common: the code reads the Authorization header and passes the whole thing to verify(), forgetting that the header's value is "Bearer " plus the token, and that extra word ruins the shape. Next: the token comes from an environment variable or a cookie that was never set in this environment, so the variable is undefined — and undefined is definitely not shaped like a JWT. Then there is the quiet mismatch: frontend and backend disagree about where the token travels, so the backend diligently reads a header the frontend never sends, or the frontend sends the token while the backend looks in a cookie. Finally, plain wrong-value bugs: code passes the entire request headers object, a session ID, or a Supabase-style API key into a function that expects a JWT.
- The Bearer prefix: the full "Bearer eyJ..." header value passed to verify instead of just the token after the space.
- Nothing there: a missing header, an unset cookie, or an undefined environment variable — no token exists to verify.
- Wrong value entirely: a headers object, a session ID, or an API key passed where a JWT belongs.
- Truncated or padded: the token got cut off in copying, or picked up quotes or whitespace when stored.
Quick answers
Is passing "Bearer" with the token really enough to cause this?
Yes — verify() expects only the token itself. The header value "Bearer eyJ..." contains a space and an extra word, which fails the shape check instantly. Split on the space and pass the second part.
Could this be my JWT secret being wrong?
Malformed means the shape check failed and the secret was never even consulted. A wrong secret gives "invalid signature" and an expired token gives "jwt expired" — both more specific errors that only appear once a properly shaped token is being verified.
It works locally but throws malformed in production. Why?
Almost certainly undefined — the variable, header, or cookie the code reads was never set in that environment. Trace the chain: what the frontend sends, the exact name the backend reads, and whether the environment variable exists where the code is running.