Skip to content

Conversation

zoontek
Copy link
Contributor

@zoontek zoontek commented Sep 29, 2025

Hi folks!

Currently, when forwarding naively back and forth headers from the gateway to the subgraphs, if we allow the request ID in both ways, we will end with 2 concatened request IDs:

const ALLOWED_HEADERS = new Set([
  "x-request-id", // <- our request ID

  "accept-language",
  "authorization",
  "content-type",
  "link",
  "location",
  "traceparent",
  "user-agent",
]);

const forwardHeaders = (input: Request | Response) => {
  const headers: Record<string, string> = {};

  for (const [key, value] of input.headers) {
    if (ALLOWED_HEADERS.has(key)) {
      headers[key] = value;
    }
  }

  return headers;
};

// …

propagateHeaders: {
  fromClientToSubgraphs: ({ request }) => forwardHeaders(request),
  fromSubgraphsToClient: ({ response }) => forwardHeaders(response),
}

This happens because:

  • useRequestId set the request ID back on onResponse (link)
  • usePropagateHeaders, that runs after, append it to because it's forwarded (link)

It can be avoided by separating the logic between forwardRequestHeaders and forwardResponseHeaders:

const REQUEST_ID_HEADER = "x-request-id"; // <- our request ID

const ALLOWED_HEADERS = new Set([
  REQUEST_ID_HEADER,

  "accept-language",
  "authorization",
  "content-type",
  "link",
  "location",
  "traceparent",
  "user-agent",
]);

const forwardRequestHeaders = (request: Request) => {
  const headers: Record<string, string> = {};

  for (const [key, value] of request.headers) {
    if (ALLOWED_HEADERS.has(key)) {
      headers[key] = value;
    }
  }

  return headers;
};

const forwardResponseHeaders = (response: Response) => {
  const headers: Record<string, string> = {};

  for (const [key, value] of response.headers) {
    if (ALLOWED_HEADERS.has(key) && key !== REQUEST_ID_HEADER) { // <- don't forward the id back (set by useRequestId)
      headers[key] = value;
    }
  }

  return headers;
};

// …

propagateHeaders: {
  fromClientToSubgraphs: ({ request }) => forwardRequestHeaders(request),
  fromSubgraphsToClient: ({ response }) => forwardResponseHeaders(response),
}

But I think we should add a basic check and don't append an identical value if it already has been set.

@Copilot Copilot AI review requested due to automatic review settings September 29, 2025 12:49
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes a header duplication issue in the usePropagateHeaders plugin where request IDs could be duplicated when forwarding headers from subgraphs to clients. The issue occurs because useRequestId sets the request ID header and then usePropagateHeaders appends it again when forwarding response headers.

  • Adds a duplicate value check before appending headers in the fromSubgraphsToClient flow
  • Prevents header duplication by comparing values before appending to response headers

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

if (value) {
for (const v of value) {
response.headers.append(key, v);
if (v !== response.headers.get(key)) {
Copy link

Copilot AI Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comparison v !== response.headers.get(key) only checks against the first header value when multiple values exist for the same key. response.headers.get(key) returns the first value or null, but headers can have multiple values. This could still allow duplicates if the header already exists with multiple values. Consider using response.headers.getSetCookie() for set-cookie headers or checking if the value already exists in the complete list of values.

Suggested change
if (v !== response.headers.get(key)) {
// Check if the value already exists among all values for this header
const existingValues =
key.toLowerCase() === 'set-cookie' && typeof response.headers.getSetCookie === 'function'
? response.headers.getSetCookie()
: response.headers.get(key)
? response.headers.get(key).split(',').map(s => s.trim())
: [];
if (!existingValues.includes(v)) {

Copilot uses AI. Check for mistakes.

@enisdenjo enisdenjo force-pushed the avoid-header-duplication branch from 4f62016 to 21a8893 Compare October 7, 2025 17:12
@enisdenjo enisdenjo force-pushed the avoid-header-duplication branch from 21a8893 to fd855fa Compare October 7, 2025 17:13
@enisdenjo enisdenjo merged commit 8c4138d into graphql-hive:main Oct 8, 2025
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants