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.