Skip to content

Commit

Permalink
fix: bring uwebsockets adapter back to working order (#117)
Browse files Browse the repository at this point in the history
  • Loading branch information
cyco130 authored Jan 8, 2024
1 parent 6de83d6 commit 90138b2
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 129 deletions.
3 changes: 1 addition & 2 deletions packages/adapter/adapter-uwebsockets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
71 changes: 29 additions & 42 deletions packages/adapter/adapter-uwebsockets/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
});
},
Expand All @@ -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<Buffer | string>)[
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<Uint8Array>) {
if (lastChunk) {
res.cork(() => res.write(lastChunk!));
}
lastChunk = chunk;
}

if (lastChunk) {
res.cork(() => res.end(lastChunk!));
}
} else {
res.end();
}
}
});
Expand Down
2 changes: 0 additions & 2 deletions packages/middleware/static/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,6 @@ export function createStaticMiddleware<C extends MinimalRequestContext>(
return new Response(null, { status: 304 });
}

headers.set("content-length", file.size.toString());

if (file.etag) {
headers.set("etag", file.etag);
}
Expand Down
13 changes: 5 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 15 additions & 5 deletions testbed/basic/ci.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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;
Expand All @@ -323,7 +328,12 @@ describe.each(cases)(
hostName + "/"
}</span></p><p>Your IP address is: <span>${ip}</span></p>`;

expect(text).toContain(EXPECTED);
const EXPECTED_6 = `<h1>Hello from Hattip!</h1><p>URL: <span>${
hostName + "/"
}</span></p><p>Your IP address is: <span>${ip6}</span></p>`;

expect([EXPECTED, EXPECTED_6]).toContain(text);

expect(response.headers.get("content-type")).toEqual(
"text/html; charset=utf-8",
);
Expand Down
29 changes: 19 additions & 10 deletions testbed/basic/entry-uws.js
Original file line number Diff line number Diff line change
@@ -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");
},
);
62 changes: 2 additions & 60 deletions testbed/basic/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <TOKEN> --project=<PROJECT> index.js`.

### Bun

All tests pass.

Run with `bun entry-bun.js`.

### Lagon

All tests except non-ASCII static file serving pass.

0 comments on commit 90138b2

Please sign in to comment.