[{"data":1,"prerenderedAt":1716},["ShallowReactive",2],{"navigation":3,"-guide-proxy":83,"-guide-proxy-surround":1713},[4,46],{"title":5,"path":6,"stem":7,"children":8,"icon":10},"Guide","\u002Fguide","1.guide\u002F1.index",[9,11,16,21,26,31,36,41],{"title":5,"path":6,"stem":7,"icon":10},"ph:book-open-duotone",{"title":12,"path":13,"stem":14,"icon":15},"Hooks","\u002Fguide\u002Fhooks","1.guide\u002F2.hooks","material-symbols-light:data-object",{"title":17,"path":18,"stem":19,"icon":20},"Peer","\u002Fguide\u002Fpeer","1.guide\u002F3.peer","mynaui:api",{"title":22,"path":23,"stem":24,"icon":25},"Message","\u002Fguide\u002Fmessage","1.guide\u002F4.message","solar:letter-line-duotone",{"title":27,"path":28,"stem":29,"icon":30},"Pub \u002F Sub","\u002Fguide\u002Fpubsub","1.guide\u002F5.pubsub","simple-icons:googlepubsub",{"title":32,"path":33,"stem":34,"icon":35},"Sync Backplane","\u002Fguide\u002Fsync","1.guide\u002F6.sync","tabler:refresh",{"title":37,"path":38,"stem":39,"icon":40},"Resolver API","\u002Fguide\u002Fresolver","1.guide\u002F7.resolver","tabler:route",{"title":42,"path":43,"stem":44,"icon":45},"WebSocket Proxy","\u002Fguide\u002Fproxy","1.guide\u002F8.proxy","tabler:arrows-exchange",{"title":47,"path":48,"stem":49,"children":50,"icon":52},"Adapters","\u002Fadapters","2.adapters\u002F1.index",[51,53,58,63,68,73,78],{"title":47,"path":48,"stem":49,"icon":52},"emojione-monotone:electric-plug",{"title":54,"path":55,"stem":56,"icon":57},"Bun","\u002Fadapters\u002Fbun","2.adapters\u002Fbun","simple-icons:bun",{"title":59,"path":60,"stem":61,"icon":62},"Bunny","\u002Fadapters\u002Fbunny","2.adapters\u002Fbunny","mdi:rabbit",{"title":64,"path":65,"stem":66,"icon":67},"Cloudflare","\u002Fadapters\u002Fcloudflare","2.adapters\u002Fcloudflare","devicon-plain:cloudflareworkers",{"title":69,"path":70,"stem":71,"icon":72},"Deno","\u002Fadapters\u002Fdeno","2.adapters\u002Fdeno","teenyicons:deno-solid",{"title":74,"path":75,"stem":76,"icon":77},"Node.js","\u002Fadapters\u002Fnode","2.adapters\u002Fnode","akar-icons:node-fill",{"title":79,"path":80,"stem":81,"icon":82},"SSE","\u002Fadapters\u002Fsse","2.adapters\u002Fsse","clarity:two-way-arrows-line",{"id":84,"title":42,"body":85,"description":1707,"extension":1708,"meta":1709,"navigation":1710,"path":43,"seo":1711,"stem":44,"__hash__":1712},"content\u002F1.guide\u002F8.proxy.md",{"type":86,"value":87,"toc":1692,"icon":45},"minimark",[88,92,120,125,135,240,243,249,267,271,279,472,498,502,510,625,646,681,778,782,789,824,833,838,848,1098,1114,1127,1131,1137,1216,1220,1238,1310,1314,1330,1456,1494,1498,1504,1513,1666,1688],[89,90,91],"p",{},"crossws ships a small helper that returns a set of ready-made hooks which proxy every peer to an upstream WebSocket server. Use it to put crossws in front of an existing backend, split traffic across services, or bridge protocols between runtimes.",[93,94,95],"note",{},[89,96,97,98,108,109,115,116,119],{},"\nThe proxy uses the global ",[99,100,104],"a",{"href":101,"rel":102},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FWebSocket",[103],"nofollow",[105,106,107],"code",{},"WebSocket"," constructor to dial the upstream, which is available on Node.js ≥ 22, Bun, Deno, Cloudflare Workers, and browsers. On older Node versions, pass a custom constructor via the ",[99,110,112,114],{"href":111},"#custom-websocket-constructor",[105,113,107],{}," option"," or install a polyfill on ",[105,117,118],{},"globalThis",".",[121,122,124],"h2",{"id":123},"usage","Usage",[89,126,127,130,131,134],{},[105,128,129],{},"createWebSocketProxy()"," returns a ",[105,132,133],{},"Partial\u003CHooks>"," object that you can pass straight to any crossws adapter.",[136,137,142],"pre",{"className":138,"code":139,"language":140,"meta":141,"style":141},"language-ts shiki shiki-themes github-light github-dark github-dark","\u002F\u002F https:\u002F\u002Fcrossws.h3.dev\u002Fadapters\nimport crossws from \"crossws\u002Fadapters\u002F\u003Cadapter>\";\nimport { createWebSocketProxy } from \"crossws\";\n\nconst websocket = crossws({\n  hooks: createWebSocketProxy(\"wss:\u002F\u002Fecho.websocket.org\"),\n});\n","ts","",[105,143,144,153,174,189,196,216,234],{"__ignoreMap":141},[145,146,149],"span",{"class":147,"line":148},"line",1,[145,150,152],{"class":151},"sCsY4","\u002F\u002F https:\u002F\u002Fcrossws.h3.dev\u002Fadapters\n",[145,154,156,160,164,167,171],{"class":147,"line":155},2,[145,157,159],{"class":158},"so5gQ","import",[145,161,163],{"class":162},"slsVL"," crossws ",[145,165,166],{"class":158},"from",[145,168,170],{"class":169},"sfrk1"," \"crossws\u002Fadapters\u002F\u003Cadapter>\"",[145,172,173],{"class":162},";\n",[145,175,177,179,182,184,187],{"class":147,"line":176},3,[145,178,159],{"class":158},[145,180,181],{"class":162}," { createWebSocketProxy } ",[145,183,166],{"class":158},[145,185,186],{"class":169}," \"crossws\"",[145,188,173],{"class":162},[145,190,192],{"class":147,"line":191},4,[145,193,195],{"emptyLinePlaceholder":194},true,"\n",[145,197,199,202,206,209,213],{"class":147,"line":198},5,[145,200,201],{"class":158},"const",[145,203,205],{"class":204},"suiK_"," websocket",[145,207,208],{"class":158}," =",[145,210,212],{"class":211},"shcOC"," crossws",[145,214,215],{"class":162},"({\n",[145,217,219,222,225,228,231],{"class":147,"line":218},6,[145,220,221],{"class":162},"  hooks: ",[145,223,224],{"class":211},"createWebSocketProxy",[145,226,227],{"class":162},"(",[145,229,230],{"class":169},"\"wss:\u002F\u002Fecho.websocket.org\"",[145,232,233],{"class":162},"),\n",[145,235,237],{"class":147,"line":236},7,[145,238,239],{"class":162},"});\n",[89,241,242],{},"Every incoming peer opens a matching upstream connection. Text and binary messages are forwarded in both directions, and close\u002Ferror events are propagated to the client.",[244,245,246],"tip",{},[89,247,248],{},"\nMessages sent by the client before the upstream connection is ready are buffered and flushed as soon as the upstream is open.",[250,251,252],"caution",{},[89,253,254,258,259,266],{},[255,256,257],"strong",{},"The default proxy is an open relay."," It accepts every incoming connection and forwards it to the configured upstream without any authorization check. Always combine it with an ",[99,260,262,265],{"href":261},"#authentication",[105,263,264],{},"upgrade"," hook"," when the upstream is not itself publicly accessible — otherwise anyone who can reach the proxy can reach the upstream.",[121,268,270],{"id":269},"authentication","Authentication",[89,272,273,275,276,278],{},[105,274,129],{}," returns a plain hooks object, so you can spread it and override individual hooks. Authenticate the upgrade request before proxying by wrapping the proxy's ",[105,277,264],{}," hook:",[136,280,282],{"className":138,"code":281,"language":140,"meta":141,"style":141},"import { createWebSocketProxy } from \"crossws\";\n\nconst proxyHooks = createWebSocketProxy(\"wss:\u002F\u002Fbackend.example.com\");\n\nconst hooks = {\n  ...proxyHooks,\n  async upgrade(req) {\n    const token = req.headers.get(\"authorization\");\n    if (!(await isValidToken(token))) {\n      return new Response(\"Unauthorized\", { status: 401 });\n    }\n    \u002F\u002F Delegate to the proxy's own `upgrade` so subprotocol echoing still works.\n    return proxyHooks.upgrade?.(req);\n  },\n};\n",[105,283,284,296,300,320,324,336,344,361,385,408,434,440,446,460,466],{"__ignoreMap":141},[145,285,286,288,290,292,294],{"class":147,"line":148},[145,287,159],{"class":158},[145,289,181],{"class":162},[145,291,166],{"class":158},[145,293,186],{"class":169},[145,295,173],{"class":162},[145,297,298],{"class":147,"line":155},[145,299,195],{"emptyLinePlaceholder":194},[145,301,302,304,307,309,312,314,317],{"class":147,"line":176},[145,303,201],{"class":158},[145,305,306],{"class":204}," proxyHooks",[145,308,208],{"class":158},[145,310,311],{"class":211}," createWebSocketProxy",[145,313,227],{"class":162},[145,315,316],{"class":169},"\"wss:\u002F\u002Fbackend.example.com\"",[145,318,319],{"class":162},");\n",[145,321,322],{"class":147,"line":191},[145,323,195],{"emptyLinePlaceholder":194},[145,325,326,328,331,333],{"class":147,"line":198},[145,327,201],{"class":158},[145,329,330],{"class":204}," hooks",[145,332,208],{"class":158},[145,334,335],{"class":162}," {\n",[145,337,338,341],{"class":147,"line":218},[145,339,340],{"class":158},"  ...",[145,342,343],{"class":162},"proxyHooks,\n",[145,345,346,349,352,354,358],{"class":147,"line":236},[145,347,348],{"class":158},"  async",[145,350,351],{"class":211}," upgrade",[145,353,227],{"class":162},[145,355,357],{"class":356},"sQHwn","req",[145,359,360],{"class":162},") {\n",[145,362,364,367,370,372,375,378,380,383],{"class":147,"line":363},8,[145,365,366],{"class":158},"    const",[145,368,369],{"class":204}," token",[145,371,208],{"class":158},[145,373,374],{"class":162}," req.headers.",[145,376,377],{"class":211},"get",[145,379,227],{"class":162},[145,381,382],{"class":169},"\"authorization\"",[145,384,319],{"class":162},[145,386,388,391,394,397,399,402,405],{"class":147,"line":387},9,[145,389,390],{"class":158},"    if",[145,392,393],{"class":162}," (",[145,395,396],{"class":158},"!",[145,398,227],{"class":162},[145,400,401],{"class":158},"await",[145,403,404],{"class":211}," isValidToken",[145,406,407],{"class":162},"(token))) {\n",[145,409,411,414,417,420,422,425,428,431],{"class":147,"line":410},10,[145,412,413],{"class":158},"      return",[145,415,416],{"class":158}," new",[145,418,419],{"class":211}," Response",[145,421,227],{"class":162},[145,423,424],{"class":169},"\"Unauthorized\"",[145,426,427],{"class":162},", { status: ",[145,429,430],{"class":204},"401",[145,432,433],{"class":162}," });\n",[145,435,437],{"class":147,"line":436},11,[145,438,439],{"class":162},"    }\n",[145,441,443],{"class":147,"line":442},12,[145,444,445],{"class":151},"    \u002F\u002F Delegate to the proxy's own `upgrade` so subprotocol echoing still works.\n",[145,447,449,452,455,457],{"class":147,"line":448},13,[145,450,451],{"class":158},"    return",[145,453,454],{"class":162}," proxyHooks.",[145,456,264],{"class":211},[145,458,459],{"class":162},"?.(req);\n",[145,461,463],{"class":147,"line":462},14,[145,464,465],{"class":162},"  },\n",[145,467,469],{"class":147,"line":468},15,[145,470,471],{"class":162},"};\n",[93,473,474],{},[89,475,476,477,479,480,483,484,487,488,490,491,497],{},"\nThe WHATWG ",[105,478,107],{}," constructor cannot forward cookies, ",[105,481,482],{},"Authorization",", or ",[105,485,486],{},"Origin"," to the upstream, so upstream identity checks relying on those headers will silently fail. Authenticate at the proxy, or pass a custom ",[105,489,107],{}," client and use the ",[99,492,494,114],{"href":493},"#forwarding-headers",[105,495,496],{},"headers"," to propagate identity.",[121,499,501],{"id":500},"dynamic-target","Dynamic target",[89,503,504,505,509],{},"Pass a function to resolve the upstream URL from the incoming ",[99,506,507],{"href":18},[105,508,17],{}," — useful for routing based on request URL, headers, or authenticated context.",[136,511,513],{"className":138,"code":512,"language":140,"meta":141,"style":141},"import { createWebSocketProxy } from \"crossws\";\n\nconst hooks = createWebSocketProxy({\n  target: (peer) => {\n    const { pathname } = new URL(peer.request.url);\n    return pathname.startsWith(\"\u002Fadmin\") ? \"wss:\u002F\u002Fadmin.internal\u002Fws\" : \"wss:\u002F\u002Fpublic.internal\u002Fws\";\n  },\n});\n",[105,514,515,527,531,543,562,586,617,621],{"__ignoreMap":141},[145,516,517,519,521,523,525],{"class":147,"line":148},[145,518,159],{"class":158},[145,520,181],{"class":162},[145,522,166],{"class":158},[145,524,186],{"class":169},[145,526,173],{"class":162},[145,528,529],{"class":147,"line":155},[145,530,195],{"emptyLinePlaceholder":194},[145,532,533,535,537,539,541],{"class":147,"line":176},[145,534,201],{"class":158},[145,536,330],{"class":204},[145,538,208],{"class":158},[145,540,311],{"class":211},[145,542,215],{"class":162},[145,544,545,548,551,554,557,560],{"class":147,"line":191},[145,546,547],{"class":211},"  target",[145,549,550],{"class":162},": (",[145,552,553],{"class":356},"peer",[145,555,556],{"class":162},") ",[145,558,559],{"class":158},"=>",[145,561,335],{"class":162},[145,563,564,566,569,572,575,578,580,583],{"class":147,"line":198},[145,565,366],{"class":158},[145,567,568],{"class":162}," { ",[145,570,571],{"class":204},"pathname",[145,573,574],{"class":162}," } ",[145,576,577],{"class":158},"=",[145,579,416],{"class":158},[145,581,582],{"class":211}," URL",[145,584,585],{"class":162},"(peer.request.url);\n",[145,587,588,590,593,596,598,601,603,606,609,612,615],{"class":147,"line":218},[145,589,451],{"class":158},[145,591,592],{"class":162}," pathname.",[145,594,595],{"class":211},"startsWith",[145,597,227],{"class":162},[145,599,600],{"class":169},"\"\u002Fadmin\"",[145,602,556],{"class":162},[145,604,605],{"class":158},"?",[145,607,608],{"class":169}," \"wss:\u002F\u002Fadmin.internal\u002Fws\"",[145,610,611],{"class":158}," :",[145,613,614],{"class":169}," \"wss:\u002F\u002Fpublic.internal\u002Fws\"",[145,616,173],{"class":162},[145,618,619],{"class":147,"line":236},[145,620,465],{"class":162},[145,622,623],{"class":147,"line":363},[145,624,239],{"class":162},[626,627,628],"warning",{},[89,629,630,633,634,637,638,641,642,645],{},[255,631,632],{},"SSRF risk."," A dynamic ",[105,635,636],{},"target"," resolver is a trust boundary. Never interpolate untrusted input (query strings, headers, path segments a client controls) directly into the returned URL — a naive resolver turns the proxy into an SSRF primitive that can dial ",[105,639,640],{},"ws:\u002F\u002F127.0.0.1",", ",[105,643,644],{},"ws:\u002F\u002F169.254.169.254",", or any reachable internal service. Always resolve against a hard-coded allowlist of hosts you control.",[89,647,648,649,652,653,659,660,665,666,669,670,673,674,676,677,680],{},"The resolver may be ",[255,650,651],{},"async"," — return a promise when the upstream address isn't known yet at connect time (a worker that's still booting, a backend being hot-reloaded, a lookup against a registry). The proxy buffers any client frames that arrive while it resolves (bounded by ",[99,654,656],{"href":655},"#api",[105,657,658],{},"maxBufferSize",") and a non-zero ",[99,661,662],{"href":655},[105,663,664],{},"connectTimeout"," also covers the resolution, so a resolver that never settles closes the peer with ",[105,667,668],{},"1011"," instead of hanging. With ",[105,671,672],{},"connectTimeout: 0"," (timeout disabled) a never-settling resolver instead keeps the peer open until ",[105,675,658],{}," is reached (",[105,678,679],{},"1009","), so give your resolver its own deadline in that case.",[136,682,684],{"className":138,"code":683,"language":140,"meta":141,"style":141},"const hooks = createWebSocketProxy({\n  \u002F\u002F Wait for the upstream worker to report its address before proxying.\n  target: async () => {\n    const addr = await worker.waitForAddress();\n    return `ws:\u002F\u002F${addr.host}:${addr.port}\u002F`;\n  },\n});\n",[105,685,686,698,703,719,740,770,774],{"__ignoreMap":141},[145,687,688,690,692,694,696],{"class":147,"line":148},[145,689,201],{"class":158},[145,691,330],{"class":204},[145,693,208],{"class":158},[145,695,311],{"class":211},[145,697,215],{"class":162},[145,699,700],{"class":147,"line":155},[145,701,702],{"class":151},"  \u002F\u002F Wait for the upstream worker to report its address before proxying.\n",[145,704,705,707,710,712,715,717],{"class":147,"line":176},[145,706,547],{"class":211},[145,708,709],{"class":162},": ",[145,711,651],{"class":158},[145,713,714],{"class":162}," () ",[145,716,559],{"class":158},[145,718,335],{"class":162},[145,720,721,723,726,728,731,734,737],{"class":147,"line":191},[145,722,366],{"class":158},[145,724,725],{"class":204}," addr",[145,727,208],{"class":158},[145,729,730],{"class":158}," await",[145,732,733],{"class":162}," worker.",[145,735,736],{"class":211},"waitForAddress",[145,738,739],{"class":162},"();\n",[145,741,742,744,747,750,752,755,758,760,762,765,768],{"class":147,"line":198},[145,743,451],{"class":158},[145,745,746],{"class":169}," `ws:\u002F\u002F${",[145,748,749],{"class":162},"addr",[145,751,119],{"class":169},[145,753,754],{"class":162},"host",[145,756,757],{"class":169},"}:${",[145,759,749],{"class":162},[145,761,119],{"class":169},[145,763,764],{"class":162},"port",[145,766,767],{"class":169},"}\u002F`",[145,769,173],{"class":162},[145,771,772],{"class":147,"line":218},[145,773,465],{"class":162},[145,775,776],{"class":147,"line":236},[145,777,239],{"class":162},[121,779,781],{"id":780},"subprotocol-negotiation","Subprotocol negotiation",[89,783,784,785,788],{},"By default, the proxy forwards the client's ",[105,786,787],{},"sec-websocket-protocol"," header to the upstream and echoes the first requested subprotocol back in the upgrade response so the client handshake succeeds. Disable this if you want to negotiate subprotocols yourself:",[136,790,792],{"className":138,"code":791,"language":140,"meta":141,"style":141},"createWebSocketProxy({\n  target: \"wss:\u002F\u002Fbackend.example.com\",\n  forwardProtocol: false,\n});\n",[105,793,794,800,810,820],{"__ignoreMap":141},[145,795,796,798],{"class":147,"line":148},[145,797,224],{"class":211},[145,799,215],{"class":162},[145,801,802,805,807],{"class":147,"line":155},[145,803,804],{"class":162},"  target: ",[145,806,316],{"class":169},[145,808,809],{"class":162},",\n",[145,811,812,815,818],{"class":147,"line":176},[145,813,814],{"class":162},"  forwardProtocol: ",[145,816,817],{"class":204},"false",[145,819,809],{"class":162},[145,821,822],{"class":147,"line":191},[145,823,239],{"class":162},[626,825,826],{},[89,827,828,829,832],{},"\nThe proxy commits to a subprotocol in the upgrade response before the upstream connection is established. If the upstream ultimately picks a different subprotocol (or rejects), the client will still see the one the proxy promised. Only keep ",[105,830,831],{},"forwardProtocol"," enabled when the upstream is known to accept the same subprotocols the client negotiates.",[834,835,837],"h3",{"id":836},"rewriting-the-upstream-subprotocol","Rewriting the upstream subprotocol",[89,839,840,842,843,847],{},[105,841,831],{}," can present a ",[844,845,846],"em",{},"different"," subprotocol to the upstream than the client offered — useful when a client re-labels a token to pass an intermediary and the proxy must restore the real token when dialing the origin. Pick the simplest form that fits:",[136,849,851],{"className":138,"code":850,"language":140,"meta":141,"style":141},"\u002F\u002F 1. Fixed value — the upstream always expects one known subprotocol.\ncreateWebSocketProxy({\n  target: \"wss:\u002F\u002Fbackend.example.com\",\n  forwardProtocol: \"vite-hmr\",\n});\n\n\u002F\u002F 2. Rewrite map — swap specific client tokens, pass the rest through.\ncreateWebSocketProxy({\n  target: \"wss:\u002F\u002Fbackend.example.com\",\n  forwardProtocol: { \"proxied-vite-hmr\": \"vite-hmr\" },\n});\n\n\u002F\u002F 3. Function — when the rewrite depends on more than the token value.\ncreateWebSocketProxy({\n  target: \"wss:\u002F\u002Fbackend.example.com\",\n  forwardProtocol: (peer) =>\n    (peer.request.headers.get(\"sec-websocket-protocol\") ?? \"\")\n      .split(\",\")\n      .map((p) => p.trim())\n      .filter(Boolean)\n      .map((p) => (p.startsWith(\"proxied-\") ? p.slice(\"proxied-\".length) : p)),\n});\n",[105,852,853,858,864,872,881,885,889,894,900,908,923,927,931,936,942,950,965,989,1005,1031,1042,1093],{"__ignoreMap":141},[145,854,855],{"class":147,"line":148},[145,856,857],{"class":151},"\u002F\u002F 1. Fixed value — the upstream always expects one known subprotocol.\n",[145,859,860,862],{"class":147,"line":155},[145,861,224],{"class":211},[145,863,215],{"class":162},[145,865,866,868,870],{"class":147,"line":176},[145,867,804],{"class":162},[145,869,316],{"class":169},[145,871,809],{"class":162},[145,873,874,876,879],{"class":147,"line":191},[145,875,814],{"class":162},[145,877,878],{"class":169},"\"vite-hmr\"",[145,880,809],{"class":162},[145,882,883],{"class":147,"line":198},[145,884,239],{"class":162},[145,886,887],{"class":147,"line":218},[145,888,195],{"emptyLinePlaceholder":194},[145,890,891],{"class":147,"line":236},[145,892,893],{"class":151},"\u002F\u002F 2. Rewrite map — swap specific client tokens, pass the rest through.\n",[145,895,896,898],{"class":147,"line":363},[145,897,224],{"class":211},[145,899,215],{"class":162},[145,901,902,904,906],{"class":147,"line":387},[145,903,804],{"class":162},[145,905,316],{"class":169},[145,907,809],{"class":162},[145,909,910,913,916,918,920],{"class":147,"line":410},[145,911,912],{"class":162},"  forwardProtocol: { ",[145,914,915],{"class":169},"\"proxied-vite-hmr\"",[145,917,709],{"class":162},[145,919,878],{"class":169},[145,921,922],{"class":162}," },\n",[145,924,925],{"class":147,"line":436},[145,926,239],{"class":162},[145,928,929],{"class":147,"line":442},[145,930,195],{"emptyLinePlaceholder":194},[145,932,933],{"class":147,"line":448},[145,934,935],{"class":151},"\u002F\u002F 3. Function — when the rewrite depends on more than the token value.\n",[145,937,938,940],{"class":147,"line":462},[145,939,224],{"class":211},[145,941,215],{"class":162},[145,943,944,946,948],{"class":147,"line":468},[145,945,804],{"class":162},[145,947,316],{"class":169},[145,949,809],{"class":162},[145,951,953,956,958,960,962],{"class":147,"line":952},16,[145,954,955],{"class":211},"  forwardProtocol",[145,957,550],{"class":162},[145,959,553],{"class":356},[145,961,556],{"class":162},[145,963,964],{"class":158},"=>\n",[145,966,968,971,973,975,978,980,983,986],{"class":147,"line":967},17,[145,969,970],{"class":162},"    (peer.request.headers.",[145,972,377],{"class":211},[145,974,227],{"class":162},[145,976,977],{"class":169},"\"sec-websocket-protocol\"",[145,979,556],{"class":162},[145,981,982],{"class":158},"??",[145,984,985],{"class":169}," \"\"",[145,987,988],{"class":162},")\n",[145,990,992,995,998,1000,1003],{"class":147,"line":991},18,[145,993,994],{"class":162},"      .",[145,996,997],{"class":211},"split",[145,999,227],{"class":162},[145,1001,1002],{"class":169},"\",\"",[145,1004,988],{"class":162},[145,1006,1008,1010,1013,1016,1018,1020,1022,1025,1028],{"class":147,"line":1007},19,[145,1009,994],{"class":162},[145,1011,1012],{"class":211},"map",[145,1014,1015],{"class":162},"((",[145,1017,89],{"class":356},[145,1019,556],{"class":162},[145,1021,559],{"class":158},[145,1023,1024],{"class":162}," p.",[145,1026,1027],{"class":211},"trim",[145,1029,1030],{"class":162},"())\n",[145,1032,1034,1036,1039],{"class":147,"line":1033},20,[145,1035,994],{"class":162},[145,1037,1038],{"class":211},"filter",[145,1040,1041],{"class":162},"(Boolean)\n",[145,1043,1045,1047,1049,1051,1053,1055,1057,1060,1062,1064,1067,1069,1071,1073,1076,1078,1080,1082,1085,1087,1090],{"class":147,"line":1044},21,[145,1046,994],{"class":162},[145,1048,1012],{"class":211},[145,1050,1015],{"class":162},[145,1052,89],{"class":356},[145,1054,556],{"class":162},[145,1056,559],{"class":158},[145,1058,1059],{"class":162}," (p.",[145,1061,595],{"class":211},[145,1063,227],{"class":162},[145,1065,1066],{"class":169},"\"proxied-\"",[145,1068,556],{"class":162},[145,1070,605],{"class":158},[145,1072,1024],{"class":162},[145,1074,1075],{"class":211},"slice",[145,1077,227],{"class":162},[145,1079,1066],{"class":169},[145,1081,119],{"class":162},[145,1083,1084],{"class":204},"length",[145,1086,556],{"class":162},[145,1088,1089],{"class":158},":",[145,1091,1092],{"class":162}," p)),\n",[145,1094,1096],{"class":147,"line":1095},22,[145,1097,239],{"class":162},[89,1099,1100,1101,1105,1106,1109,1110,1113],{},"The function receives the ",[99,1102,1103],{"href":18},[105,1104,17],{}," and returns a ",[105,1107,1108],{},"string",", an array of strings, or ",[105,1111,1112],{},"undefined"," to offer none.",[93,1115,1116],{},[89,1117,1118,1119,1122,1123,1126],{},"\nThese forms control only what is offered to the ",[255,1120,1121],{},"upstream",". The subprotocol echoed back to the ",[255,1124,1125],{},"client"," stays the first token the client offered — per RFC 6455 the selected subprotocol must be one the client proposed, so echoing a rewritten value (which the client may not have offered) would make browsers fail the connection.",[121,1128,1130],{"id":1129},"custom-websocket-constructor","Custom WebSocket constructor",[89,1132,1133,1134,1136],{},"Pass a ",[105,1135,107],{}," constructor via options to override the global — useful on Node.js \u003C 22, to plug in a different client implementation, or to stub the upstream in tests.",[136,1138,1140],{"className":138,"code":1139,"language":140,"meta":141,"style":141},"import { WebSocket } from \"ws\";\nimport { createWebSocketProxy } from \"crossws\";\n\nconst hooks = createWebSocketProxy({\n  target: \"wss:\u002F\u002Fbackend.example.com\",\n  WebSocket: WebSocket as unknown as typeof globalThis.WebSocket,\n});\n",[105,1141,1142,1156,1168,1172,1184,1192,1212],{"__ignoreMap":141},[145,1143,1144,1146,1149,1151,1154],{"class":147,"line":148},[145,1145,159],{"class":158},[145,1147,1148],{"class":162}," { WebSocket } ",[145,1150,166],{"class":158},[145,1152,1153],{"class":169}," \"ws\"",[145,1155,173],{"class":162},[145,1157,1158,1160,1162,1164,1166],{"class":147,"line":155},[145,1159,159],{"class":158},[145,1161,181],{"class":162},[145,1163,166],{"class":158},[145,1165,186],{"class":169},[145,1167,173],{"class":162},[145,1169,1170],{"class":147,"line":176},[145,1171,195],{"emptyLinePlaceholder":194},[145,1173,1174,1176,1178,1180,1182],{"class":147,"line":191},[145,1175,201],{"class":158},[145,1177,330],{"class":204},[145,1179,208],{"class":158},[145,1181,311],{"class":211},[145,1183,215],{"class":162},[145,1185,1186,1188,1190],{"class":147,"line":198},[145,1187,804],{"class":162},[145,1189,316],{"class":169},[145,1191,809],{"class":162},[145,1193,1194,1197,1200,1203,1206,1209],{"class":147,"line":218},[145,1195,1196],{"class":162},"  WebSocket: WebSocket ",[145,1198,1199],{"class":158},"as",[145,1201,1202],{"class":204}," unknown",[145,1204,1205],{"class":158}," as",[145,1207,1208],{"class":158}," typeof",[145,1210,1211],{"class":162}," globalThis.WebSocket,\n",[145,1213,1214],{"class":147,"line":236},[145,1215,239],{"class":162},[834,1217,1219],{"id":1218},"unix-domain-sockets","Unix domain sockets",[89,1221,1222,1223,1225,1226,1233,1234,1237],{},"The proxy does not enforce any scheme allowlist — whatever the configured ",[105,1224,107],{}," constructor accepts is accepted. For example, the ",[99,1227,1230],{"href":1228,"rel":1229},"https:\u002F\u002Fgithub.com\u002Fwebsockets\u002Fws",[103],[105,1231,1232],{},"ws"," package supports Unix domain sockets via its ",[105,1235,1236],{},"ws+unix:"," scheme:",[136,1239,1241],{"className":138,"code":1240,"language":140,"meta":141,"style":141},"import { WebSocket } from \"ws\";\nimport { createWebSocketProxy } from \"crossws\";\n\nconst hooks = createWebSocketProxy({\n  target: \"ws+unix:\u002Fvar\u002Frun\u002Fbackend.sock:\u002Fchat\",\n  WebSocket: WebSocket as unknown as typeof globalThis.WebSocket,\n});\n",[105,1242,1243,1255,1267,1271,1283,1292,1306],{"__ignoreMap":141},[145,1244,1245,1247,1249,1251,1253],{"class":147,"line":148},[145,1246,159],{"class":158},[145,1248,1148],{"class":162},[145,1250,166],{"class":158},[145,1252,1153],{"class":169},[145,1254,173],{"class":162},[145,1256,1257,1259,1261,1263,1265],{"class":147,"line":155},[145,1258,159],{"class":158},[145,1260,181],{"class":162},[145,1262,166],{"class":158},[145,1264,186],{"class":169},[145,1266,173],{"class":162},[145,1268,1269],{"class":147,"line":176},[145,1270,195],{"emptyLinePlaceholder":194},[145,1272,1273,1275,1277,1279,1281],{"class":147,"line":191},[145,1274,201],{"class":158},[145,1276,330],{"class":204},[145,1278,208],{"class":158},[145,1280,311],{"class":211},[145,1282,215],{"class":162},[145,1284,1285,1287,1290],{"class":147,"line":198},[145,1286,804],{"class":162},[145,1288,1289],{"class":169},"\"ws+unix:\u002Fvar\u002Frun\u002Fbackend.sock:\u002Fchat\"",[145,1291,809],{"class":162},[145,1293,1294,1296,1298,1300,1302,1304],{"class":147,"line":218},[145,1295,1196],{"class":162},[145,1297,1199],{"class":158},[145,1299,1202],{"class":204},[145,1301,1205],{"class":158},[145,1303,1208],{"class":158},[145,1305,1211],{"class":162},[145,1307,1308],{"class":147,"line":236},[145,1309,239],{"class":162},[121,1311,1313],{"id":1312},"forwarding-headers","Forwarding headers",[89,1315,1316,1317,1319,1320,641,1323,641,1326,1329],{},"Passing a ",[105,1318,496],{}," option attaches extra headers to the upstream handshake. This is the usual way to forward identity (",[105,1321,1322],{},"cookie",[105,1324,1325],{},"authorization",[105,1327,1328],{},"origin",") or inject a shared secret to the upstream.",[136,1331,1333],{"className":138,"code":1332,"language":140,"meta":141,"style":141},"import { WebSocket } from \"ws\";\nimport { createWebSocketProxy } from \"crossws\";\n\nconst hooks = createWebSocketProxy({\n  target: \"wss:\u002F\u002Fbackend.example.com\",\n  WebSocket: WebSocket as unknown as typeof globalThis.WebSocket,\n  headers: (peer) => ({\n    cookie: peer.request.headers.get(\"cookie\") ?? \"\",\n    \"x-forwarded-for\": peer.remoteAddress ?? \"\",\n  }),\n});\n",[105,1334,1335,1347,1359,1363,1375,1383,1397,1413,1433,1447,1452],{"__ignoreMap":141},[145,1336,1337,1339,1341,1343,1345],{"class":147,"line":148},[145,1338,159],{"class":158},[145,1340,1148],{"class":162},[145,1342,166],{"class":158},[145,1344,1153],{"class":169},[145,1346,173],{"class":162},[145,1348,1349,1351,1353,1355,1357],{"class":147,"line":155},[145,1350,159],{"class":158},[145,1352,181],{"class":162},[145,1354,166],{"class":158},[145,1356,186],{"class":169},[145,1358,173],{"class":162},[145,1360,1361],{"class":147,"line":176},[145,1362,195],{"emptyLinePlaceholder":194},[145,1364,1365,1367,1369,1371,1373],{"class":147,"line":191},[145,1366,201],{"class":158},[145,1368,330],{"class":204},[145,1370,208],{"class":158},[145,1372,311],{"class":211},[145,1374,215],{"class":162},[145,1376,1377,1379,1381],{"class":147,"line":198},[145,1378,804],{"class":162},[145,1380,316],{"class":169},[145,1382,809],{"class":162},[145,1384,1385,1387,1389,1391,1393,1395],{"class":147,"line":218},[145,1386,1196],{"class":162},[145,1388,1199],{"class":158},[145,1390,1202],{"class":204},[145,1392,1205],{"class":158},[145,1394,1208],{"class":158},[145,1396,1211],{"class":162},[145,1398,1399,1402,1404,1406,1408,1410],{"class":147,"line":236},[145,1400,1401],{"class":211},"  headers",[145,1403,550],{"class":162},[145,1405,553],{"class":356},[145,1407,556],{"class":162},[145,1409,559],{"class":158},[145,1411,1412],{"class":162}," ({\n",[145,1414,1415,1418,1420,1422,1425,1427,1429,1431],{"class":147,"line":363},[145,1416,1417],{"class":162},"    cookie: peer.request.headers.",[145,1419,377],{"class":211},[145,1421,227],{"class":162},[145,1423,1424],{"class":169},"\"cookie\"",[145,1426,556],{"class":162},[145,1428,982],{"class":158},[145,1430,985],{"class":169},[145,1432,809],{"class":162},[145,1434,1435,1438,1441,1443,1445],{"class":147,"line":387},[145,1436,1437],{"class":169},"    \"x-forwarded-for\"",[145,1439,1440],{"class":162},": peer.remoteAddress ",[145,1442,982],{"class":158},[145,1444,985],{"class":169},[145,1446,809],{"class":162},[145,1448,1449],{"class":147,"line":410},[145,1450,1451],{"class":162},"  }),\n",[145,1453,1454],{"class":147,"line":436},[145,1455,239],{"class":162},[1457,1458,1459],"important",{},[89,1460,1461,1462,1464,1465,1468,1469,1471,1472,1474,1475,1479,1480,1485,1486,1493],{},"\nThe WHATWG global ",[105,1463,107],{}," constructor does ",[255,1466,1467],{},"not"," accept custom headers. ",[105,1470,496],{}," is only honored when a ",[105,1473,107],{}," constructor that takes a third options argument is passed via the ",[99,1476,1477,114],{"href":111},[105,1478,107],{}," — e.g. ",[99,1481,1483],{"href":1228,"rel":1482},[103],[105,1484,1232],{}," or ",[99,1487,1490],{"href":1488,"rel":1489},"https:\u002F\u002Fundici.nodejs.org",[103],[105,1491,1492],{},"undici",". With the global constructor the option is silently ignored.",[121,1495,1497],{"id":1496},"api","API",[834,1499,1501],{"id":1500},"createwebsocketproxytarget",[105,1502,1503],{},"createWebSocketProxy(target)",[89,1505,1506,1507,1485,1509,1512],{},"Accepts either a target URL (",[105,1508,1108],{},[105,1510,1511],{},"URL","), a resolver function, or an options object:",[1514,1515,1516,1542,1597,1610,1630,1650],"ul",{},[1517,1518,1519,1523,1524,1527,1528,1530,1531,1533,1534,1536,1537,1541],"li",{},[255,1520,1521],{},[105,1522,636],{}," — ",[105,1525,1526],{},"string | URL | (peer: Peer) => string | URL | Promise\u003Cstring | URL>",". The upstream WebSocket URL, or a function (optionally async) that resolves it per peer. The proxy does not enforce a scheme allowlist; any URL the configured ",[105,1529,107],{}," constructor accepts (including ",[105,1532,1236],{}," with ",[105,1535,1232],{},") works. See the ",[99,1538,1540],{"href":1539},"#dynamic-target","SSRF warning"," before using a dynamic resolver.",[1517,1543,1544,1523,1548,1551,1552,1555,1556,1588,1591,1592,1596],{},[255,1545,1546],{},[105,1547,831],{},[105,1549,1550],{},"boolean | string | string[] | Record\u003Cstring, string> | (peer: Peer) => string | string[] | undefined"," (default ",[105,1553,1554],{},"true","). Controls the subprotocol(s) offered to the upstream:",[1514,1557,1558,1569,1579,1585],{},[1517,1559,1560,1562,1563,1565,1566,1568],{},[105,1561,1554],{}," forwards the client's ",[105,1564,787],{}," header verbatim; ",[105,1567,817],{}," offers none.",[1517,1570,1571,1572,1574,1575,1578],{},"a ",[105,1573,1108],{},"\u002F",[105,1576,1577],{},"string[]"," offers a fixed value upstream regardless of the client.",[1517,1580,1571,1581,1584],{},[105,1582,1583],{},"Record\u003Cstring, string>"," rewrites matching client tokens to their mapped values, passing unmapped tokens through.",[1517,1586,1587],{},"a function resolves the value per peer.",[1589,1590],"br",{},"See ",[99,1593,1595],{"href":1594},"#rewriting-the-upstream-subprotocol","rewriting the upstream subprotocol",". In all cases the value echoed back to the client is the first client-offered token (RFC 6455), and values that are not valid RFC 7230 tokens are dropped from the echo.",[1517,1598,1599,1523,1603,1606,1607,1609],{},[255,1600,1601],{},[105,1602,496],{},[105,1604,1605],{},"HeadersInit | (peer: Peer) => HeadersInit",". Extra headers to send on the upstream handshake. Only honored when a custom ",[105,1608,107],{}," constructor that accepts a third options argument is supplied — the WHATWG global ignores it.",[1517,1611,1612,1523,1616,1551,1619,1622,1623,1625,1626,1629],{},[255,1613,1614],{},[105,1615,658],{},[105,1617,1618],{},"number",[105,1620,1621],{},"1048576",", i.e. 1 MiB). Maximum number of bytes buffered per peer while the upstream is still connecting. String frames are accounted at their UTF-8 worst case (3 bytes per UTF-16 code unit) to avoid undercounting multi-byte content. When exceeded, the peer is closed with code ",[105,1624,679],{}," (Message Too Big). Set to ",[105,1627,1628],{},"0"," to disable.",[1517,1631,1632,1523,1636,1551,1638,1641,1642,1644,1645,1647,1648,1629],{},[255,1633,1634],{},[105,1635,664],{},[105,1637,1618],{},[105,1639,1640],{},"10000","). Milliseconds to wait for the upstream WebSocket handshake to complete — and, for an async ",[105,1643,636],{}," resolver, for the resolver to settle. If exceeded, the peer is closed with code ",[105,1646,668],{},". Set to ",[105,1649,1628],{},[1517,1651,1652,1523,1656,1551,1659,1662,1663,1665],{},[255,1653,1654],{},[105,1655,107],{},[105,1657,1658],{},"typeof WebSocket",[105,1660,1661],{},"globalThis.WebSocket","). Custom ",[105,1664,107],{}," constructor used to dial the upstream. Falls back to the global when omitted; throws at setup time if neither is available.",[89,1667,1668,1669,1671,1672,641,1674,641,1677,641,1680,1683,1684,1687],{},"Returns a ",[105,1670,133],{}," object containing ",[105,1673,264],{},[105,1675,1676],{},"open",[105,1678,1679],{},"message",[105,1681,1682],{},"close",", and ",[105,1685,1686],{},"error"," hooks.",[1689,1690,1691],"style",{},"html pre.shiki code .sCsY4, html code.shiki .sCsY4{--shiki-light:#6A737D;--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .so5gQ, html code.shiki .so5gQ{--shiki-light:#D73A49;--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .slsVL, html code.shiki .slsVL{--shiki-light:#24292E;--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .sfrk1, html code.shiki .sfrk1{--shiki-light:#032F62;--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .suiK_, html code.shiki .suiK_{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .shcOC, html code.shiki .shcOC{--shiki-light:#6F42C1;--shiki-default:#B392F0;--shiki-dark:#B392F0}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sQHwn, html code.shiki .sQHwn{--shiki-light:#E36209;--shiki-default:#FFAB70;--shiki-dark:#FFAB70}",{"title":141,"searchDepth":155,"depth":155,"links":1693},[1694,1695,1696,1697,1700,1703,1704],{"id":123,"depth":155,"text":124},{"id":269,"depth":155,"text":270},{"id":500,"depth":155,"text":501},{"id":780,"depth":155,"text":781,"children":1698},[1699],{"id":836,"depth":176,"text":837},{"id":1129,"depth":155,"text":1130,"children":1701},[1702],{"id":1218,"depth":176,"text":1219},{"id":1312,"depth":155,"text":1313},{"id":1496,"depth":155,"text":1497,"children":1705},[1706],{"id":1500,"depth":176,"text":1503},"Forward incoming WebSocket connections to an upstream ws:\u002F\u002F or wss:\u002F\u002F target.","md",{"icon":45},{"icon":45},{"title":42,"description":1707},"y7dBSDm69g5ydvtXUJ0xrOwByzmHGOMAKDtfkBY28Y4",[1714,1715],{"title":37,"path":38,"stem":39,"description":141,"icon":40,"children":-1},{"title":47,"path":48,"stem":49,"description":141,"icon":52,"children":-1},1782852616982]