Files
What you'll learn
- Accept a bearer token or basic credentials on the same routes with some(), and know whose challenge a refused request sees
- Chain checks that must all pass with every(), in order
- Exclude paths from a guard with except(), by pattern or by a function of the context
Combining middleware
Eight lessons have added eight guards, each a single app.use(). Real APIs need combinations: a route that accepts a token or a password, a set of checks that must all hold, a rule that applies everywhere except the health check and the public listing. Written by hand those are nested conditionals inside a custom middleware, and they are easy to get subtly wrong, calling next() twice or not at all. hono/combine gives three functions that compose middleware into one, and this lesson uses all three on the cats API.
some(): the first that accepts
import { some } from 'hono/combine';
app.use('/api/*', some(bearerAuth({ token }), basicAuth({ username, password })));
some(a, b, …) runs a; if it throws, the way every auth middleware refuses, b runs on the same request, and so on. The first middleware that lets the request through ends the search. When every one refuses, the error of the last one is what the client sees, so some(bearerAuth(…), basicAuth(…)) refuses with WWW-Authenticate: Basic …, which a browser turns into a login prompt, while the reverse order refuses with the bearer challenge. Order the alternatives by which refusal you want a client without credentials to meet.
A middleware inside some() must refuse by throwing or by setting c.res, as the built-ins do. One that merely returns a Response is not treated as a refusal and its response is dropped, which ends as a 500.
every(): all of them, in order
import { every } from 'hono/combine';
app.use('/api/*', every(requestId(), rateLimit(), bearerAuth({ token })));
every(a, b, …) runs each in turn, exactly as registering them one after another would, and stops at the first that refuses. It exists for the composition: an every() can be one branch of a some(), and the result is one middleware to register or export.
except(): everywhere but here
import { except } from 'hono/combine';
app.use('/api/*', except('/api/public/*', bearerAuth({ token })));
app.use('*', except(['/health', '/public/*'], bearerAuth({ token })));
app.use('*', except((c) => c.req.method === 'GET', bearerAuth({ token })));
except(condition, mw…) skips its middleware when the condition holds. A string is a route pattern, an array is several, and a function of the context decides anything else, such as letting every GET through unguarded. Underneath it is some(condition, every(mw…)): the condition as a middleware that passes when it matches, and the rest when it does not.
Composing
Combinators nest. The guard for the cats API reads inside out: either credential, some(bearer, basic); with a request id first, every(requestId(), guard); except the public routes, except('/api/public/*', …); mounted on /api/*. One app.use() line, one place to read what protects the API, and one thing to export and reuse in another app.
Your task
Everything under /api is open.
- Build one guard that accepts either the bearer token
TOKENor the basic credentialsadmin/secretin the realmCats, ordered so a request with no credentials sees the Basic challenge. - Combine it with
requestId()so every guarded request gets an id first. - Mount the combination on
/api/*, excluding/api/public/*. GET /api/catsanswers{ "requestId", "cats" }.
Send GET /api/cats from the request bar with no header, then with Authorization: Bearer cat-token, then with Authorization: Basic YWRtaW46c2VjcmV0.
When it fails
- A request with no credentials sees
Bearer realm="": the alternatives are in the other order. The last one's refusal is the one shown. /api/public/catsasks for credentials:except()is missing, or its pattern lacks the/*.requestIdisundefinedin the response:requestId()is outside the combination, or after the guard inevery(), so it never ran on a refused request; put it first.- Every guarded request answers
500and the console saysContext is not finalized: a custom middleware insidesome()returned a response instead of throwing. The built-ins throw; a custom guard shouldthrow new HTTPException(401, …).
Remember
some(a, b)accepts with the first that passes and refuses with the last one's error.every(a, b)runs all, in order, stopping at the first refusal.except(pattern | patterns | fn, mw)skips the middleware where the condition holds.- Compose inside out into one middleware; guards inside
some()must throw to refuse.
Stuck? Show a hint
some(a, b) runs a and, if it throws, b; every(a, b) runs both. except('/api/public/*', mw) skips mw for matching paths. Compose from the inside out: the guard is some(bearerAuth(…), basicAuth(…)); every(requestId(), guard) adds the id; except(…) wraps the whole thing; app.use('/api/*', …) mounts it.
Press Run tests to start the app. Its log appears here.Graded endpoints
except() skips the whole combination for /api/public/*: no credentials, no request id
Both guards in some() refused; the last one's challenge is the answer, so the Basic realm is what a browser sees
The first guard accepts, so the second never runs; every() also ran requestId(), which the handler reads
The bearer guard refuses and some() moves on to basicAuth, which accepts admin:secret
Refused by both: again the last guard's challenge
The same combined guard on every method under /api
Past the guard, the handler's 404
Tom is gone for everyone