Skip to content

fix: avoid immutable header mutation for mapped streaming responses on node#1551

Open
Falven wants to merge 1 commit intoPortkey-AI:mainfrom
Falven:fix/immutable-response-headers
Open

fix: avoid immutable header mutation for mapped streaming responses on node#1551
Falven wants to merge 1 commit intoPortkey-AI:mainfrom
Falven:fix/immutable-response-headers

Conversation

@Falven
Copy link

@Falven Falven commented Mar 4, 2026

Description: (required)

  • Fixes a TypeError: immutable failure in Node runtime when /v1/responses streaming output goes through the mapped response path.
  • Clones mapped streaming responses (isResponseAlreadyMapped && isStreaming && node) into a new Response with copied status, statusText, body, and headers before updateHeaders() mutates headers.
  • Preserves existing behavior for non-streaming and non-Node paths while keeping x-portkey-* response headers.
  • Fixes bug: /v1/responses stream=true throws TypeError: immutable in responseService.updateHeaders #1550.

Tests Run/Test cases added: (required)

  • pnpm run build
  • Manual check: POST /v1/responses with "stream": true returns SSE successfully and does not throw TypeError: immutable.

Type of Change:

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)

…n node

Problem\n- Streaming /v1/responses requests can fail in Node runtime with `TypeError: immutable` when the response object returned from the mapped path has immutable headers.\n\nRoot cause\n- `ResponseService.create` always calls `updateHeaders`, which appends and deletes headers in place.\n- For `isResponseAlreadyMapped=true` with streaming responses, the underlying `Response` may have an immutable header guard in undici.\n\nChange\n- In `ResponseService.create`, clone mapped streaming responses on Node into a new `Response` using copied status/statusText/body/headers before `updateHeaders` runs.\n- Keep all existing header enrichment/removal logic unchanged.\n\nWhy this is safe\n- Scope is minimal and only applies to the problematic path: mapped + streaming + Node runtime.\n- Non-streaming and non-Node paths are unaffected.\n- Response body streaming is preserved while ensuring headers are mutable for Portkey metadata injection.
Copy link
Contributor

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 TypeError: immutable crash that occurs in the Node.js runtime when streaming responses go through the mapped response path in /v1/responses. The root cause is that Node's undici fetch implementation returns Response objects with immutable headers, and updateHeaders() tries to mutate them in-place.

Changes:

  • Clones the streaming mapped response into a new Response with explicitly mutable headers (via new Headers()) before updateHeaders() is called, but only when the specific conditions that trigger the bug are met (isResponseAlreadyMapped && isStreaming && node runtime).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +64 to +74
if (
isResponseAlreadyMapped &&
this.context.isStreaming &&
getRuntimeKey() == 'node'
) {
finalMappedResponse = new Response(finalMappedResponse.body, {
status: finalMappedResponse.status,
statusText: finalMappedResponse.statusText,
headers: new Headers(finalMappedResponse.headers),
});
}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

This new code path (isResponseAlreadyMapped && isStreaming && node) lacks a corresponding unit test. The existing test file at tests/unit/src/handlers/services/responseService.test.ts has comprehensive tests for create() but none where isStreaming is true. Consider adding a test that sets mockRequestContext.isStreaming = true, mocks getRuntimeKey to return 'node', and verifies that updateHeaders succeeds without throwing (i.e., the cloned response has mutable headers). You could also verify the cloned response preserves status, statusText, and headers from the original.

Copilot uses AI. Check for mistakes.
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.

bug: /v1/responses stream=true throws TypeError: immutable in responseService.updateHeaders

2 participants