Error message
Invalid Compact JWS
jose (JavaScript) — JWSInvalid, code ERR_JWS_INVALID
What it means
jose could not split your input into the three dot-separated segments of compact JWS serialization (header.payload.signature). The string you passed is structurally not a signed token; nothing cryptographic was attempted. It is jose's equivalent of jsonwebtoken's "jwt malformed" — and like that error, it means the problem is upstream of the JWT library, in whatever produced or transported the string.
Why it happens
Prefix, quotes or whitespace around the token
common"Bearer " prefixes, JSON quotes from a copied config value, or a trailing newline all break the three-segment structure. The token itself may be fine — the string around it is not.
The variable does not hold a token
commonundefined coerced to "undefined", an empty string from a missing cookie, or an opaque (non-JWT) OAuth access token — none of these have JWS structure. The failure is upstream of jose.
A JWE handed to a JWS API
occasionalEncrypted tokens (JWE) have five segments, not three. If your issuer encrypts tokens, jwtVerify/compactVerify will reject them here — decryption (jwtDecrypt) is the right entry point, followed by verifying the nested JWS if present.
How to fix it
- 1.
Sanitize and segment-check the input
Trim, strip the scheme, and assert the segment count before calling jose — you get precise app-level errors instead of a generic parse failure.
js const token = raw.replace(/^Bearer\s+/i, "").trim(); const segments = token.split(".").length; if (segments !== 3) throw new Error(`expected JWS (3 segments), got ${segments}`); - 2.
Route JWE tokens to decryption
If you count five segments, the token is encrypted. Use jwtDecrypt with the recipient's private key instead of jwtVerify.
js import { jwtDecrypt } from "jose"; const { payload } = await jwtDecrypt(token, privateKey); // 5-segment JWE - 3.
Trace where the string was built
If a sanitized value still fails, log its length and first characters at every hop — client, gateway, storage, handler. A signed JWT's first segment is base64url JSON, so real tokens start with "eyJ" (the encoding of '{"'); the hop where that stops being true is where the corruption happens.
js console.log(token.length, JSON.stringify(token.slice(0, 12))); // healthy: 300+ 'eyJhbGciOiJF'