Skip to content

[ERR_HTTP_HEADERS_SENT] Cannot write headers after they are sent to the client #3099

Open
@gajus

Description

@gajus

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions