This release introduces powerful improvements to ctx.file()
, allowing direct, streaming responses from native, Node, and cloud storage sources β alongside foundational work to unify stream behavior and remove legacy friction.
Improved
- feat: [EXPERIMENTAL]
ctx.file()
on top of passing a path now also supports direct streaming from a variety of sources. NativeReadableStream
(Workerd, Bun, and browser-compatible sources), Node.jsReadable
streams (e.g. fromfs.createReadStream
, S3 SDKs), Buffers, strings,Uint8Array
,ArrayBuffer
andBlob
inputs. This makes TriFrost file responses work seamlessly with S3, R2, and dynamic stream-based backends. One API, many sources. π - qol:
ctx.file()
withdownload: true
now uses the original file name in theContent-Disposition
header, giving users a proper fallback for filenames in their downloads when not passing a custom one. - qol: Internals for
ctx.stream()
(a protected internal method) and runtime stream handling (Bun/Workerd/Node) have been unified and hardened. - qol: Added runtime-safe validation for supported stream types.
- qol: Void tags in the JSX renderer now includes svg-spec tags
path
,circle
,ellipse
,line
,polygon
,polyline
,rect
,stop
,use
. - qol: Inline atomic hydrator will no longer be given a
defer
marker as there's no longer a need for this - deps: Upgrade @types/node to 22.15.33
- deps: Upgrade typescript-eslint to 8.35.0
Example using s3 sdk:
import {GetObjectCommand, S3Client} from '@aws-sdk/client-s3';
const s3 = new S3Client({ region: 'us-east-1' });
const Bucket = 'my-bucket';
export default async function handler (ctx) {
const Key = ctx.query.get('key');
if (!Key) return ctx.status(400);
try {
const {Body, ContentLength} = await s3.send(new GetObjectCommand({Bucket, Key}));
return ctx.file({
stream: Body,
size: ContentLength,
name: Key,
}, {download: true});
} catch (err) {
ctx.logger.error('S3 fetch failed', { err });
ctx.status(404);
}
}
Example using Cloudflare R2 (workerd runtime):
export default async function handler (ctx) {
const key = ctx.query.get('key');
if (!key) return ctx.status(400);
try {
const res = await ctx.env.MY_BUCKET.get(key); /* R2 binding via `wrangler.toml` */
if (!res?.body) return ctx.status(404);
return ctx.file({
stream: res.body,
size: res.size ?? null,
name: key,
}, {download: true});
} catch (err) {
ctx.logger.error('R2 fetch failed', {err});
ctx.status(500);
}
}
Fixed
- Fixed an edge case where long-running streams in
ctx.file()
could incorrectly inherit stale timeouts.
Deprecated
- π§Ή Removed: `UWSContext` (uWebSockets.js runtime). Back in 2024,
uWS
felt like a great addition to TriFrost β an "automatic upgrade" path for Node users who wanted raw speed with minimal changes. But the landscape has shifted: Node has steadily improved its performance, whileuWS
continues to demand non-standard stream handling, complicates internal abstractions and also has some quirks (such as the fact that they add a uWS header to every response and that uWS will not work on a large amount of systems when containerizing). As a result after long pondering and thinking, we've removed support foruWS
. This eliminates boilerplate, makes TriFrost just that bit leaner while simplifying internal stream behavior and clearing the path for better DX and broader runtime support (π looking at you, Netlify and Vercel). Donβt worry though, if you're currently running TriFrost with uWS, the system will gracefully fall back to the Node runtime, no changes required from your end.
With the removal of the uWS runtime, TriFrost enters a new phase: simpler, cleaner, and more aligned with modern cross-runtime needs. Whether you're streaming files from S3, R2, or piping blobs from memory, the file API stays minimal and consistent.
And maybe, somewhere in the near future we'll add another runtime or two, can't wait!
As always, stay frosty βοΈ