Work started for a request can outlive the reason for it: the client
disconnects, or an upstream call takes longer than anyone will wait. The
platform’s AbortSignal handles both.
ctx.req.signal aborts when the client disconnects. Pass it to the work
the handler starts, and that work stops when nobody is left to receive the
answer:
route({
method: "GET"
The method this route answers.
Kept as a literal rather than widened to
Method
: the method is
half of a route's identity, and an application that remembers its
routes — for a generated client, for tooling — needs to know which one
this is.
method: "GET",
path: "/rates"
Route path with :param segments, e.g. "/orders/:id/cancel".
Must start with /, contain no empty segments and no trailing slash;
a malformed literal is a compile error.
path: "/rates",
handler: (ctx: {
readonlyreq:Request& {
readonlycookies?:Bun.CookieMap;
};
readonlyserver:Bun.Server<unknown>;
readonlyout:Outgoing;
readonlyroute:RouteInfo;
readonlystartedAt:number;
readonlyparams: {};
}) =>Promise<Rates>
The endpoint logic; ctx is fully inferred, never annotate it.
The return type is inferred rather than demanded, and checked twice
over. Against the route's own contract, by the
HandlerResult
bound on R: answering with something response never declared is a
compile error that says so, instead of a structural diff against
Response. And against what the framework can serialize at all, by
the intersected
ValidateResult
: a stream handed over bare is
refused whether or not the route declared anything, because that is
the case no contract covers — without a response schema
HandlerResult is unknown and accepts every value there is.
The raw incoming request, always available as an escape hatch.
On a request that matched a route, Bun's router delivers its own
request object carrying cookies — a Bun.CookieMap whose mutations
are applied to the response as Set-Cookie automatically, with Bun's
defaults (Path=/; SameSite=Lax). The field is optional because it
does not exist where Bun's router was not involved: the 404 fallback
and unit-tested handlers. ctx.out.headers.append("set-cookie", ...)
is the fallback that works everywhere.
req.
signal: AbortSignal
The read-only signal property of the Request interface returns the AbortSignal associated with the request.
AbortSignal.timeout() aborts after a delay, and AbortSignal.any()
combines it with the request’s signal, so the work stops at whichever comes
first:
route({
method: "GET"
The method this route answers.
Kept as a literal rather than widened to
Method
: the method is
half of a route's identity, and an application that remembers its
routes — for a generated client, for tooling — needs to know which one
this is.
method: "GET",
path: "/quotes"
Route path with :param segments, e.g. "/orders/:id/cancel".
Must start with /, contain no empty segments and no trailing slash;
a malformed literal is a compile error.
path: "/quotes",
handler: (ctx: {
readonlyreq:Request& {
readonlycookies?:Bun.CookieMap;
};
readonlyserver:Bun.Server<unknown>;
readonlyout:Outgoing;
readonlyroute:RouteInfo;
readonlystartedAt:number;
readonlyparams: {};
}) =>Promise<{
id:string;
}>
The endpoint logic; ctx is fully inferred, never annotate it.
The return type is inferred rather than demanded, and checked twice
over. Against the route's own contract, by the
HandlerResult
bound on R: answering with something response never declared is a
compile error that says so, instead of a structural diff against
Response. And against what the framework can serialize at all, by
the intersected
ValidateResult
: a stream handed over bare is
refused whether or not the route declared anything, because that is
the case no contract covers — without a response schema
HandlerResult is unknown and accepts every value there is.
handler: async (
ctx: {
readonly req: Request & {
readonly cookies?: Bun.CookieMap;
};
readonly server: Bun.Server<unknown>;
readonly out: Outgoing;
readonly route: RouteInfo;
readonly startedAt: number;
readonly params: {};
}
ctx) => {
const
constsignal:AbortSignal
signal= AbortSignal.any([
ctx: {
readonly req: Request & {
readonly cookies?: Bun.CookieMap;
};
readonly server: Bun.Server<unknown>;
readonly out: Outgoing;
readonly route: RouteInfo;
readonly startedAt: number;
readonly params: {};
}
ctx.
req: Request & {
readonly cookies?: Bun.CookieMap;
}
The raw incoming request, always available as an escape hatch.
On a request that matched a route, Bun's router delivers its own
request object carrying cookies — a Bun.CookieMap whose mutations
are applied to the response as Set-Cookie automatically, with Bun's
defaults (Path=/; SameSite=Lax). The field is optional because it
does not exist where Bun's router was not involved: the 404 fallback
and unit-tested handlers. ctx.out.headers.append("set-cookie", ...)
is the fallback that works everywhere.
req.
signal: AbortSignal
The read-only signal property of the Request interface returns the AbortSignal associated with the request.
An expired deadline rejects with a DOMException named TimeoutError.
Left alone, it answers 500 and is reported as unhandled. An onError
hook turns it into the answer you mean:
constupstreamTimeout= hook.onError((ctx) => {
if (ctx.
error: unknown
errorinstanceofDOMException&& ctx.
error: DOMException
error.
name: string
The name read-only property of the one of the strings associated with an error name.
Aborting a signal does not stop the handler. It stops the calls that were
given the signal; everything else runs on. A query started without one
runs to completion, and a loop that never checks the signal keeps looping.
So pass the signal to every call that may take long, and in a loop of your
own call signal.throwIfAborted() between steps.
A framework timeout would answer 503 once a request takes too long. The
framework does not offer one, because JavaScript cannot interrupt a
running function: the handler would keep running, mid-query or
mid-payment, after the client was told the request failed. A client that
retries on 503 would then run the operation twice.
A deadline that really stops the work has to be passed to the work, and
only the handler knows which calls are safe to abandon.
AbortSignal.any() with AbortSignal.timeout() is that deadline, in one
line. For a retry that must not repeat its effect, the client sends an
idempotency key and the handler remembers what it answered.
Bun closes a connection that sends nothing for its idleTimeout, set where
the application is served. ctx.server.timeout(ctx.req, seconds) changes
it for one request, such as a long upload or a slow report:
route({
method: "POST"
The method this route answers.
Kept as a literal rather than widened to
Method
: the method is
half of a route's identity, and an application that remembers its
routes — for a generated client, for tooling — needs to know which one
this is.
method: "POST",
path: "/reports"
Route path with :param segments, e.g. "/orders/:id/cancel".
Must start with /, contain no empty segments and no trailing slash;
a malformed literal is a compile error.
path: "/reports",
handler: (ctx: {
readonlyreq:Request& {
readonlycookies?:Bun.CookieMap;
};
readonlyserver:Bun.Server<unknown>;
readonlyout:Outgoing;
readonlyroute:RouteInfo;
readonlystartedAt:number;
readonlyparams: {};
}) =>Promise<{
rows:number;
}>
The endpoint logic; ctx is fully inferred, never annotate it.
The return type is inferred rather than demanded, and checked twice
over. Against the route's own contract, by the
HandlerResult
bound on R: answering with something response never declared is a
compile error that says so, instead of a structural diff against
Response. And against what the framework can serialize at all, by
the intersected
ValidateResult
: a stream handed over bare is
refused whether or not the route declared anything, because that is
the case no contract covers — without a response schema
HandlerResult is unknown and accepts every value there is.
handler: async (
ctx: {
readonly req: Request & {
readonly cookies?: Bun.CookieMap;
};
readonly server: Bun.Server<unknown>;
readonly out: Outgoing;
readonly route: RouteInfo;
readonly startedAt: number;
readonly params: {};
}
ctx) => {
ctx: {
readonly req: Request & {
readonly cookies?: Bun.CookieMap;
};
readonly server: Bun.Server<unknown>;
readonly out: Outgoing;
readonly route: RouteInfo;
readonly startedAt: number;
readonly params: {};
}
ctx.
server: Bun.Server<unknown>
The server handling this request — the real Bun.Server, whole and
unguarded, like req an escape hatch to the platform.
The way to reach connection- and server-level facts a Request does
not carry: ctx.server.requestIP(ctx.req) for rate limiting by
address, ctx.server.timeout(ctx.req, seconds) for a per-request idle
timeout.
An address is in the form the socket reports it: a server listening on
both stacks — Bun's default — reports an IPv4 client as
::ffff:203.0.113.7, which a comparison with 203.0.113.7 misses.
Deliberately not a facade: every hook is code the
application author wrote or vetted, and hiding stop/reload from
in-process code protects nothing. They are still process-level
operations with no business inside a request — calling
ctx.server.stop() from a hook takes the whole listener down.
upgrade is for the endpoints ws() declares, which the application
upgrades itself. A socket upgraded by hand reaches the application's
websocket handler with no endpoint on it: the connection drops, and
each of its events is reported as a failure.
server.timeout(
ctx: {
readonly req: Request & {
readonly cookies?: Bun.CookieMap;
};
readonly server: Bun.Server<unknown>;
readonly out: Outgoing;
readonly route: RouteInfo;
readonly startedAt: number;
readonly params: {};
}
ctx.
req: Request & {
readonly cookies?: Bun.CookieMap;
}
The raw incoming request, always available as an escape hatch.
On a request that matched a route, Bun's router delivers its own
request object carrying cookies — a Bun.CookieMap whose mutations
are applied to the response as Set-Cookie automatically, with Bun's
defaults (Path=/; SameSite=Lax). The field is optional because it
does not exist where Bun's router was not involved: the 404 fallback
and unit-tested handlers. ctx.out.headers.append("set-cookie", ...)
is the fallback that works everywhere.
req, 120);
returnawait
constreports: {
build(signal:AbortSignal):Promise<{
rows:number;
}>;
}
reports.build(
ctx: {
readonly req: Request & {
readonly cookies?: Bun.CookieMap;
};
readonly server: Bun.Server<unknown>;
readonly out: Outgoing;
readonly route: RouteInfo;
readonly startedAt: number;
readonly params: {};
}
ctx.
req: Request & {
readonly cookies?: Bun.CookieMap;
}
The raw incoming request, always available as an escape hatch.
On a request that matched a route, Bun's router delivers its own
request object carrying cookies — a Bun.CookieMap whose mutations
are applied to the response as Set-Cookie automatically, with Bun's
defaults (Path=/; SameSite=Lax). The field is optional because it
does not exist where Bun's router was not involved: the 404 fallback
and unit-tested handlers. ctx.out.headers.append("set-cookie", ...)
is the fallback that works everywhere.
req.
signal: AbortSignal
The read-only signal property of the Request interface returns the AbortSignal associated with the request.
An event stream from @tetsujs/sse sets its own, above its heartbeat; see
Idle connections. On a unix socket
Bun ignores ctx.server.timeout(), and only the server’s idleTimeout
applies.