Skip to content

Commit db2e7d7

Browse files
authored
fix(node:http) fix socket detach (#19052)
Co-authored-by: cirospaciari <[email protected]>
1 parent 47d2b00 commit db2e7d7

26 files changed

+483
-0
lines changed

src/js/node/http.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,6 +1140,9 @@ const ServerPrototype = {
11401140
http_res.detachSocket(socket);
11411141
return;
11421142
}
1143+
if (http_res.socket) {
1144+
http_res.on("finish", http_res.detachSocket.bind(http_res, socket));
1145+
}
11431146

11441147
const { reject, resolve, promise } = $newPromiseCapability(Promise);
11451148
resolveFunction = resolve;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.*
7+
.yarn/*
8+
!.yarn/patches
9+
!.yarn/plugins
10+
!.yarn/releases
11+
!.yarn/versions
12+
13+
# testing
14+
/coverage
15+
16+
# next.js
17+
/.next/
18+
/out/
19+
20+
# production
21+
/build
22+
23+
# misc
24+
.DS_Store
25+
*.pem
26+
27+
# debug
28+
npm-debug.log*
29+
yarn-debug.log*
30+
yarn-error.log*
31+
.pnpm-debug.log*
32+
33+
# env files (can opt-in for committing if needed)
34+
.env*
35+
36+
# vercel
37+
.vercel
38+
39+
# typescript
40+
*.tsbuildinfo
41+
next-env.d.ts
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { FlatCompat } from "@eslint/eslintrc";
2+
import { dirname } from "path";
3+
import { fileURLToPath } from "url";
4+
5+
const __filename = fileURLToPath(import.meta.url);
6+
const __dirname = dirname(__filename);
7+
8+
const compat = new FlatCompat({
9+
baseDirectory: __dirname,
10+
});
11+
12+
const eslintConfig = [...compat.extends("next/core-web-vitals", "next/typescript")];
13+
14+
export default eslintConfig;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { NextConfig } from "next";
2+
3+
const nextConfig: NextConfig = {
4+
/* config options here */
5+
eslint: {
6+
ignoreDuringBuilds: true,
7+
},
8+
};
9+
10+
export default nextConfig;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "next",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "next dev",
7+
"build": "next build",
8+
"start": "next start",
9+
"lint": "next lint"
10+
},
11+
"dependencies": {
12+
"next": "15.3.0",
13+
"next-auth": "5.0.0-beta.25",
14+
"react": "19.0.0",
15+
"react-dom": "19.0.0"
16+
},
17+
"devDependencies": {
18+
"@eslint/eslintrc": "^3",
19+
"@tailwindcss/postcss": "^4",
20+
"@types/node": "^20",
21+
"@types/react": "^19",
22+
"@types/react-dom": "^19",
23+
"eslint": "^9",
24+
"eslint-config-next": "15.3.0",
25+
"tailwindcss": "^4",
26+
"typescript": "^5"
27+
}
28+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const config = {
2+
plugins: ["@tailwindcss/postcss"],
3+
};
4+
5+
export default config;
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import net from "net";
2+
import { parse } from "url";
3+
import http from "http";
4+
import next from "next";
5+
import { expect } from "bun:test";
6+
function test(port) {
7+
const payload = Buffer.from(JSON.stringify({ message: "bun" }));
8+
9+
function sendRequest(socket) {
10+
const { promise, resolve } = Promise.withResolvers();
11+
let first = true;
12+
socket.on("data", data => {
13+
if (first) {
14+
first = false;
15+
const statusText = data.toString("utf8").split("HTTP/1.1")[1]?.split("\r\n")[0]?.trim();
16+
try {
17+
expect(statusText).toBe("200 OK");
18+
resolve();
19+
} catch (err) {
20+
console.error(err);
21+
process.exit(1);
22+
}
23+
}
24+
});
25+
socket.write(
26+
`POST /api/echo HTTP/1.1\r\nHost: localhost:8080\r\nConnection: keep-alive\r\nContent-Length: ${payload.byteLength}\r\n\r\n`,
27+
);
28+
socket.write(payload);
29+
30+
return promise;
31+
}
32+
const socket = net.connect({ port: port, host: "127.0.0.1" }, async () => {
33+
const timer = setTimeout(() => {
34+
console.error("timeout");
35+
process.exit(1);
36+
}, 30_000).unref();
37+
await sendRequest(socket);
38+
await sendRequest(socket);
39+
await sendRequest(socket);
40+
console.log("request sent");
41+
clearTimeout(timer);
42+
process.exit(0);
43+
});
44+
socket.on("error", err => {
45+
console.error(err);
46+
process.exit(1);
47+
});
48+
}
49+
50+
const app = next({ dev: true, dir: import.meta.dirname, quiet: true });
51+
const handle = app.getRequestHandler();
52+
53+
app.prepare().then(() => {
54+
const server = http
55+
.createServer((req, res) => {
56+
const parsedUrl = parse(req.url, true);
57+
handle(req, res, parsedUrl);
58+
})
59+
.listen(0, "127.0.0.1", () => {
60+
console.log("server listening", server.address().port);
61+
test(server.address().port);
62+
});
63+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { handlers } from "@/auth";
2+
3+
export const { GET, POST } = handlers;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { NextRequest } from 'next/server';
2+
3+
export async function POST(request: NextRequest) {
4+
try {
5+
const body = await request.json();
6+
return Response.json(body);
7+
} catch (error) {
8+
return Response.json({ error: 'Invalid JSON' }, { status: 400 });
9+
}
10+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import Link from 'next/link';
5+
6+
export default function EchoPage() {
7+
const [input, setInput] = useState('');
8+
const [response, setResponse] = useState<any>(null);
9+
const [loading, setLoading] = useState(false);
10+
11+
const handleSubmit = async (e: React.FormEvent) => {
12+
e.preventDefault();
13+
setLoading(true);
14+
15+
try {
16+
const res = await fetch('/api/echo', {
17+
method: 'POST',
18+
headers: {
19+
'Content-Type': 'application/json',
20+
},
21+
body: JSON.stringify({ message: input }),
22+
});
23+
24+
const data = await res.json();
25+
setResponse(data);
26+
} catch (error) {
27+
setResponse({ error: 'Failed to fetch' });
28+
} finally {
29+
setLoading(false);
30+
}
31+
};
32+
33+
return (
34+
<div className="flex min-h-screen flex-col items-center justify-center p-8">
35+
<main className="w-full max-w-md">
36+
<h1 className="text-2xl font-bold mb-6">Echo API Demo</h1>
37+
38+
<form onSubmit={handleSubmit} className="mb-6">
39+
<div className="flex flex-col gap-4">
40+
<input
41+
type="text"
42+
value={input}
43+
onChange={(e) => setInput(e.target.value)}
44+
placeholder="Enter a message"
45+
className="p-3 border rounded-md"
46+
/>
47+
<button
48+
type="submit"
49+
disabled={loading}
50+
className="p-3 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 disabled:opacity-50"
51+
>
52+
{loading ? 'Sending...' : 'Send'}
53+
</button>
54+
</div>
55+
</form>
56+
57+
{response && (
58+
<div className="w-full p-4 border rounded-md mb-6">
59+
<h2 className="text-lg font-bold mb-2">Response:</h2>
60+
<pre className="bg-gray-100 p-3 rounded overflow-auto">
61+
{JSON.stringify(response, null, 2)}
62+
</pre>
63+
</div>
64+
)}
65+
66+
<Link
67+
href="/"
68+
className="text-indigo-600 hover:underline"
69+
>
70+
← Back to home
71+
</Link>
72+
</main>
73+
</div>
74+
);
75+
}
Binary file not shown.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
@import "tailwindcss";
2+
3+
:root {
4+
--background: #ffffff;
5+
--foreground: #171717;
6+
}
7+
8+
@theme inline {
9+
--color-background: var(--background);
10+
--color-foreground: var(--foreground);
11+
--font-sans: var(--font-geist-sans);
12+
--font-mono: var(--font-geist-mono);
13+
}
14+
15+
@media (prefers-color-scheme: dark) {
16+
:root {
17+
--background: #0a0a0a;
18+
--foreground: #ededed;
19+
}
20+
}
21+
22+
body {
23+
background: var(--background);
24+
color: var(--foreground);
25+
font-family: Arial, Helvetica, sans-serif;
26+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { Metadata } from "next";
2+
import { Geist, Geist_Mono } from "next/font/google";
3+
import "./globals.css";
4+
5+
const geistSans = Geist({
6+
variable: "--font-geist-sans",
7+
subsets: ["latin"],
8+
});
9+
10+
const geistMono = Geist_Mono({
11+
variable: "--font-geist-mono",
12+
subsets: ["latin"],
13+
});
14+
15+
export const metadata: Metadata = {
16+
title: "Create Next App",
17+
description: "Generated by create next app",
18+
};
19+
20+
export default function RootLayout({
21+
children,
22+
}: Readonly<{
23+
children: React.ReactNode;
24+
}>) {
25+
return (
26+
<html lang="en">
27+
<body
28+
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
29+
>
30+
{children}
31+
</body>
32+
</html>
33+
);
34+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { auth } from "@/auth";
2+
3+
export default async function Home() {
4+
const session = await auth();
5+
6+
return (
7+
<div>Hello</div>
8+
);
9+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { auth, signOut } from "@/auth";
2+
3+
export default async function ProtectedPage() {
4+
const session = await auth();
5+
6+
return (
7+
<div >
8+
<form action={async () => {
9+
"use server";
10+
await signOut({ redirectTo: "/" });
11+
}}>
12+
<button
13+
type="submit"
14+
>
15+
Sign out
16+
</button>
17+
</form>
18+
</div>
19+
);
20+
}

0 commit comments

Comments
 (0)