Open
Description
Describe the bug
[22:36:15.758] 0ms error %@contra/contra-api: unhandledRejection
error:
code: ERR_HTTP_HEADERS_SENT
message: Cannot write headers after they are sent to the client
name: Error
stack:
"""
Error [ERR_HTTP_HEADERS_SENT]: Cannot write headers after they are sent to the client
at ServerResponse.writeHead (node:_http_server:345:11)
at safeWriteHead (/srv/node_modules/.pnpm/[email protected]/node_modules/fastify/lib/reply.js:556:9)
at onSendEnd (/srv/node_modules/.pnpm/[email protected]/node_modules/fastify/lib/reply.js:595:5)
at wrapOnSendEnd (/srv/node_modules/.pnpm/[email protected]/node_modules/fastify/lib/reply.js:549:5)
at next (/srv/node_modules/.pnpm/[email protected]/node_modules/fastify/lib/hooks.js:293:7)
at handleResolve (/srv/node_modules/.pnpm/[email protected]/node_modules/fastify/lib/hooks.js:310:5)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
"""
Added console.trace
to try to catch it.
Trace: header
at Reply.header (/srv/node_modules/.pnpm/[email protected]_patch_hash=lcbqiye6cxjewuoghmhivz2pjm/node_modules/fastify/lib/reply.js:247:11)
at Reply.headers (/srv/node_modules/.pnpm/[email protected]_patch_hash=lcbqiye6cxjewuoghmhivz2pjm/node_modules/fastify/lib/reply.js:274:10)
at Object.handler (file:///srv/apps/contra-api/dist/contra-api/factories/createGraphqlPlugin.js:60:19)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
Trace: header
at Reply.header (/srv/node_modules/.pnpm/[email protected]_patch_hash=lcbqiye6cxjewuoghmhivz2pjm/node_modules/fastify/lib/reply.js:247:11)
at Reply.headers (/srv/node_modules/.pnpm/[email protected]_patch_hash=lcbqiye6cxjewuoghmhivz2pjm/node_modules/fastify/lib/reply.js:274:10)
at Object.handler (file:///srv/apps/contra-api/dist/contra-api/factories/createGraphqlPlugin.js:60:19)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
Trace: header
at Reply.header (/srv/node_modules/.pnpm/[email protected]_patch_hash=lcbqiye6cxjewuoghmhivz2pjm/node_modules/fastify/lib/reply.js:247:11)
at Reply.headers (/srv/node_modules/.pnpm/[email protected]_patch_hash=lcbqiye6cxjewuoghmhivz2pjm/node_modules/fastify/lib/reply.js:274:10)
at Object.handler (file:///srv/apps/contra-api/dist/contra-api/factories/createGraphqlPlugin.js:60:19)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
Trace: send
at Reply.send (/srv/node_modules/.pnpm/[email protected]_patch_hash=lcbqiye6cxjewuoghmhivz2pjm/node_modules/fastify/lib/reply.js:134:11)
at Object.handler (file:///srv/apps/contra-api/dist/contra-api/factories/createGraphqlPlugin.js:71:26)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
Trace: header
at Reply.header (/srv/node_modules/.pnpm/[email protected]_patch_hash=lcbqiye6cxjewuoghmhivz2pjm/node_modules/fastify/lib/reply.js:247:11)
at setCookies (/srv/node_modules/.pnpm/@[email protected]/node_modules/@fastify/cookie/plugin.js:80:15)
at Object.fastifyCookieOnSendHandler (/srv/node_modules/.pnpm/@[email protected]/node_modules/@fastify/cookie/plugin.js:109:5)
at next (/srv/node_modules/.pnpm/[email protected]_patch_hash=lcbqiye6cxjewuoghmhivz2pjm/node_modules/fastify/lib/hooks.js:299:30)
at onSendHookRunner (/srv/node_modules/.pnpm/[email protected]_patch_hash=lcbqiye6cxjewuoghmhivz2pjm/node_modules/fastify/lib/hooks.js:321:3)
at onSendHook (/srv/node_modules/.pnpm/[email protected]_patch_hash=lcbqiye6cxjewuoghmhivz2pjm/node_modules/fastify/lib/reply.js:537:5)
at Reply.send (/srv/node_modules/.pnpm/[email protected]_patch_hash=lcbqiye6cxjewuoghmhivz2pjm/node_modules/fastify/lib/reply.js:161:7)
at Object.handler (file:///srv/apps/contra-api/dist/contra-api/factories/createGraphqlPlugin.js:71:26)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
Our setup is pretty basic:
import { createContext } from './createContext.js';
import { AuthenticationError, ForbiddenError } from '@contra/errors';
import { type DatabasePool } from '@contra/slonik';
import { type Configuration } from '@contra/temporary-common/types.js';
import { useSchema } from '@envelop/core';
import { useParserCache } from '@envelop/parser-cache';
import { useValidationCache } from '@envelop/validation-cache';
import { renderGraphiQL } from '@graphql-yoga/render-graphiql';
import { FastifyReply, FastifyRequest } from 'fastify';
import { fastifyPlugin } from 'fastify-plugin';
import { GraphQLError, type GraphQLSchema } from 'graphql';
import { createYoga, maskError } from 'graphql-yoga';
import { type Redis } from 'ioredis';
import { randomUUID } from 'node:crypto';
import lru from 'tiny-lru';
export const createGraphqlPlugin = fastifyPlugin<{
configuration: Configuration;
pool: DatabasePool;
redis: Redis;
schema: GraphQLSchema;
}>(async (fastify, options) => {
const DAY = 24 * 60 * 60 * 1_000;
type ServerContext = {
reply: FastifyReply;
request: FastifyRequest;
};
const yoga = createYoga<ServerContext>({
context: ({ request }) => {
return createContext({
configuration: options.configuration,
pool: options.pool,
redis: options.redis,
request,
});
},
graphqlEndpoint: '/',
maskedErrors: options.configuration['error-masking']
? {
maskError: (error, message, isDevelopment) => {
// We don't know what this error, therefore using the default masking function
if (!(error instanceof GraphQLError)) {
return maskError(error, message, isDevelopment);
}
const originalError = error?.originalError as
| (Error & {
code?: string;
extensions?: Record<string, any>;
})
| undefined;
const uid = randomUUID();
// Authentication/authorization errors are expected, so we don't mask or report them either
if (
originalError instanceof AuthenticationError ||
originalError instanceof ForbiddenError
) {
return new GraphQLError(
originalError.message,
error.nodes,
error.source,
error.positions,
error.path,
null,
{ code: originalError.code, uid },
);
}
return maskError(error, message, isDevelopment);
},
}
: false,
plugins: [
useSchema(options.schema),
useParserCache({
documentCache: lru(10_000, DAY),
}),
useValidationCache({
cache: lru(10_000, DAY),
}),
],
renderGraphiQL,
schema: options.schema,
});
void fastify.route({
handler: async (request, reply) => {
const response = await yoga.handleNodeRequest(request, {
reply,
request,
});
reply.headers({
...Object.fromEntries(response.headers.entries()),
'x-contra-release-version':
options.configuration['release-version'] ?? 'n/a',
});
if (reply.sent) {
console.trace('already sent 1');
}
reply.status(response.status);
if (reply.sent) {
console.trace('already sent 2');
}
return reply.send(response.body);
},
method: ['GET', 'POST', 'OPTIONS'],
url: yoga.graphqlEndpoint,
});
});
This error appeared only after trying to migrate from Helix to Yoga.
Your Example Website or App
N/A
Steps to Reproduce the Bug or Issue
N/A
Expected behavior
N/A
Screenshots or Videos
No response
Platform
N/A
Additional context
No response