diff --git a/.gitignore b/.gitignore
index 090cd6e..98775e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -73,6 +73,8 @@ __pycache__/
# Node
node_modules/
package-lock.json
+# ...except for servers, where the lock pins the image's dependencies by hash
+!src/Servers/**/package-lock.json
# Probe results (local testing)
probe-*.json
diff --git a/docs/content/servers/uwebsockets.md b/docs/content/servers/uwebsockets.md
new file mode 100644
index 0000000..9e0d7a2
--- /dev/null
+++ b/docs/content/servers/uwebsockets.md
@@ -0,0 +1,137 @@
+---
+title: "uWebSockets.js"
+description: "uWebSockets.js (JavaScript) tested against RFC 9110/9112 for HTTP/1.1 compliance, request smuggling resistance, and malformed input handling."
+toc: true
+breadcrumbs: false
+---
+
+**Language:** JavaScript · [View source on GitHub](https://github.com/MDA2AV/Http11Probe/tree/main/src/Servers/UWebSocketsServer)
+
+## Dockerfile
+
+```dockerfile
+# trixie, not the default bookworm: uWS ships prebuilt binaries needing glibc >= 2.38
+FROM node:22-trixie-slim
+WORKDIR /app
+COPY src/Servers/UWebSocketsServer/package.json src/Servers/UWebSocketsServer/package-lock.json ./
+RUN npm ci --omit=dev --ignore-scripts
+COPY src/Servers/UWebSocketsServer/server.js .
+USER node
+ENTRYPOINT ["node", "server.js", "8080"]
+```
+
+## Source — `package.json`
+
+uWebSockets.js is not published to the npm registry, so it is pinned to a release tarball. Release tags carry prebuilt `.node` binaries, so there is no build step and no lifecycle scripts to run. A generated `package-lock.json` sits alongside this file, pinning the tarball by sha512 integrity hash so `npm ci` gets identical bytes on every build.
+
+```json
+{
+ "name": "uwebsockets-server",
+ "private": true,
+ "dependencies": {
+ "uWebSockets.js": "https://github.com/uNetworking/uWebSockets.js/archive/refs/tags/v20.69.0.tar.gz"
+ }
+}
+```
+
+## Source — `server.js`
+
+```javascript
+const uWS = require('uWebSockets.js');
+
+const port = Number.parseInt(process.argv[2] || '8080', 10);
+
+/* uWS invalidates `req` the moment the handler returns, so everything needed
+ * later has to be read out synchronously. Only the body echo is async here. */
+
+function readBody(res, onDone) {
+ const chunks = [];
+ res.onAborted(() => { res.aborted = true; });
+ res.onData((ab, isLast) => {
+ /* The ArrayBuffer is only valid inside this callback — slice(0) copies it. */
+ chunks.push(Buffer.from(ab.slice(0)));
+ if (isLast) onDone(Buffer.concat(chunks));
+ });
+}
+
+const app = uWS.App();
+
+app.any('/cookie', (res, req) => {
+ let body = '';
+ const raw = req.getHeader('cookie');
+ for (const pair of raw.split(';')) {
+ const trimmed = pair.trimStart();
+ const eq = trimmed.indexOf('=');
+ if (eq > 0) body += trimmed.substring(0, eq) + '=' + trimmed.substring(eq + 1) + '\n';
+ }
+ res.writeHeader('Content-Type', 'text/plain');
+ res.end(body);
+});
+
+app.any('/echo', (res, req) => {
+ let body = '';
+ req.forEach((name, value) => { body += name + ': ' + value + '\n'; });
+ res.writeHeader('Content-Type', 'text/plain');
+ res.end(body);
+});
+
+/* Wildcards must be registered last. */
+app.any('/*', (res, req) => {
+ if (req.getMethod() === 'post') {
+ readBody(res, (body) => {
+ if (res.aborted) return;
+ /* Cork when responding from an async callback. */
+ res.cork(() => {
+ res.writeHeader('Content-Type', 'text/plain');
+ res.end(body);
+ });
+ });
+ return;
+ }
+ res.writeHeader('Content-Type', 'text/plain');
+ res.end('OK');
+});
+
+app.listen('0.0.0.0', port, (token) => {
+ if (!token) {
+ console.error('Failed to listen on port ' + port);
+ process.exit(1);
+ }
+});
+```
+
+## Test Results
+
+
+
+### Compliance
+
+
+
+### Smuggling
+
+
+
+### Malformed Input
+
+
+
+### Caching
+
+
+
+### Cookies
+
+
+
+
+
+
diff --git a/src/Servers/UWebSocketsServer/Dockerfile b/src/Servers/UWebSocketsServer/Dockerfile
new file mode 100644
index 0000000..a6ea591
--- /dev/null
+++ b/src/Servers/UWebSocketsServer/Dockerfile
@@ -0,0 +1,8 @@
+# trixie, not the default bookworm: uWS ships prebuilt binaries needing glibc >= 2.38
+FROM node:22-trixie-slim
+WORKDIR /app
+COPY src/Servers/UWebSocketsServer/package.json src/Servers/UWebSocketsServer/package-lock.json ./
+RUN npm ci --omit=dev --ignore-scripts
+COPY src/Servers/UWebSocketsServer/server.js .
+USER node
+ENTRYPOINT ["node", "server.js", "8080"]
diff --git a/src/Servers/UWebSocketsServer/package-lock.json b/src/Servers/UWebSocketsServer/package-lock.json
new file mode 100644
index 0000000..53c1add
--- /dev/null
+++ b/src/Servers/UWebSocketsServer/package-lock.json
@@ -0,0 +1,19 @@
+{
+ "name": "uwebsockets-server",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "uwebsockets-server",
+ "dependencies": {
+ "uWebSockets.js": "https://github.com/uNetworking/uWebSockets.js/archive/refs/tags/v20.69.0.tar.gz"
+ }
+ },
+ "node_modules/uWebSockets.js": {
+ "version": "20.69.0",
+ "resolved": "https://github.com/uNetworking/uWebSockets.js/archive/refs/tags/v20.69.0.tar.gz",
+ "integrity": "sha512-vSARYbg98BfI8G4hB305Oa4iU3Ocon3o+SKzy1iza7hQGECgZSxJJ0/yp7LvPdTdQlY3bVQs+A4Ir7tCGQ2hnA==",
+ "license": "Apache-2.0"
+ }
+ }
+}
diff --git a/src/Servers/UWebSocketsServer/package.json b/src/Servers/UWebSocketsServer/package.json
new file mode 100644
index 0000000..caa1807
--- /dev/null
+++ b/src/Servers/UWebSocketsServer/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "uwebsockets-server",
+ "private": true,
+ "dependencies": {
+ "uWebSockets.js": "https://github.com/uNetworking/uWebSockets.js/archive/refs/tags/v20.69.0.tar.gz"
+ }
+}
diff --git a/src/Servers/UWebSocketsServer/probe.json b/src/Servers/UWebSocketsServer/probe.json
new file mode 100644
index 0000000..ee071da
--- /dev/null
+++ b/src/Servers/UWebSocketsServer/probe.json
@@ -0,0 +1 @@
+{"name": "uWebSockets.js", "language": "JavaScript", "repository": "https://github.com/uNetworking/uWebSockets.js"}
diff --git a/src/Servers/UWebSocketsServer/server.js b/src/Servers/UWebSocketsServer/server.js
new file mode 100644
index 0000000..6868cee
--- /dev/null
+++ b/src/Servers/UWebSocketsServer/server.js
@@ -0,0 +1,61 @@
+const uWS = require('uWebSockets.js');
+
+const port = Number.parseInt(process.argv[2] || '8080', 10);
+
+/* uWS invalidates `req` the moment the handler returns, so everything needed
+ * later has to be read out synchronously. Only the body echo is async here. */
+
+function readBody(res, onDone) {
+ const chunks = [];
+ res.onAborted(() => { res.aborted = true; });
+ res.onData((ab, isLast) => {
+ /* The ArrayBuffer is only valid inside this callback — slice(0) copies it. */
+ chunks.push(Buffer.from(ab.slice(0)));
+ if (isLast) onDone(Buffer.concat(chunks));
+ });
+}
+
+const app = uWS.App();
+
+app.any('/cookie', (res, req) => {
+ let body = '';
+ const raw = req.getHeader('cookie');
+ for (const pair of raw.split(';')) {
+ const trimmed = pair.trimStart();
+ const eq = trimmed.indexOf('=');
+ if (eq > 0) body += trimmed.substring(0, eq) + '=' + trimmed.substring(eq + 1) + '\n';
+ }
+ res.writeHeader('Content-Type', 'text/plain');
+ res.end(body);
+});
+
+app.any('/echo', (res, req) => {
+ let body = '';
+ req.forEach((name, value) => { body += name + ': ' + value + '\n'; });
+ res.writeHeader('Content-Type', 'text/plain');
+ res.end(body);
+});
+
+/* Wildcards must be registered last. */
+app.any('/*', (res, req) => {
+ if (req.getMethod() === 'post') {
+ readBody(res, (body) => {
+ if (res.aborted) return;
+ /* Cork when responding from an async callback. */
+ res.cork(() => {
+ res.writeHeader('Content-Type', 'text/plain');
+ res.end(body);
+ });
+ });
+ return;
+ }
+ res.writeHeader('Content-Type', 'text/plain');
+ res.end('OK');
+});
+
+app.listen('0.0.0.0', port, (token) => {
+ if (!token) {
+ console.error('Failed to listen on port ' + port);
+ process.exit(1);
+ }
+});