hi folks,

hi folks, i'm leveraging DO for a websocket w/ hubernation support, but keen to understand how i can pass on context to my "webSocketMessage". i want to simply pass on the user id of the user into the webSocketMessage handler. when it gets hibernated, it seems to get lost. i'll drop my fairly basic code in a thread.
4 Replies
dandoen
dandoenOP6d ago
// This gets called in my fetch handler
/*
if (request.url.includes('/websocket')) {
return handleWebsocketRequest(request, env, appUrl, user);
}
*/
export const handleWebsocketRequest = async (
request: Request,
env: Env,
appUrl: string,
user: User | null,
): Promise<Response> => {
if (request.url.includes('/websocket') && user) {
let id = env.WEBSOCKET_HIBERNATION_SERVER.idFromName(`user-${user?.id}`);
let stub = env.WEBSOCKET_HIBERNATION_SERVER.get(id);
// Let the Durable Object handle the WebSocket creation and upgrade
const response = await stub.fetch(request.url, {
method: request.method,
headers: request.headers,
body: request.body,
});

return response as Response;
}

return new Response(
JSON.stringify({
error: {
code: 'Unauthorized',
message: 'You must be logged in to use this endpoint',
},
}),
{
status: 401,
headers: {
'content-type': 'application/json',
'Access-Control-Allow-Origin': appUrl,
'Access-Control-Allow-Credentials': 'true',
},
},
);
};
// This gets called in my fetch handler
/*
if (request.url.includes('/websocket')) {
return handleWebsocketRequest(request, env, appUrl, user);
}
*/
export const handleWebsocketRequest = async (
request: Request,
env: Env,
appUrl: string,
user: User | null,
): Promise<Response> => {
if (request.url.includes('/websocket') && user) {
let id = env.WEBSOCKET_HIBERNATION_SERVER.idFromName(`user-${user?.id}`);
let stub = env.WEBSOCKET_HIBERNATION_SERVER.get(id);
// Let the Durable Object handle the WebSocket creation and upgrade
const response = await stub.fetch(request.url, {
method: request.method,
headers: request.headers,
body: request.body,
});

return response as Response;
}

return new Response(
JSON.stringify({
error: {
code: 'Unauthorized',
message: 'You must be logged in to use this endpoint',
},
}),
{
status: 401,
headers: {
'content-type': 'application/json',
'Access-Control-Allow-Origin': appUrl,
'Access-Control-Allow-Credentials': 'true',
},
},
);
};
// Durable Object
export default class WebSocketHibernationServer extends DurableObject {
async fetch(request: Request): Promise<Response> {
const upgradeHeader = request.headers.get('Upgrade');
if (!upgradeHeader || upgradeHeader !== 'websocket') {
return new Response('Expected Upgrade: websocket', { status: 426 });
}

// Extract userId from the protocol
const wsProtocol = request.headers.get('Sec-WebSocket-Protocol');
const [protocol] = wsProtocol?.split(', ') || [];

console.log('server', { protocol });

const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);

this.ctx.acceptWebSocket(server);

return new Response(null, {
status: 101,
headers: {
Upgrade: 'websocket',
Connection: 'Upgrade',
'Sec-WebSocket-Protocol': protocol || '',
},
webSocket: client,
});
}

async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) {
// TODO: How do we get the user ID from the websocket?

const websockets = this.ctx.getWebSockets();
for (const ws of websockets) {
ws.send(message);
}
}

async webSocketClose(ws: WebSocket, code: number, reason: string, _wasClean: boolean) {
ws.close(code, reason);
}
}
// Durable Object
export default class WebSocketHibernationServer extends DurableObject {
async fetch(request: Request): Promise<Response> {
const upgradeHeader = request.headers.get('Upgrade');
if (!upgradeHeader || upgradeHeader !== 'websocket') {
return new Response('Expected Upgrade: websocket', { status: 426 });
}

// Extract userId from the protocol
const wsProtocol = request.headers.get('Sec-WebSocket-Protocol');
const [protocol] = wsProtocol?.split(', ') || [];

console.log('server', { protocol });

const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);

this.ctx.acceptWebSocket(server);

return new Response(null, {
status: 101,
headers: {
Upgrade: 'websocket',
Connection: 'Upgrade',
'Sec-WebSocket-Protocol': protocol || '',
},
webSocket: client,
});
}

async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) {
// TODO: How do we get the user ID from the websocket?

const websockets = this.ctx.getWebSockets();
for (const ws of websockets) {
ws.send(message);
}
}

async webSocketClose(ws: WebSocket, code: number, reason: string, _wasClean: boolean) {
ws.close(code, reason);
}
}
any push in the right direction would be appreciated!
DaniFoldi
DaniFoldi6d ago
Hey, you can store an attachment on each connection (up to 2048 bytes): https://developers.cloudflare.com/durable-objects/best-practices/websockets/#extended-methods which you can serialize and deserialize any time, and these get saved with the hibernated ws, and you also have the tags which you can set when you create the connection and then retrieve later using getTags
Cloudflare Docs
Using WebSockets · Cloudflare Durable Objects docs
WebSockets are long-lived TCP connections that enable bi-directional, real-time communication between client and server. Both Cloudflare Durable Objects and Workers can act as WebSocket endpoints – either as a client or as a server. Because WebSocket sessions are long-lived, applications commonly use Durable Objects to accept either the client o...
DaniFoldi
DaniFoldi6d ago
it depends on your use case, which one I'd use, if you need to get the connections (eg to forward messages) by user id, you could add a tag userid:<actual id> to the websocket when they connect, but if you only find out later, you can add it to the attachment (although you can't use it later for listing that efficiently)
dandoen
dandoenOP6d ago
oh nice, thank you.. did not see serialize/deserialize

Did you find this page helpful?