feat(http): typed context extension via next(extra) in middleware #14
No Reviewers
Labels
Clear labels
Agentic
Component/CI
Component/Funnel
Component/React
Component/State
Component/Taildrive
Component/Taildrop
Component/Tailscale
Component/Tailshare
Component/Transport
Component/tsconnect
Component/VFS
Component/WebRTC
Component/Worker
Human
Protocol/FTP
Protocol/HTTP
Protocol/SFTP
Protocol/SMB
Protocol/SSH
Protocol/WebDAV
Protocol/WebSocket
Security
Agent
claude-fable-5
1
Work done by Claude Fable 5
Agent
claude-opus-4-8
2
Work done by Claude Opus 4.8
Agent
claude-opus-5
Work done by Claude Opus 5
Agent
claude-sonnet-4-6
3
Work done by Claude Sonnet 4.6
Agent
claude-sonnet-5
Work done by Claude Sonnet 5
Agent
gpt-5.5
7
Work done by GPT 5.5
Agent
gpt-5.6-luna
6
Work done by GPT 5.6 Luna
Agent
gpt-5.6-sol
4
Work done by GPT 5.6 Sol
Agent
gpt-5.6-terra
5
Work done by GPT 5.6 Terra
Opened by an agent
Work on the CI tooling
Work on the Tailscale Funnel or certificate system
Work on a React binding
Work on a state store (eg Redux)
Work on the taildrive system
Work on the taildrop system
Work on the Tailscale fork
Work on the Tailshare app
Work on the transport system
Work on the tsconnect packages
Work on the VFS system
Work on the WebRTC system
Work on the worker system
Opened by a human
Kind
Bug
Bug work
Kind
Enhancement
Enhancement work
Kind
Feature
Feature work
Kind
Maintenance
Maintenance work
Priority
P0
1
Critical work that must be done right now
Priority
P1
2
Urgent work
Priority
P2
3
Medium priority work
Priority
P3
4
Low priority work
Priority
P4
5
Lowest priority work, wishlist-tier
Work on the FTP protocol
Work on the HTTP protocol
Work on the SFTP protocol
Work on the SMB protocol
Work on the SSH protocol
Work on the WebDAV protocol
Work on the WebSocket protocol
Security work
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: webnet/webnet#14
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What
Three additions to the
httppackage router:1. Typed context extension via
next(extra)Middlewares declare what they add to the context by annotating
nextwithNext<TAdd>. TypeScript propagates the type to all downstream middlewares and handlers. No casts needed.```typescript
app.use(async (ctx, next: Next<{ user: User }>) => {
await next({ user: await authenticate(ctx.req) })
})
.get("/profile", async (ctx) => {
ctx.user // typed as User
})
```
2.
route(path)sub-path proxyrouter.route(path)returns aRouteProxy<T, TGlobal>that prependspathto every registration method. Routes land directly in the underlying router's layer list. Nesting is supported.```typescript
const api = app.route("/api")
api.get("/users", listUsers) // GET /api/users
api.route("/users/:id").get(getUser) // GET /api/users/:id
```
`route(path).use(mw)` is also the correct pattern for path-scoped typed extension — registering a middleware whose context additions are only visible to handlers also registered on that proxy.
3. Soundness fixes
Several type-level soundness issues are resolved:
Path-scoped extending overload removed.
use(path, mw)on bothRouterandRouteProxyonly has non-extending overloads. If the middleware'snextis annotated withNext<TAdd>, the call still compiles (callback contravariance), butTis not updated and handlers at other paths cannot access the added properties.route(path).use(mw)is the safe pattern.Router<T, TGlobal>second type parameter.TGlobaltracks what global middleware has contributed. Global middleware receivesContext<TGlobal & Partial<T> & PartialMatch>: its own prior global additions are required, but non-global additions arePartialbecause non-global mw does not run on 404/405 paths.RouteProxyexported as type-only. The class is accessible via_internalsfor advanced use; public consumers only see the TypeScript type (returned byrouter.route()).4. Compile-time type tests
tsconfig.typetest.json+src/server/router.typetest.tsadd positive and negative@ts-expect-errorassertions, checked vianpm run typetest. If a regression removes an expected error, the "unused @ts-expect-error" directive becomes a build failure.How
Next<TAdd>— conditional generic:() => Promise<void>whenTAdd = {},(extra: TAdd) => Promise<void>otherwise. Annotatingnext: Next<TAdd>gives TypeScript enough information to inferTAddand select the extending overload.Router<T, TGlobal>—T= full accumulated context (route handlers seeContext<T & Match>);TGlobal ⊆ T= globally-guaranteed subset. Non-globaluse(mw)growsTonly; globaluse({global:true}, mw)grows both. The formulaTGlobal & Partial<T>correctly makes non-global additions optional for global middleware while keeping global additions required.Overload ordering — extending overloads come before non-extending ones. TypeScript rejects an extending overload when
next()is called without args and falls through to the non-extending form.RouteProxy— delegates all registration toRouter.layer()with paths joined. The#routerfield is typed asRouter<T, TGlobal>, so both type parameters flow through the proxy chain.Router<T> is now generic. Middlewares can call next({ key: value }) to extend the context for all downstream middlewares and handlers. TypeScript infers the added type from the argument — no casts or explicit annotations required by callers. The overload design places extending overloads (required next arg) before non-extending ones so TypeScript naturally falls through based on whether next() is called with or without arguments. The chain builder was reworked to thread context as an argument so extensions accumulate per-request. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>913f964031to00f7f5600e