Skip to content

Bridge Express writes the 401/403 response itself when a request fails authentication or authorization — the middleware does not call next() and your handler never runs. Responses follow RFC 6750: 401s carry a WWW-Authenticate header so your frontend can distinguish error types and react appropriately.

WWW-Authenticate errorMeaningRecommended action
missing_tokenNo Authorization header was sentRedirect to login
expired_tokenToken signature is valid but past expiryAttempt silent refresh, then redirect
invalid_tokenToken is malformed, tampered, or uses an unknown keyRedirect to login
invalid_requestWrong credential type for this endpoint (e.g. API token sent to a jwt-only route)Use the correct credential
src/lib/api.ts
import { auth } from '@nebulr-group/bridge-react'; // or bridge-svelte, etc.
async function apiFetch(endpoint: string, options: RequestInit = {}) {
const token = auth.getAccessToken();
const response = await fetch(`http://localhost:3000${endpoint}`, {
...options,
headers: {
...options.headers,
...(token ? { Authorization: `Bearer ${token}` } : {}),
'Content-Type': 'application/json',
},
});
if (response.status === 401) {
const wwwAuth = response.headers.get('WWW-Authenticate') ?? '';
if (wwwAuth.includes('expired_token')) {
try {
await auth.refresh();
const newToken = auth.getAccessToken();
return fetch(`http://localhost:3000${endpoint}`, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${newToken}`,
'Content-Type': 'application/json',
},
}).then((r) => r.json());
} catch {
auth.login();
return;
}
}
// missing_token or invalid_token — redirect to login
auth.login();
return;
}
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}

Returned when a token is missing, expired, or invalid. Includes the RFC 6750 WWW-Authenticate header:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="expired_token", error_description="The access token has expired"
{
"statusCode": 401,
"error": "Unauthorized",
"message": "The access token has expired"
}

Auth-type rejection (e.g. an API token sent to a bridge.protect({ acceptAuth: 'jwt' }) endpoint):

WWW-Authenticate: Bearer error="invalid_request", error_description="API token authentication is not accepted for this endpoint"
{
"statusCode": 401,
"error": "Unauthorized",
"message": "auth type not accepted"
}

Returned when authenticated but lacking the required role, privilege, or feature flag:

Role check failed:

{
"statusCode": 403,
"error": "Forbidden",
"message": "Role 'ADMIN' required"
}

Privilege check failed:

{
"statusCode": 403,
"error": "Forbidden",
"message": "Privilege 'USER_READ' required"
}

Feature flag not enabled:

{
"statusCode": 403,
"error": "Forbidden",
"message": "Feature flag 'beta-access' is not enabled"
}

Thrown internally by JwksService when token verification fails. Error codes:

CodeMeaning
TOKEN_EXPIREDJWT has expired
TOKEN_INVALIDJWT is malformed, inactive, or not an API token
JWKS_NO_MATCHNo matching key found in the JWKS endpoint
CLAIM_VALIDATION_FAILEDToken claim validation failed (e.g. wrong issuer or audience)
APP_MISMATCHToken was issued for a different app ID

The middleware catches these and maps them to RFC 6750 WWW-Authenticate errors:

Error CodeRFC 6750 ErrorDescription
TOKEN_EXPIREDexpired_tokenThe access token has expired
TOKEN_INVALIDinvalid_tokenThe access token is invalid
JWKS_NO_MATCHinvalid_tokenThe access token signature could not be verified
CLAIM_VALIDATION_FAILEDinvalid_tokenThe access token claim validation failed
APP_MISMATCHinvalid_tokenThe access token was issued for a different application

You don’t catch TokenVerificationError yourself — the middleware handles it and writes the 401. It is importable (import { TokenVerificationError } from '@nebulr-group/bridge-express') for advanced cases where you verify a token manually.

Thrown by bridge.http (the token-forwarding HTTP client) when a downstream call returns a non-2xx response:

import { BridgeHttpError } from '@nebulr-group/bridge-express';
class BridgeHttpError extends Error {
readonly status: number; // HTTP status code
readonly url: string; // Request URL
}
router.get('/inventory', async (req, res) => {
try {
const data = await bridge.http.get('http://inventory-service/stock', req.bridgeAccessToken);
res.json(data);
} catch (err) {
if (err instanceof BridgeHttpError) {
console.log(err.status); // e.g. 404
console.log(err.url); // 'http://inventory-service/stock'
if (err.status === 404) {
res.json({ stock: [] });
return;
}
res.status(502).json({ error: 'Bad Gateway', message: 'Inventory service unavailable' });
return;
}
res.status(500).json({ error: 'Internal Server Error' });
}
});

Bridge Express only writes responses for auth/authz failures. Errors thrown inside your own handlers are yours to handle — register a standard Express error-handling middleware as the last app.use(...) to catch them:

app.use((err: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
if (err instanceof BridgeHttpError) {
res.status(502).json({ error: 'Bad Gateway', message: err.message });
return;
}
console.error(err);
res.status(500).json({ error: 'Internal Server Error' });
});