From 90138b21a6e99f5855a2e0ef0393aa16b4ce7bd4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fatih=20Ayg=C3=BCn?=
Date: Mon, 8 Jan 2024 18:14:35 +0000
Subject: [PATCH] fix: bring uwebsockets adapter back to working order (#117)
---
.../adapter/adapter-uwebsockets/package.json | 3 +-
.../adapter/adapter-uwebsockets/src/common.ts | 71 ++++++++-----------
packages/middleware/static/src/index.ts | 2 -
pnpm-lock.yaml | 13 ++--
testbed/basic/ci.test.ts | 20 ++++--
testbed/basic/entry-uws.js | 29 +++++---
testbed/basic/readme.md | 62 +---------------
7 files changed, 71 insertions(+), 129 deletions(-)
diff --git a/packages/adapter/adapter-uwebsockets/package.json b/packages/adapter/adapter-uwebsockets/package.json
index 4321fd5a..5b2f2469 100644
--- a/packages/adapter/adapter-uwebsockets/package.json
+++ b/packages/adapter/adapter-uwebsockets/package.json
@@ -40,7 +40,6 @@
"dependencies": {
"@hattip/core": "workspace:*",
"@hattip/polyfills": "workspace:*",
- "mrmime": "^2.0.0",
- "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.31.0"
+ "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.38.0"
}
}
diff --git a/packages/adapter/adapter-uwebsockets/src/common.ts b/packages/adapter/adapter-uwebsockets/src/common.ts
index 46e58205..8715649c 100644
--- a/packages/adapter/adapter-uwebsockets/src/common.ts
+++ b/packages/adapter/adapter-uwebsockets/src/common.ts
@@ -32,8 +32,6 @@ export interface UWebSocketAdapterOptions {
trustProxy?: boolean;
/** Use SSL (https) */
ssl?: boolean;
- /** Static file directory */
- staticDir?: string;
/**
* Callback to configure the uWebSockets.js app.
* Useful for adding WebSocket or HTTP routes before the HatTip handler
@@ -154,8 +152,7 @@ export function createServer(
: new ReadableStream({
start(controller) {
res.onData((chunk, isLast) => {
- const buffer = Buffer.from(chunk);
- controller.enqueue(buffer);
+ controller.enqueue(new Uint8Array(chunk));
if (isLast) controller.close();
});
},
@@ -177,51 +174,41 @@ export function createServer(
async function finish(response: Response) {
if (aborted) return;
- res.writeStatus(
- `${response.status}${
- response.statusText ? " " + response.statusText : ""
- }`,
- );
-
- response.headers.forEach((value, key) => {
- if (key === "set-cookie") {
- const values = response.headers.getSetCookie();
- for (const value of values) {
- res.writeHeader(key, value);
+ res.cork(() => {
+ res.writeStatus(
+ `${response.status}${
+ response.statusText ? " " + response.statusText : ""
+ }`,
+ );
+
+ const uniqueHeaderNames = new Set(response.headers.keys());
+ for (const name of uniqueHeaderNames) {
+ if (name === "set-cookie") {
+ for (const value of response.headers.getSetCookie()) {
+ res.writeHeader(name, value);
+ }
+ } else {
+ res.writeHeader(name, response.headers.get(name)!);
}
- } else {
- res.writeHeader(key, value);
+ }
+
+ if (!response.body) {
+ res.end();
}
});
if (response.body) {
- const reader = (response.body as any as AsyncIterable)[
- Symbol.asyncIterator
- ]();
-
- const first = await reader.next();
- if (first.done) {
- res.end();
- } else {
- const secondPromise = reader.next();
- let second = await Promise.race([
- secondPromise,
- Promise.resolve(null),
- ]);
-
- if (second && second.done) {
- res.end(first.value);
- } else {
- res.write(first.value);
- second = await secondPromise;
- for (; !second.done; second = await reader.next()) {
- res.write(Buffer.from(second.value));
- }
- res.end();
+ let lastChunk: Uint8Array | undefined;
+ for await (const chunk of response.body as any as AsyncIterable) {
+ if (lastChunk) {
+ res.cork(() => res.write(lastChunk!));
}
+ lastChunk = chunk;
+ }
+
+ if (lastChunk) {
+ res.cork(() => res.end(lastChunk!));
}
- } else {
- res.end();
}
}
});
diff --git a/packages/middleware/static/src/index.ts b/packages/middleware/static/src/index.ts
index d78de60d..f8d56dd8 100644
--- a/packages/middleware/static/src/index.ts
+++ b/packages/middleware/static/src/index.ts
@@ -107,8 +107,6 @@ export function createStaticMiddleware(
return new Response(null, { status: 304 });
}
- headers.set("content-length", file.size.toString());
-
if (file.etag) {
headers.set("etag", file.etag);
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 61f0e812..edd7384e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -384,12 +384,9 @@ importers:
'@hattip/polyfills':
specifier: workspace:*
version: link:../../base/polyfills
- mrmime:
- specifier: ^2.0.0
- version: 2.0.0
uWebSockets.js:
- specifier: github:uNetworking/uWebSockets.js#v20.31.0
- version: github.com/uNetworking/uWebSockets.js/809b99d2d7d12e2cbf89b7135041e9b41ff84084
+ specifier: github:uNetworking/uWebSockets.js#v20.38.0
+ version: github.com/uNetworking/uWebSockets.js/560035d4ad6caeda563deee1d3d68143462a305b
devDependencies:
'@cyco130/eslint-config':
specifier: ^3.6.0
@@ -14101,8 +14098,8 @@ packages:
/zod@3.22.4:
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
- github.com/uNetworking/uWebSockets.js/809b99d2d7d12e2cbf89b7135041e9b41ff84084:
- resolution: {tarball: https://codeload.github.com/uNetworking/uWebSockets.js/tar.gz/809b99d2d7d12e2cbf89b7135041e9b41ff84084}
+ github.com/uNetworking/uWebSockets.js/560035d4ad6caeda563deee1d3d68143462a305b:
+ resolution: {tarball: https://codeload.github.com/uNetworking/uWebSockets.js/tar.gz/560035d4ad6caeda563deee1d3d68143462a305b}
name: uWebSockets.js
- version: 20.31.0
+ version: 20.38.0
dev: false
diff --git a/testbed/basic/ci.test.ts b/testbed/basic/ci.test.ts
index dd8faf9e..dbd09ee3 100644
--- a/testbed/basic/ci.test.ts
+++ b/testbed/basic/ci.test.ts
@@ -54,7 +54,7 @@ if (process.env.CI === "true") {
const bunAvailable = process.platform !== "win32";
- const uwsAvailable = false; // nodeVersionMajor >= 18 && process.platform === "linux";
+ const uwsAvailable = true; // nodeVersionMajor >= 18 && process.platform === "linux";
// if (!uwsAvailable) {
// console.warn(
// "Node version < 18 or not on Linux, will skip uWebSockets.js tests",
@@ -136,7 +136,7 @@ if (process.env.CI === "true") {
},
uwsAvailable && {
name: "uWebSockets.js",
- command: `node ${noFetchFlag} entry-uws.js`,
+ command: `node entry-uws.js`,
},
{
name: "Lagon",
@@ -305,12 +305,17 @@ describe.each(cases)(
);
const text = await response.text();
- let ip;
+ let ip: string;
+ let ip6: string;
if (["127.0.0.1", "localhost"].includes(new URL(host).hostname)) {
ip = "127.0.0.1";
+ ip6 = "::1";
} else {
- ip = await fetch("http://api.ipify.org").then((r) => r.text());
+ [ip, ip6] = await Promise.all([
+ fetch("http://api.ipify.org").then((r) => r.text()),
+ fetch("http://api64.ipify.org").then((r) => r.text()),
+ ]);
}
let hostName = host;
@@ -323,7 +328,12 @@ describe.each(cases)(
hostName + "/"
}
Your IP address is: ${ip}
`;
- expect(text).toContain(EXPECTED);
+ const EXPECTED_6 = `Hello from Hattip!
URL: ${
+ hostName + "/"
+ }
Your IP address is: ${ip6}
`;
+
+ expect([EXPECTED, EXPECTED_6]).toContain(text);
+
expect(response.headers.get("content-type")).toEqual(
"text/html; charset=utf-8",
);
diff --git a/testbed/basic/entry-uws.js b/testbed/basic/entry-uws.js
index 90d0c934..8d54a7d1 100644
--- a/testbed/basic/entry-uws.js
+++ b/testbed/basic/entry-uws.js
@@ -1,14 +1,23 @@
// @ts-check
-import { createServer } from "@hattip/adapter-uwebsockets";
+import { createServer } from "@hattip/adapter-uwebsockets/native-fetch";
+import { walk } from "@hattip/walk";
import handler from "./index.js";
+import { createStaticMiddleware } from "@hattip/static";
+import { createFileReader } from "@hattip/static/fs";
-createServer(handler, {
- staticDir: "./public",
-}).listen(3000, (success) => {
- if (!success) {
- console.error("Failed to listen on port 3000");
- process.exit(1);
- }
+const root = new URL("./public", import.meta.url);
+const files = walk(root);
+const reader = createFileReader(root);
+const staticMiddleware = createStaticMiddleware(files, reader);
- console.log("Server listening on http://127.0.0.1:3000");
-});
+createServer((ctx) => staticMiddleware(ctx) || handler(ctx)).listen(
+ 3000,
+ (success) => {
+ if (!success) {
+ console.error("Failed to listen on port 3000");
+ process.exit(1);
+ }
+
+ console.log("Server listening on http://127.0.0.1:3000");
+ },
+);
diff --git a/testbed/basic/readme.md b/testbed/basic/readme.md
index 137ae0ea..cb5c1e1c 100644
--- a/testbed/basic/readme.md
+++ b/testbed/basic/readme.md
@@ -6,88 +6,30 @@ When the environment variable `CI` equals `true`, `pnpm run ci` will all the aut
To manually test streaming, run `curl -ND - 'http://127.0.0.1:3000/bin-stream?delay=50'` and observe the typewriter effect.
-## Status
+## Manual Tests
-### Node.js with `node-fetch`
-
-All tests pass.
-
-### Node.js with native fetch
-
-All tests pass.
-
-### Cloudflare Workers with `wrangler dev`
-
-All tests pass.
-
-Launch with `wrangler dev --port 3000`.
-
-### Netlify Functions with `netlify serve`
-
-All tests except "doesn't fully buffer binary stream" pass which is automatically skipped in the CI. Netlify Functions have no streaming support.
-
-Build locally with `pnpm build:netlify-functions`, test with `netlify serve`.
-
-### Netlify Edge Functions with `netlify serve`
-
-All tests except "doesn't fully buffer binary stream" pass which is automatically skipped in the CI. `netlify serve` doesn't seem to support streaming. It works fine when actually deployed, though.
-
-Build locally with `pnpm build:netlify-edge`, test with `netlify serve`.
-
-### Deno
-
-All tests pass.
-
-Build with `pnpm build:deno`, test with `deno run --allow-read --allow-net --allow-env dist/deno/index.js`.
-
----
-
-TODO: Tests below this line are currently run manually. Find a way to run them automatically.
-
----
+All environments that provide a local development server are tested automatically. But testing actual deployments is also desirable. Follow the instructions below to test deployments.
### Cloudflare Workers
-All tests pass.
-
Publish with `wrangler publish`.
### Vercel Serverless Functions
-All tests pass.
-
Build locally with `pnpm build:vercel` and deploy with `vercel deploy --prebuilt`.
### Vercel Edge Functions
-All tests pass.
-
Build locally with `pnpm build:vercel-edge` and deploy with `vercel deploy --prebuilt`.
### Netlify Functions (live)
-All tests except "doesn't fully buffer binary stream" pass. Netlify Functions have no streaming support.
-
Build locally with `pnpm build:netlify-functions`, deploy with `netlify deploy`.
### Netlify Edge Functions (live)
-All tests pass.
-
Build locally with `pnpm build:netlify-edge`, deploy with `netlify deploy`.
### Deno Deploy
-All tests pass.
-
Build with `pnpm build:deno`, `cd` into `dist/deno` and deploy with `deployctl deploy --token --project= index.js`.
-
-### Bun
-
-All tests pass.
-
-Run with `bun entry-bun.js`.
-
-### Lagon
-
-All tests except non-ASCII static file serving pass.