Skip to content

Commit f25af65

Browse files
authored
Merge pull request #14 from tinyhttp/fix/websocket-upgrade-head-buffer
fix: use server upgrade event to access head buffer (#12)
2 parents a097a0b + 6bee045 commit f25af65

File tree

11 files changed

+220
-93
lines changed

11 files changed

+220
-93
lines changed

.github/workflows/main.yml

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,22 @@
1-
# This is a basic workflow to help you get started with Actions
2-
31
name: CI
42

5-
# Controls when the action will run. Triggers the workflow on push or pull request
6-
# events but only for the master branch
73
on:
84
push:
9-
branches: [master]
105
pull_request:
11-
branches: [master]
126

13-
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
147
jobs:
15-
# This workflow contains a single job called "test"
168
test:
17-
# The type of runner that the job will run on
189
runs-on: ubuntu-latest
19-
20-
# Steps represent a sequence of tasks that will be executed as part of the job
2110
steps:
22-
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
2311
- uses: actions/checkout@v4
2412
- uses: oven-sh/setup-bun@v2
25-
with:
26-
bun-version: latest
13+
with:
14+
bun-version: latest
2715
- run: bun i
16+
- run: bun run lint
2817
- run: bun test --coverage
2918
- name: Coveralls
19+
if: github.ref == 'refs/heads/master'
3020
uses: coverallsapp/github-action@master
3121
with:
3222
github-token: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/release.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
8+
permissions:
9+
id-token: write
10+
contents: read
11+
12+
jobs:
13+
publish:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- uses: actions/setup-node@v4
19+
with:
20+
node-version: '22'
21+
registry-url: 'https://registry.npmjs.org'
22+
23+
- uses: oven-sh/setup-bun@v2
24+
with:
25+
bun-version: latest
26+
27+
- run: bun i
28+
- run: bun run build
29+
30+
- run: npm publish --provenance --access public
31+
env:
32+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

README.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,7 @@ pnpm i ws tinyws
4141
import { App, Request } from '@tinyhttp/app'
4242
import { tinyws, TinyWSRequest } from 'tinyws'
4343

44-
const app = new App<any, Request & TinyWSRequest>()
45-
46-
app.use(tinyws())
44+
const app = new App<Request & TinyWSRequest>()
4745

4846
app.use('/ws', async (req, res) => {
4947
if (req.ws) {
@@ -55,7 +53,20 @@ app.use('/ws', async (req, res) => {
5553
}
5654
})
5755

58-
app.listen(3000)
56+
const server = app.listen(3000)
57+
tinyws(app, server)
58+
```
59+
60+
### Restricting WebSocket to specific paths
61+
62+
You can restrict WebSocket handling to specific paths using the `paths` option:
63+
64+
```ts
65+
// Single path
66+
tinyws(app, server, { paths: '/ws' })
67+
68+
// Multiple paths
69+
tinyws(app, server, { paths: ['/ws', '/socket'] })
5970
```
6071

6172
See [examples](examples) for express and polka integration.

biome.json

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
{
22
"$schema": "https://biomejs.dev/schemas/1.8.2/schema.json",
33
"files": {
4-
"ignore": [
5-
"node_modules",
6-
"dist",
7-
"coverage",
8-
".pnpm-store"
9-
]
4+
"ignore": ["node_modules", "dist", "coverage", ".pnpm-store"]
105
},
116
"formatter": {
127
"enabled": true,

bun.lockb

4.85 KB
Binary file not shown.

examples/basic.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import { type TinyWSRequest, tinyws } from '../src/index'
44

55
const app = new App<Request & TinyWSRequest>()
66

7-
app.use(tinyws())
8-
97
app.use('/hmr', async (req, res) => {
108
if (req.ws) {
119
const ws = await req.ws()
@@ -15,4 +13,5 @@ app.use('/hmr', async (req, res) => {
1513
res.send('Hello from HTTP!')
1614
})
1715

18-
app.listen(3000)
16+
const server = app.listen(3000)
17+
tinyws(app, server)

examples/express.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ declare global {
1212

1313
const app = express()
1414

15-
app.use('/hmr', tinyws(), async (req, res) => {
15+
app.use('/hmr', async (req, res) => {
1616
if (req.ws) {
1717
const ws = await req.ws()
1818

@@ -21,4 +21,5 @@ app.use('/hmr', tinyws(), async (req, res) => {
2121
res.send('Hello from HTTP!')
2222
})
2323

24-
app.listen(3000)
24+
const server = app.listen(3000)
25+
tinyws({ handler: app }, server)

examples/polka.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import { type TinyWSRequest, tinyws } from '../src/index'
44

55
const app = polka<polka.Request & TinyWSRequest>()
66

7-
app.use(tinyws())
8-
97
app.use('/hmr', async (req, res) => {
108
if (req.ws) {
119
const ws = await req.ws()
@@ -15,4 +13,5 @@ app.use('/hmr', async (req, res) => {
1513
res.end('Hello from HTTP!')
1614
})
1715

18-
app.listen(3000)
16+
const server = app.listen(3000)
17+
tinyws({ handler: app.handler }, server)

package.json

Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
"name": "tinyws",
33
"version": "0.1.0",
44
"description": "Tiny WebSocket middleware for Node.js based on ws.",
5-
"files": [
6-
"dist"
7-
],
5+
"files": ["dist"],
86
"engines": {
97
"node": ">=12.4"
108
},
@@ -13,51 +11,39 @@
1311
"types": "dist/index.d.ts",
1412
"scripts": {
1513
"build": "tsc -p tsconfig.build.json",
16-
"test": "uvu -r tsm tests",
17-
"test:coverage": "c8 --include=src pnpm test",
18-
"test:report": "c8 report --reporter=text-lcov > coverage.lcov",
19-
"lint": "eslint \"./**/*.ts\"",
20-
"format": "prettier --write \"./**/*.ts\"",
21-
"prepublishOnly": "npm run test && npm run lint && npm run build"
14+
"test": "bun test",
15+
"test:coverage": "bun test --coverage",
16+
"lint": "biome check --write .",
17+
"prepublishOnly": "bun test && bun run lint && bun run build"
2218
},
2319
"repository": {
2420
"type": "git",
2521
"url": "git+https://github.com/talentlessguy/tinyws.git"
2622
},
27-
"keywords": [
28-
"ws",
29-
"express",
30-
"tinyhttp",
31-
"websocket",
32-
"middleware",
33-
"polka",
34-
"http",
35-
"server"
36-
],
23+
"keywords": ["ws", "express", "tinyhttp", "websocket", "middleware", "polka", "http", "server"],
3724
"author": "v1rtl (https://v1rtl.site)",
3825
"license": "MIT",
3926
"bugs": {
4027
"url": "https://github.com/talentlessguy/tinyws/issues"
4128
},
4229
"homepage": "https://github.com/talentlessguy/tinyws#readme",
4330
"devDependencies": {
44-
"@biomejs/biome": "^1.8.2",
45-
"@commitlint/cli": "^17.6.5",
46-
"@commitlint/config-conventional": "^17.6.5",
47-
"@tinyhttp/app": "^2.1.0",
48-
"@types/bun": "^1.1.5",
49-
"@types/express": "^4.17.17",
50-
"@types/node": "^18.16.18",
51-
"@types/ws": "^8.5.5",
31+
"@biomejs/biome": "^1.9.4",
32+
"@commitlint/cli": "^17.8.1",
33+
"@commitlint/config-conventional": "^17.8.1",
34+
"@tinyhttp/app": "^2.5.2",
35+
"@types/bun": "^1.3.8",
36+
"@types/express": "^4.17.25",
37+
"@types/node": "^18.19.130",
38+
"@types/ws": "^8.18.1",
5239
"c8": "7.12.0",
53-
"express": "^4.18.2",
40+
"express": "^4.22.1",
5441
"husky": "^8.0.3",
55-
"polka": "^1.0.0-next.25",
42+
"polka": "^1.0.0-next.28",
5643
"typescript": "^4.9.5",
57-
"ws": "^8.13.0"
44+
"ws": "^8.19.0"
5845
},
5946
"peerDependencies": {
6047
"ws": ">=8"
61-
},
62-
"packageManager": "pnpm@9.3.0+sha256.e1f9e8d1a16607a46dd3c158b5f7a7dc7945501d1c6222d454d63d033d1d918f"
48+
}
6349
}

src/index.ts

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,65 @@
1-
import type * as http from 'node:http'
1+
import * as http from 'node:http'
2+
import type { Socket } from 'node:net'
23
import type { ServerOptions, WebSocket } from 'ws'
3-
import { WebSocketServer as Server } from 'ws'
4+
import { WebSocketServer } from 'ws'
45

56
export interface TinyWSRequest extends http.IncomingMessage {
67
ws: () => Promise<WebSocket>
78
}
89

10+
export interface TinyWSOptions extends ServerOptions {
11+
paths?: string | string[]
12+
}
13+
914
/**
1015
* tinyws - adds `req.ws` method that resolves when websocket request appears
11-
* @param wsOptions
16+
* @param app - The application instance with a handler function
17+
* @param server - The HTTP server instance
18+
* @param options - Optional WebSocket server options and paths to restrict WebSocket handling
19+
* @param wss - Optional existing WebSocketServer instance
20+
* @returns The WebSocketServer instance
1221
*/
13-
export const tinyws =
14-
(wsOptions?: ServerOptions, wss: Server = new Server({ ...wsOptions, noServer: true })) =>
15-
async (req: TinyWSRequest, _: unknown, next: () => void | Promise<void>) => {
16-
const upgradeHeader = (req.headers.upgrade || '').split(',').map((s) => s.trim())
17-
18-
// When upgrade header contains "websocket" it's index is 0
19-
if (upgradeHeader.indexOf('websocket') === 0) {
20-
req.ws = () =>
22+
export const tinyws = (
23+
app: { handler: (req: any, res: any) => void },
24+
server: http.Server,
25+
options?: TinyWSOptions,
26+
wss: WebSocketServer = new WebSocketServer({ ...options, noServer: true })
27+
) => {
28+
const { paths, ...wsOptions } = options || {}
29+
const allowedPaths = paths ? (Array.isArray(paths) ? paths : [paths]) : null
30+
31+
const upgradeHandler = (request: http.IncomingMessage, socket: Socket, head: Buffer) => {
32+
const response = new http.ServerResponse(request)
33+
response.assignSocket(socket)
34+
35+
// Copy the head buffer to avoid keeping the entire slab buffer alive
36+
const copyOfHead = Buffer.alloc(head.length)
37+
head.copy(copyOfHead)
38+
39+
response.on('finish', () => {
40+
if (response.socket !== null) {
41+
response.socket.destroy()
42+
}
43+
})
44+
45+
const upgradeHeader = (request.headers.upgrade || '').split(',').map((s) => s.trim())
46+
const requestPath = request.url?.split('?')[0] || '/'
47+
48+
const pathMatches = allowedPaths === null || allowedPaths.some((p) => requestPath.startsWith(p))
49+
50+
if (upgradeHeader.indexOf('websocket') === 0 && pathMatches) {
51+
;(request as TinyWSRequest).ws = () =>
2152
new Promise((resolve) => {
22-
wss.handleUpgrade(req, req.socket, Buffer.alloc(0), (ws) => {
23-
wss.emit('connection', ws, req)
53+
wss.handleUpgrade(request, socket, copyOfHead, (ws) => {
54+
wss.emit('connection', ws, request)
2455
resolve(ws)
2556
})
2657
})
2758
}
2859

29-
await next()
60+
app.handler(request, response)
3061
}
62+
63+
server.on('upgrade', upgradeHandler)
64+
return wss
65+
}

0 commit comments

Comments
 (0)