You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Some of the utility functions currently used in the examples are quite useful and generic enough that they could be provided by uWebSockets.js itself.
For example, a helper such as readJson could be exposed as part of the library instead of requiring users to copy the implementation from the examples.
This would have a couple of advantages:
users wouldn't have to copy and maintain these utilities themselves;
improvements and bug fixes could be propagated to all users through library updates;
examples could use the same utilities that are recommended for real applications;
it would make some common tasks easier without adding much complexity to the API.
If uWebSockets.js offered utilities like this, it would end up a bit like hyper-express or Bun. I don't think this is bad, I think it is an opportunity. Many people find uWebSockets.js hard to use compared to other solutions in Node.js, which give a simpler API: you only write your business logic and you don't have to care about the low level parts.
I am in favor, and I am happy to contribute to something like this.
The existing example is more complex than it needs to be. res.collectBody already does this natively: one buffer in C++, and null when the body is over a limit. The example just does not use it. With it readJson is a few lines:
I tried how close uWebSockets.js can get to Express like or hyper-express like. A handler that reads a JSON, changes it and answers with JSON could be 3 lines:
Today, with collectBody, the same handler is 10 lines, and the user must handle abort, cork, size limit and invalid JSON:
app.post('/user',(res,req)=>{letaborted=false;res.onAborted(()=>{aborted=true;});res.collectBody(1024*1024,async(body)=>{if(!body)returnres.writeStatus('413 Payload Too Large').end();letuser;try{user=JSON.parse(Buffer.from(body));}catch{returnres.writeStatus('400 Bad Request').end();}user.saved=awaitdb.save(user);if(aborted)return;res.cork(()=>res.writeHeader('content-type','application/json').end(JSON.stringify(user)));});});
Four things are missing:
collectJson returning a Promise. uWS answers 413 when the body is too big and 400 when the JSON is invalid, and on abort the promise never resolves, so the handler just stops. I have it as a prototype in 10 lines of JS over collectBody and it works: the code after the await still runs inside the uWS callback, so the write there is corked.
endJson(value): content-type application/json and the body from JSON.stringify.
Writes after a second await (the db.save) are not corked and print the warning. uWS could cork them by itself.
If the client goes away during db.save, res.end throws and, with Node defaults, the unhandled rejection stops the process. Writes after an abort could do nothing instead.
1 and 2 are only new API. 3 and 4 change how uWS works today, so I ask first: would you accept them? I can do the work.
After a deep diving, this is the my new API proposal, nothing changes for existing code:
collectBody, collectText and collectJson returning a Promise when there is no callback. A body too big gets 413, invalid JSON gets 400, and on abort the promise never resolves.
endJson(value): content-type and JSON.stringify.
writeStatus(404) with a number, uWS adds the reason phrase.
endFile(path), or endStream(stream, size) returning a Promise, done in C++
upgrade(userData, req, context): uWS reads the sec-websocket-* headers itself.
listen(port) returning a Promise when there is no callback.
Changes of behavior, so I ask first:
When a handler returns a Promise, uWS tracks the abort itself, corks the writes after an await, ignores writes after an abort instead of throwing, and answers 500 if the promise rejects. Today AsyncFunction, HttpCache, ServerSentEvents and UpgradeAsync all repeat onAborted, an aborted flag and cork.
Right, constants are better: uWS.HTTP_404 as "404 Not Found", no lookup and still easy to read.
upgrade(userData, req, context)
True, it helps only the sync upgrade (three getHeader calls less and no spelling). The async one needs #1036 first, so I drop it for now.
Promise
I measured it in #1348: the native promise resolves inside a node::CallbackScope, so it pays the same drain as MakeCallback, and it costs a few % over the callback (1 KB body: 21.1 vs 19.6 us per request, 18 KB: 188 vs 184 us). Not zero, but small next to the rest of the request.
It goes to the JS tick only when someone used nextTick or there is a rejection to warn. In a uWS handler that awaits collectJson this is rarely the case, so the promise drain is the vanilla one. What costs in MakeCallback is the work around it: the async id stack, the context frame for AsyncLocalStorage, the weakref cleanup.
That is what I am cutting in nodejs/node#66395 (MakeCallback 11-13% cheaper), and with #66326 and #66344 the goal is from about 160 to about 100 ns (nodejs/performance#24).
I don't want to give you the idea that I want to convince you to take one solution or the other. But I want to give you my opinion, and feel free to throw it away.
Today the whole JavaScript world turns around promises, and callbacks are seen as old. You are right that promises move away from the C++ interface and make you more dependent on Node than today, so think well about it, because you pay that price in maintenance. But from my point of view a callback API is now old, or at least it is seen like that. People will end up wrapping the callbacks in promises, or using some wrapper around uWS that does it, and both are slower.
New things that aren't in C++ can be Promises if they also have a callback version, so for instance collectBody and collectJson can have a Promise and callback version, that's no problem but the existing interface is not going to change just because someone thinks it's "outdated" or "not hip". uWS has never been "hip", and will never be.
The nice thing about collectJson with Promise is that it covers both the onAborted registration and the onData registration (since rejection is onAbort and resolve is onData). So in that case it even makes it simpler and maybe faster.
The C++ lib does not have collectBody or collectJson so those can be Promises.
Btw, I don't think Promises necessarily makes it more elegant or simpler. The problem with onAborted and onData isn't that they are callbacks, the problem is that onAborted is very non-elegant. I have forgotten what I thought about but I know I thought about replacing both of them with a generic onStream that would cover both events. I know there was some reason I didn't do it, but can't remember exactly.
Anyways, a nicer interface does not have to be Promise based it just needs to be less shitty (like onAborted which nobody likes). You can make extremely elegant interfaces only with callbacks.
I think before going all-in on Promises, one should think about replacing onAborted and onData with something that covers both.
I know there was some reason I didn't do it, but can't remember exactly
I looked for it: you added onStream in #1245 / #1246 on 15 March, and five days later you replaced it with onDataV2 (d0a49aa), before any release. From #1159 the reasons seem to be:
onStream took the onAborted slot, so it overwrote an onAborted set by the user or by a helper (the point of ValoChet).
An async GET with no body had to write if (chunk) return; only to get the abort.
You wanted to stay 1:1 with the C++ lib: "maybe just another variant of onData".
A version without 1 and 2 can be done only in the wrapper: the abort goes through one dispatcher that calls the onStream callback with null and also the onAborted callback, so they compose and nothing is overwritten. A GET keeps using onAborted alone. Point 3 stays: it is a JS only API, like collectBody.
If you think it is worth it I can make a PR. Otherwise I go on with #1348, with the promise that rejects on abort as you described (today the POC never settles it).
Activity
nigrosimone commentedon Oct 3, 2026
If uWebSockets.js offered utilities like this, it would end up a bit like hyper-express or Bun. I don't think this is bad, I think it is an opportunity. Many people find uWebSockets.js hard to use compared to other solutions in Node.js, which give a simpler API: you only write your business logic and you don't have to care about the low level parts.
I am in favor, and I am happy to contribute to something like this.
nigrosimone commentedon Oct 3, 2026
The existing example is more complex than it needs to be. res.collectBody already does this natively: one buffer in C++, and null when the body is over a limit. The example just does not use it. With it readJson is a few lines:
I opened #1347 to update the example.
nigrosimone commentedon Oct 3, 2026
I tried how close uWebSockets.js can get to Express like or hyper-express like. A handler that reads a JSON, changes it and answers with JSON could be 3 lines:
Today, with collectBody, the same handler is 10 lines, and the user must handle abort, cork, size limit and invalid JSON:
Four things are missing:
1 and 2 are only new API. 3 and 4 change how uWS works today, so I ask first: would you accept them? I can do the work.
nigrosimone commentedon Oct 3, 2026
After a deep diving, this is the my new API proposal, nothing changes for existing code:
collectBody,collectTextandcollectJsonreturning aPromisewhen there is no callback. A body too big gets 413, invalid JSON gets 400, and on abort the promise never resolves.endJson(value): content-type and JSON.stringify.writeStatus(404)with a number, uWS adds the reason phrase.endFile(path), orendStream(stream, size)returning aPromise, done in C++upgrade(userData, req, context): uWS reads the sec-websocket-* headers itself.listen(port)returning aPromisewhen there is no callback.Changes of behavior, so I ask first:
Promise, uWS tracks the abort itself, corks the writes after an await, ignores writes after an abort instead of throwing, and answers 500 if the promise rejects. Today AsyncFunction, HttpCache, ServerSentEvents and UpgradeAsync all repeat onAborted, an aborted flag and cork.uNetworkingAB commentedon Oct 3, 2026
This is not good. It might as well be writeStatus(UWS_HTTP_404) and skip the lookup cost. UWS_HTTP_404 is just a constant to the string.
uNetworkingAB commentedon Oct 3, 2026
The req doesn't survive async wait, so you cannot upgrade async with that interface
nigrosimone commentedon Oct 3, 2026
Right, constants are better: uWS.HTTP_404 as "404 Not Found", no lookup and still easy to read.
True, it helps only the sync upgrade (three getHeader calls less and no spelling). The async one needs #1036 first, so I drop it for now.
I measured it in #1348: the native promise resolves inside a node::CallbackScope, so it pays the same drain as MakeCallback, and it costs a few % over the callback (1 KB body: 21.1 vs 19.6 us per request, 18 KB: 188 vs 184 us). Not zero, but small next to the rest of the request.
nigrosimone commentedon Oct 3, 2026
One thing I saw while working on MakeCallback in Node: when no nextTick is pending, InternalCallbackScope::Close already drains the queue with V8's own MicrotaskQueue::PerformCheckpoint, the same call the auto policy makes:
https://github.com/nodejs/node/blob/9067cc47719232c044bfe22c8f206aa5c0f1a48d/src/api/callback.cc#L181-L185
It goes to the JS tick only when someone used nextTick or there is a rejection to warn. In a uWS handler that awaits collectJson this is rarely the case, so the promise drain is the vanilla one. What costs in MakeCallback is the work around it: the async id stack, the context frame for AsyncLocalStorage, the weakref cleanup.
That is what I am cutting in nodejs/node#66395 (MakeCallback 11-13% cheaper), and with #66326 and #66344 the goal is from about 160 to about 100 ns (nodejs/performance#24).
nigrosimone commentedon Oct 3, 2026
I don't want to give you the idea that I want to convince you to take one solution or the other. But I want to give you my opinion, and feel free to throw it away.
Today the whole JavaScript world turns around promises, and callbacks are seen as old. You are right that promises move away from the C++ interface and make you more dependent on Node than today, so think well about it, because you pay that price in maintenance. But from my point of view a callback API is now old, or at least it is seen like that. People will end up wrapping the callbacks in promises, or using some wrapper around uWS that does it, and both are slower.
uNetworkingAB commentedon Oct 3, 2026
New things that aren't in C++ can be Promises if they also have a callback version, so for instance collectBody and collectJson can have a Promise and callback version, that's no problem but the existing interface is not going to change just because someone thinks it's "outdated" or "not hip". uWS has never been "hip", and will never be.
The nice thing about collectJson with Promise is that it covers both the onAborted registration and the onData registration (since rejection is onAbort and resolve is onData). So in that case it even makes it simpler and maybe faster.
The C++ lib does not have collectBody or collectJson so those can be Promises.
uNetworkingAB commentedon Oct 3, 2026
Btw, I don't think Promises necessarily makes it more elegant or simpler. The problem with onAborted and onData isn't that they are callbacks, the problem is that onAborted is
very non-elegant. I have forgotten what I thought about but I know I thought about replacing both of them with a generic onStream that would cover both events. I know there was some reason I didn't do it, but can't remember exactly.Anyways, a nicer interface does not have to be Promise based it just needs to be less shitty (like onAborted which nobody likes). You can make extremely elegant interfaces only with callbacks.
I think before going all-in on Promises, one should think about replacing onAborted and onData with something that covers both.
nigrosimone commentedon Oct 4, 2026
I looked for it: you added
onStreamin #1245 / #1246 on 15 March, and five days later you replaced it withonDataV2(d0a49aa), before any release. From #1159 the reasons seem to be:onStreamtook theonAbortedslot, so it overwrote anonAbortedset by the user or by a helper (the point of ValoChet).if (chunk) return;only to get the abort.A version without 1 and 2 can be done only in the wrapper: the abort goes through one dispatcher that calls the
onStreamcallback withnulland also theonAbortedcallback, so they compose and nothing is overwritten. A GET keeps usingonAbortedalone. Point 3 stays: it is a JS only API, likecollectBody.If you think it is worth it I can make a PR. Otherwise I go on with #1348, with the promise that rejects on abort as you described (today the POC never settles it).