Open yusukebe opened 2 years ago
I tried months ago in Hono, but it failed with some dependencies in edge environment (but I already forgot what exactly it's now), Let me check it again to have a try.
Ah, I see. Maybe it's not so straightforward.
I think basically the blocker is in @apollo/server
itself, it assumes a Node.js runtime in many places by importing things like os
, util
, zlib
, and so on.
https://github.com/apollographql/apollo-server/issues/6034#issuecomment-1312468412
Thanks @ronny ,yeah, I had been doing the the same experiment, and saw the same thing. There're not only node
runtime anymore, there're bunch of services running on bun
, deno
, or workerd
, the @apollo/server
should make some changes to support those runtimes.
I tested edge runtime apollo integration for cloudflare workers recently. but TTFB is too long as it has to start server for a single request and shut it down after it reply. So not make much sense IMO
I believe a proper wrapper for both Apollo and/or GraphQL Yoga would be a nice touch.
Hey @yusukebe ! Can I do it ?
Hi @FaureAlexis
Thanks. But perhaps we can run Apollo Server with app.mount()
on the Hono app. Though this issue is open, since we would like to have as little middleware managed in the @hono
namespace as possible, it may not be necessary to create @hono/apollo-server
.
Of course, you can create it in your personal repository.
I'm using hono with apollo server. @FaureAlexis this may serve as a starting point.
import { StatusCode } from 'hono/utils/http-status';
const apollo = new ApolloServer();
await apollo.start();
app.on(['GET', 'POST', 'OPTIONS'], '/graphql', async ctx => {
if (ctx.req.method === 'OPTIONS') {
// prefer status 200 over 204
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS#status_code
return ctx.text('');
}
const httpGraphQLResponse = await apollo.executeHTTPGraphQLRequest({
httpGraphQLRequest: {
body: ctx.req.method === 'POST' ? await ctx.req.json() : undefined,
headers: new HeaderMap(Object.entries(ctx.req.header())),
method: ctx.req.method,
search: new URL(ctx.req.url).search,
},
context: async () => getContext(ctx), // getContext is my own graphql context factory
});
const { headers, body, status } = httpGraphQLResponse;
for (const [headerKey, headerValue] of headers) {
ctx.header(headerKey, headerValue);
}
ctx.status((status as StatusCode) ?? 200);
if (body.kind === 'complete') {
return ctx.body(body.string);
}
return ctx.body(body.asyncIterator);
});
@yusukebe What is the benefit of app.mount()
over app.on()
?
@obedm503 That's great!!
@yusukebe What is the benefit of app.mount() over app.on()?
I did not try but if we use integration like cloudflare integration, we can write just like this:
app.mount('/grahph', handleGraphQLRequest)
But your way is better than it since we don't have to use integrations and app.mount()
can't manage each handler of the adapted app.
Sounds good.
I still have a few questions about that might help @FaureAlexis and anyone else trying to do this.
How does hono handle duplicate headers? I am naively converting the object returned by ctx.req.header()
into apollo's HeaderMap
but this might not work with duplicates. Node's headers can be either a single string of a list of strings. Apollo integrations (cloudflare, koa) usually handle this by doing something like
const headerMap = new HeaderMap();
headers.forEach((value, key) => {
headerMap.set(key, Array.isArray(value) ? value.join(', ') : value);
});
but this would not be needed if hono already normalizes them.
Also, is ctx.body()
the right way to return a streamed response? Does it need to be converted to a Readable
stream beforehand? Should stream.pipe()
from hono/streaming
be used instead? httpGraphQLResponse.body.asyncIterator
is of type AsyncIterableIterator<string>
@obedm503
How does hono handle duplicate headers?
It uses the Header
object to manage header values. The append
method allows it to handle multiple values, so we don't do anything special for duplicated headers.
Also, is
ctx.body()
the right way to return a streamed response?
Yes! But is stream ReadableStream? You can return a RadableStream content with c.body()
:
return c.body(stream)
If not, you may have to use hono/streaming
.
I tested edge runtime apollo integration for cloudflare workers recently. but TTFB is too long as it has to start server for a single request and shut it down after it reply. So not make much sense IMO
Yeah, that's why I started the project https://github.com/metrue/EdgeQL aiming to provide a fast way to have GraphQL on edge.
Thanks you very much @obedm503 ! Works like a charm
For headers, I was doing like this :
const headers = new HeaderMap();
c.req.raw.headers.forEach((value: string, key: string) => {
if (value) {
headers.set(key, Array.isArray(value) ? value.join(', ') : value);
}
})
@FaureAlexis Since the Headers
class already handles duplicates, it's not necessary to join header values manually. As per MDN. I tested it to confirm.
When Header values are iterated over, they are automatically sorted in lexicographical order, and values from duplicate header names are combined.
Here's my current implementation of a honoApollo
middleware factory function. Feel free to use it and even create an @honojs/apollo-server
or @as-integrations/hono
package.
The honoApollo
would be used as such:
import { honoApollo } from './hono-apollo';
const app = new Hono();
const apolloServer = new ApolloServer();
await apolloServer.start();
app.route(
'/graphql',
honoApollo(apolloServer, async ctx => getContext(ctx)),
);
hono-apollo.ts
would look like this:
import {
HeaderMap,
type ApolloServer,
type BaseContext,
type ContextFunction,
type HTTPGraphQLRequest,
} from '@apollo/server';
import type { Context as HonoContext } from 'hono';
import { stream } from 'hono/streaming';
import { Hono } from 'hono/tiny';
import type { BlankSchema, Env } from 'hono/types';
import type { StatusCode } from 'hono/utils/http-status';
export function honoApollo(
server: ApolloServer<BaseContext>,
getContext?: ContextFunction<[HonoContext], BaseContext>,
): Hono<Env, BlankSchema, '/'>;
export function honoApollo<TContext extends BaseContext>(
server: ApolloServer<TContext>,
getContext: ContextFunction<[HonoContext], TContext>,
): Hono<Env, BlankSchema, '/'>;
export function honoApollo<TContext extends BaseContext>(
server: ApolloServer<TContext>,
getContext?: ContextFunction<[HonoContext], TContext>,
) {
const app = new Hono();
// Handle `OPTIONS` request
// Prefer status 200 over 204
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS#status_code
app.options('/', ctx => ctx.text(''));
// This `any` is safe because the overload above shows that context can
// only be left out if you're using BaseContext as your context, and {} is a
// valid BaseContext.
const defaultContext: ContextFunction<[HonoContext], any> = async () => ({});
const context = getContext ?? defaultContext;
app.on(['GET', 'POST'], '/', async ctx => {
const headerMap = new HeaderMap();
// Use `ctx.req.raw.headers` to avoid multiple loops and intermediate objects
ctx.req.raw.headers.forEach((value, key) => {
// When Header values are iterated over, they are automatically sorted in
// lexicographical order, and values from duplicate header names are combined.
// https://developer.mozilla.org/en-US/docs/Web/API/Headers
headerMap.set(key, value);
});
const httpGraphQLRequest: HTTPGraphQLRequest = {
// Avoid parsing the body unless necessary
body: ctx.req.method === 'POST' ? await ctx.req.json() : undefined,
headers: headerMap,
method: ctx.req.method,
search: new URL(ctx.req.url).search,
};
const httpGraphQLResponse = await server.executeHTTPGraphQLRequest({
httpGraphQLRequest,
context: () => context(ctx),
});
for (const [key, value] of httpGraphQLResponse.headers) {
ctx.header(key, value);
}
ctx.status((httpGraphQLResponse.status as StatusCode) ?? 200);
if (httpGraphQLResponse.body.kind === 'complete') {
return ctx.body(httpGraphQLResponse.body.string);
}
// This should work but remains untested
const asyncIterator = httpGraphQLResponse.body.asyncIterator;
return stream(ctx, async stream => {
for await (const part of asyncIterator) {
await stream.write(part);
}
});
});
return app;
}
@yusukebe Do you have any recommendations on the best router to use here? Does it matter?
@obedm503
Sorry for the super delayed response.
@yusukebe Do you have any recommendations on the best router to use here? Does it matter?
I don't think it matters that much which router you choose. In this case, hono
is fine, not hono/tiny
in particular. Bundle size will change, but not by that much.
How about building Apollo Server Middleware as third-party middleware?
GraphQL middleware is available, but Apollo Sever middleware would be nice to have. Repository and npm repository name will be:
github.com/honojs/apollo-server
@honojs/apollo-server
We can use this issue https://github.com/apollographql/apollo-server/issues/6034#issuecomment-1198712473 as a reference.