Skip to content

bug: retry.fetch strips headers set by HTTP client libraries #2490

@nickwang0808

Description

@nickwang0808

Provide environment information

System:
OS: Windows 11 10.0.26100
CPU: (24) x64 Intel(R) Core(TM) i7-14650HX
Memory: 21.86 GB / 63.78 GB
Binaries:
Node: 22.11.0 - C:\Program Files\nodejs\node.EXE
npm: 10.9.0 - C:\Program Files\nodejs\npm.CMD
bun: 1.1.38 - C:\Program Files\nodejs\bun.CMD

Describe the bug

Summary

The retry.fetch wrapper from @trigger.dev/sdk doesn't preserve headers that are set by HTTP client libraries like openapi-fetch. This causes API requests to fail with "Missing or incorrect Content-Type header" errors when using retry functionality.

Environment

  • @trigger.dev/sdk version: [your version]
  • Runtime: Bun 1.1.38
  • OS: Windows
  • HTTP Client: openapi-fetch

Expected Behavior

When using retry.fetch as a custom fetch implementation with HTTP client libraries, headers set by the client (either in initial config or middleware) should be preserved across retry attempts.

Actual Behavior

Headers set by HTTP client libraries are stripped during retry attempts, causing requests to fail with missing headers.

Reproduction Steps

  1. Create an openapi-fetch client with headers in config:
import createClient from "openapi-fetch";
import { retry } from "@trigger.dev/sdk";

const client = createClient({
  baseUrl: "https://api.example.com",
  headers: {
    "Authorization": "Bearer token",
    "Content-Type": "application/json",
  },
  fetch: retry.fetch, // Using retry.fetch
});
  1. Make a request that triggers a retry (e.g., to an endpoint that returns 500):
const result = await client.POST("/api/endpoint", {
  body: { data: "test" }
});
  1. Observe that the retry attempts are missing the Authorization and Content-Type headers.

Root Cause Analysis

Looking at the retry.fetch source code, the issue is in the doFetchRequest function:

const response = await fetch(input, {
  ...init,
  headers: {
    ...init?.headers,  // Only preserves ORIGINAL headers from init
    "x-retry-count": attemptCount.toString(),
  },
});

The retry mechanism only spreads init?.headers (the original headers), completely ignoring any headers that were modified by HTTP client middleware or merged by the client library.

Workaround

We had to create a custom fetch wrapper:

const retryFetchWithHeaders = async (
  input: RequestInfo | URL,
  init?: RequestInit
): Promise<Response> => {
  const headers = new Headers(init?.headers);
  
  // Ensure required headers are present
  if (!headers.has('Authorization')) {
    headers.set('Authorization', `Bearer ${API_TOKEN}`);
  }
  if (!headers.has('Content-Type')) {
    headers.set('Content-Type', 'application/json');
  }

  // Convert Headers to plain object for retry.fetch
  const headersObj: Record<string, string> = {};
  headers.forEach((value, key) => {
    headersObj[key] = value;
  });

  return retry.fetch(input, {
    ...init,
    headers: headersObj,
  });
};

Suggested Fix

The doFetchRequest function should preserve all headers from the incoming request, not just the original init?.headers. Consider:

const response = await fetch(input, {
  ...init,
  headers: {
    ...Object.fromEntries(new Headers(init?.headers).entries()),
    "x-retry-count": attemptCount.toString(),
  },
});

Or better yet, use the Headers API properly to merge headers without losing any that were set by middleware.

Impact

This affects any HTTP client library that:

  • Sets headers in middleware (like openapi-fetch, axios interceptors, etc.)
  • Merges headers from multiple sources
  • Relies on dynamic header setting

Additional Context

This is particularly problematic for OpenAPI-based clients where type-safe headers and authentication are crucial for API functionality. The current behavior makes retry.fetch incompatible with modern HTTP client libraries.

Reproduction repo

Included in description

To reproduce

Included in description

Additional information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions