Skip to content

Instantly share code, notes, and snippets.

@ashbuilds
Last active December 31, 2024 13:34
Show Gist options
  • Save ashbuilds/490d504d703a5ddfcdf504f6c21e220d to your computer and use it in GitHub Desktop.
Save ashbuilds/490d504d703a5ddfcdf504f6c21e220d to your computer and use it in GitHub Desktop.
Implementing GraphQL Subscriptions with Websockets in a Bun Server using graphql-yoga
import Bun from 'bun'
import { createYoga, YogaInitialContext, YogaServerInstance } from 'graphql-yoga'
import { makeHandler } from "graphql-ws/lib/use/bun";
import { ExecutionArgs } from "@envelop/types";
import { schema } from './graphql/schema';
interface IUserContext {
token?: string;
}
const PORT = process.env.PORT || 4000;
const yoga: YogaServerInstance<{}, YogaInitialContext> = createYoga({
graphqlEndpoint: '/graphql',
schema,
graphiql: {
subscriptionsProtocol: 'WS',
},
context: async ({ request }): Promise<IUserContext> => {
return {
token: request.headers.get("token") ?? ""
}
}
})
const websocketHandler = makeHandler({
schema,
execute: (args: ExecutionArgs) => args.rootValue.execute(args),
subscribe: (args: ExecutionArgs) => args.rootValue.subscribe(args),
onSubscribe: async (ctx, msg) => {
const {schema, execute, subscribe, contextFactory, parse, validate} = yoga.getEnveloped({
...ctx,
req: ctx.extra.request,
socket: ctx.extra.socket,
params: msg.payload
})
const args = {
schema,
operationName: msg.payload.operationName,
document: parse(msg.payload.query),
variableValues: msg.payload.variables,
contextValue: await contextFactory(),
rootValue: {
execute,
subscribe
}
}
const errors = validate(args.schema, args.document)
if (errors.length) return errors
return args
},
})
const server: Bun.Server = Bun.serve({
fetch: (request: Request, server: Bun.Server): Promise<Response> | Response => {
// Upgrade the request to a WebSocket
if (server.upgrade(request)) {
return new Response()
}
return yoga.fetch(request, server)
},
port: PORT,
websocket: websocketHandler,
})
console.info(
`🚀 Server is running on ${new URL(
yoga.graphqlEndpoint,
`http://${server.hostname}:${server.port}`
)}`
)
@i-void
Copy link

i-void commented Dec 6, 2023

this line cannot set request to context (line 34)
req: ctx.extra.request,
it is undefined so cannot access headers

@CTOHacon
Copy link

CTOHacon commented Dec 3, 2024

For those who faced issues with accessing the passed headers for custom context providing, like authorisation header, here's a useful note on the Authorization token example:

export function getContextJWTToken(context: AppQraphQLContext): string | null {
    if (context.params.extensions?.headers?.Authorization) {
        return getJWTFromStr(context.params.extensions.headers.Authorization);
    }
    if (context.request) {
        const request = context.request;
        const authHeader = request.headers.get('Authorization');
        if (authHeader) return getJWTFromStr(authHeader);
    }

    return null;
}

The typical context.request headers are not present in WS connection but appear in context.params.extensions?.headers

Full example in context of global user context pass:

type AppQraphQLContext = YogaInitialContext & {
  user: User | null;
};
function getContextJWTToken(context: AppQraphQLContext): string | null {
    if (context.params.extensions?.headers?.Authorization) {
        return getJWTFromStr(context.params.extensions.headers.Authorization);
    }
    if (context.request) {
        const request = context.request;
        const authHeader = request.headers.get('Authorization');
        if (authHeader) return getJWTFromStr(authHeader);
    }

    return null;
}
const getContextUser = async (context: AppQraphQLContext) => {
    let token = getContextJWTToken(context);
    try {
        const decoded: any = jwt.verify(token || '', process.env.JWT_SECRET);
        const user = await findUser({ id: decoded.userId });

        return user;
    }
    catch (err) {
        return null;
    }
};

const yoga = createYoga<AppQraphQLContext>({
    schema,
    graphiql: {
        subscriptionsProtocol: 'WS',
    },
    context: async (context: AppQraphQLContext) => {
        return {
            ...context,
            user: await getContextUser(context)
        }
    },
    landingPage: false,
    graphqlEndpoint: '/',
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment