@@ -12,8 +12,9 @@ import type {
1212 NodeServerInstance ,
1313 Route ,
1414 Server ,
15+ WebSocketData ,
1516} from "./types" ;
16-
17+ import type { ServerWebSocket } from "bun" ;
1718/**
1819 * Runtime detection utilities for identifying the current JavaScript runtime environment.
1920 * @internal
@@ -615,7 +616,7 @@ export class Web<T extends Record<string, unknown> = Record<string, unknown>, B
615616 return response ;
616617 } ,
617618 ] ,
618- optionsId
619+ optionsId ,
619620 ) ;
620621 }
621622
@@ -1133,7 +1134,7 @@ export class Web<T extends Record<string, unknown> = Record<string, unknown>, B
11331134 } ) ;
11341135 }
11351136 return res ;
1136- } )
1137+ } ) ,
11371138 ) ;
11381139 return this ;
11391140 }
@@ -1154,7 +1155,7 @@ export class Web<T extends Record<string, unknown> = Record<string, unknown>, B
11541155 params : Record < string , string > ,
11551156 parsedUrl : { pathname : string ; searchParams ?: URLSearchParams } ,
11561157 clientIp ?: string ,
1157- env ?: B
1158+ env ?: B ,
11581159 ) : Context < T , B > {
11591160 // Initialize response headers storage
11601161 const responseHeaders = new Headers ( ) ;
@@ -1424,7 +1425,7 @@ export class Web<T extends Record<string, unknown> = Record<string, unknown>, B
14241425 headers : new Headers ( { "Content-Type" : "text/html; charset=utf-8" } ) ,
14251426 } ) ,
14261427 query : ( ) => parsedUrl . searchParams || EMPTY_SEARCH_PARAMS ,
1427- body : async ( ) => ( { } as any ) ,
1428+ body : async ( ) => ( { } ) as any ,
14281429 header : ( ) => { } ,
14291430 set : ( ) => { } ,
14301431 get : ( ) => undefined as any ,
@@ -1598,7 +1599,7 @@ export class Web<T extends Record<string, unknown> = Record<string, unknown>, B
15981599 headers : new Headers ( { "Content-Type" : "text/html; charset=utf-8" } ) ,
15991600 } ) ,
16001601 query : ( ) => parsedUrl . searchParams || EMPTY_SEARCH_PARAMS ,
1601- body : async ( ) => ( { } as any ) ,
1602+ body : async ( ) => ( { } ) as any ,
16021603 header : ( ) => { } ,
16031604 set : ( ) => { } ,
16041605 get : ( ) => undefined as any ,
@@ -1610,9 +1611,70 @@ export class Web<T extends Record<string, unknown> = Record<string, unknown>, B
16101611 }
16111612 }
16121613
1614+ /**
1615+ * Processes middleware for WebSocket upgrade requests to extract client IP and other context.
1616+ * This runs middleware (like ipExtract) before upgrading the connection.
1617+ *
1618+ * @param req - The incoming Request object
1619+ * @param directIp - The direct connection IP from Bun
1620+ * @returns Promise resolving to WebSocket data to attach during upgrade
1621+ * @private
1622+ */
1623+ private async processWebSocketUpgrade ( req : Request , directIp ?: string ) : Promise < WebSocketData > {
1624+ const method = "GET" as Method ; // WebSocket upgrades are always GET
1625+ const parsedUrl = this . parseUrl ( req . url ) ;
1626+ const path = parsedUrl . pathname ;
1627+
1628+ // Match route to get params
1629+ const matched = this . match ( method , path ) ;
1630+ const params = matched ?. params || { } ;
1631+
1632+ // Start with direct IP
1633+ let clientIp = directIp ;
1634+
1635+ // If we have middleware, run them to extract IP (like ipExtract)
1636+ if ( this . middlewares . length > 0 ) {
1637+ const methodMiddlewares = this . getMethodMiddlewares ( method ) ;
1638+
1639+ // Create context for middleware
1640+ const ctx = this . createContext ( req , params , parsedUrl , directIp ) ;
1641+
1642+ // Run middleware that matches this path
1643+ for ( const mw of methodMiddlewares ) {
1644+ if ( mw . pathPrefix && ! path . startsWith ( mw . pathPrefix ) ) {
1645+ continue ;
1646+ }
1647+
1648+ const matchResult = mw . match ( path ) ;
1649+ if ( matchResult . matched ) {
1650+ try {
1651+ // Run the middleware with a no-op next
1652+ await mw . handler ( ctx , async ( ) => { } ) ;
1653+
1654+ // Check if middleware set clientIp (like ipExtract does)
1655+ if ( ctx . clientIp ) {
1656+ clientIp = ctx . clientIp ;
1657+ }
1658+ } catch {
1659+ // Ignore middleware errors for WebSocket upgrade
1660+ }
1661+ }
1662+ }
1663+ }
1664+
1665+ return {
1666+ clientIp,
1667+ url : path ,
1668+ params,
1669+ query : parsedUrl . searchParams || EMPTY_SEARCH_PARAMS ,
1670+ } ;
1671+ }
1672+
16131673 /**
16141674 * Request handler optimized for Bun runtime with automatic IP extraction.
16151675 * Uses Bun's server request info for reliable client IP detection.
1676+ * For WebSocket upgrades, runs middleware (like ipExtract) before upgrading
1677+ * to ensure the real client IP is available via ws.data.clientIp.
16161678 *
16171679 * @param req - The incoming Request object
16181680 * @param server - Bun server instance
@@ -1629,21 +1691,23 @@ export class Web<T extends Record<string, unknown> = Record<string, unknown>, B
16291691 */
16301692 async handleBun ( req : Request , server : unknown ) : Promise < Response > {
16311693 // Extract client IP from Bun's server object
1632- const clientIp = ( server as any ) ?. requestIP ?.( req ) ?. address ;
1694+ const directIp = ( server as any ) ?. requestIP ?.( req ) ?. address ;
16331695
16341696 // Check if this is a WebSocket upgrade request
16351697 if ( this . bunWebSocket && req . headers . get ( "upgrade" ) === "websocket" ) {
1636- // Try to upgrade to WebSocket
1698+ // Process middleware to extract IP and other context data
1699+ const wsData = await this . processWebSocketUpgrade ( req , directIp ) ;
1700+
1701+ // Try to upgrade to WebSocket with the extracted data
16371702 const bunServer = server as BunServerInstance ;
1638- if ( bunServer . upgrade ( req ) ) {
1703+ if ( bunServer . upgrade ( req , { data : wsData } ) ) {
16391704 // Return empty response to indicate upgrade was handled
16401705 return new Response ( null , { status : 101 } ) ;
16411706 }
16421707 }
16431708
1644- return this . handleWithIp ( req , clientIp ) ;
1709+ return this . handleWithIp ( req , directIp ) ;
16451710 }
1646-
16471711 /**
16481712 * Request handler optimized for Deno runtime with automatic IP extraction.
16491713 * Uses Deno's ServeHandlerInfo for reliable client IP detection.
@@ -2085,42 +2149,86 @@ export class Web<T extends Record<string, unknown> = Record<string, unknown>, B
20852149 * Sets up WebSocket handlers for Bun runtime.
20862150 * This method must be called before starting the server with listen().
20872151 *
2152+ * The WebSocket handlers receive typed data via ws.data that includes:
2153+ * - clientIp: The real client IP extracted by middleware (like ipExtract)
2154+ * - url: The original request URL pathname
2155+ * - params: URL parameters extracted from the route
2156+ * - query: Query parameters from the upgrade request
2157+ *
20882158 * @param handlers - WebSocket handler configuration
20892159 * @returns The Web instance for method chaining
20902160 *
20912161 * @example
20922162 * ```typescript
2163+ * // Use ipExtract middleware to get real client IP behind proxies
2164+ * app.use(ipExtract("cloudflare"));
2165+ *
20932166 * app.websocket({
20942167 * idleTimeout: 120,
20952168 * maxPayloadLength: 1024 * 1024,
20962169 * open(ws) {
2097- * console.log('WebSocket connected');
2170+ * // Access the real client IP (extracted by ipExtract middleware)
2171+ * const clientIp = ws.data.clientIp;
2172+ * console.log(`Client connected from: ${clientIp}`);
2173+ *
2174+ * // Access URL params (e.g., from /ws/room/:roomId)
2175+ * const roomId = ws.data.params.roomId;
2176+ *
2177+ * // Access query params
2178+ * const token = ws.data.query.get('token');
2179+ *
20982180 * ws.subscribe('prices');
20992181 * },
21002182 * message(ws, message) {
2101- * console.log('Received:', message);
2183+ * console.log(`Message from ${ws.data.clientIp}: ${ message}` );
21022184 * },
21032185 * close(ws) {
21042186 * ws.unsubscribe('prices');
21052187 * }
21062188 * });
21072189 *
2108- * // Then use in a route
2109- * app.get('/ws', (ctx) => {
2190+ * // WebSocket route with parameters
2191+ * app.get('/ws/room/:roomId ', (ctx) => {
21102192 * if (ctx.req.headers.get('upgrade') === 'websocket') {
2111- * // The upgrade will be handled automatically by the framework
21122193 * return new Response(null, { status: 101 });
21132194 * }
21142195 * return ctx.text('WebSocket endpoint');
21152196 * });
21162197 * ```
21172198 */
2118- websocket ( handlers : BunWebSocketHandler ) : this {
2119- this . bunWebSocket = handlers ;
2199+ websocket < D extends Record < string , unknown > = Record < string , unknown > > ( handlers : BunWebSocketHandler < D > ) : this {
2200+ this . bunWebSocket = handlers as BunWebSocketHandler ;
21202201 return this ;
21212202 }
21222203}
21232204
2205+ /**
2206+ * Helper function to get the real client IP from a WebSocket connection.
2207+ * Use this instead of ws.remoteAddress to get the real client IP when behind proxies.
2208+ *
2209+ * @param ws - The WebSocket connection
2210+ * @returns The client IP address or undefined
2211+ *
2212+ * @example
2213+ * ```typescript
2214+ * import { getWebSocketClientIp } from "@rabbit-company/web";
2215+ *
2216+ * app.websocket({
2217+ * open(ws) {
2218+ * const ip = getWebSocketClientIp(ws);
2219+ * console.log(`Client connected from: ${ip}`);
2220+ * },
2221+ * message(ws, message) {
2222+ * const ip = getWebSocketClientIp(ws);
2223+ * console.log(`Message from ${ip}: ${message}`);
2224+ * }
2225+ * });
2226+ * ```
2227+ */
2228+ export function getWebSocketClientIp < D extends Record < string , unknown > = Record < string , unknown > > ( ws : ServerWebSocket < WebSocketData < D > > ) : string | undefined {
2229+ return ws . data ?. clientIp ;
2230+ }
2231+
21242232/**
21252233 * Extracts the static prefix from a path pattern by finding the longest initial segment
21262234 * that doesn't contain parameters (:) or wildcards (*). Used for quick middleware filtering.
0 commit comments