Skip to content

feat(server): compression plugin filter option should override default content type filter #880

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 14, 2025

Conversation

unnoq
Copy link
Owner

@unnoq unnoq commented Aug 14, 2025

Summary by CodeRabbit

  • New Features
    • Added support to override default compression decisions via a custom filter, enabling compression for custom content types.
  • Bug Fixes
    • Ensured event-stream (Server-Sent Events) and Event Iterator responses are never compressed, even with a custom filter.
    • Confirmed no compression occurs when responses lack a content-type.
  • Documentation
    • Clarified default compression behavior and the filter option, including the event-stream exception.
  • Tests
    • Expanded coverage for custom filter behavior, event-stream handling, and missing content-type scenarios.

Copy link

vercel bot commented Aug 14, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Project Deployment Preview Comments Updated (UTC)
orpc Ready Preview Comment Aug 14, 2025 4:13am

Copy link

coderabbitai bot commented Aug 14, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Refactors compression filter logic in fetch and node adapters to centralize decision-making and explicitly avoid compressing Event Iterator responses. Updates tests to cover custom filter behavior (true/false), no-content-type handling, and event-stream invariants. Documentation/comments updated to reflect filter override semantics and default compressible content-type behavior.

Changes

Cohort / File(s) Summary
Fetch adapter tests
packages/server/src/adapters/fetch/compression-plugin.test.ts
Added tests for: missing content-type (no compression); custom filter returning true (forces compression over non-compressible type); custom filter returning false (no compression); event-stream remains uncompressed even with filter. Verifies filter invocation args.
Fetch adapter implementation
packages/server/src/adapters/fetch/compression-plugin.ts
Made filter non-optional with a default. Centralized compression decision in filter, including explicit block for Event Iterator responses. Simplified invocation guard. Updated docs/comments.
Node adapter tests
packages/server/src/adapters/node/compression-plugin.test.ts
Added large payload fixture. Added tests for filter true (force gzip) and filter false (no compression). Ensured event iterator remains uncompressed even when filter is true.
Node adapter implementation
packages/server/src/adapters/node/compression-plugin.ts
Revised filter/decision flow: never compress Event Iterator responses (content-type event-stream without content-disposition). Delegates to user filter or default compression.filter. Updated docs/comments.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Adapter
  participant CompressionPlugin
  participant Responder

  Client->>Adapter: HTTP Request
  Adapter->>Responder: Produce Response
  Responder-->>Adapter: Response (body, headers)
  Adapter->>CompressionPlugin: shouldCompress(req, res)?
  CompressionPlugin->>CompressionPlugin: check Event Iterator (text/event-stream && no Content-Disposition)
  alt Event Iterator
    CompressionPlugin-->>Adapter: false
  else
    CompressionPlugin->>CompressionPlugin: userFilter? use it : defaultContentTypeFilter
    CompressionPlugin-->>Adapter: true/false
  end
  alt true
    Adapter->>Adapter: apply gzip
  else false
    Adapter->>Adapter: pass through
  end
  Adapter-->>Client: Final Response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

size:XL

Poem

I twitch my ears at streams that sing,
No zip for events—let text take wing.
Filters hop in, true or no,
Gzip burrows where bytes should go.
Headers rustle, bodies light—
A tidy warren, snug and right. 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/server/improve-compression-plugin-filter-option

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Aug 14, 2025
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Summary of Changes

Hello @unnoq, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the CompressionPlugin in the server by allowing its filter option to explicitly override the default content type checks. This provides greater control over when responses are compressed, enabling compression even for content types typically considered non-compressible, while still preventing compression for Event Iterator responses.

Highlights

  • Custom Filter Override: The CompressionPlugin's filter option now takes precedence over the default content type checks, allowing users to force compression for any response type (except Event Iterators) by returning true from their custom filter.
  • Clarified Documentation: The JSDoc for the filter option has been updated to clearly explain its overriding behavior and to explicitly state that Event Iterator responses are never compressed.
  • Comprehensive Test Coverage: New test cases have been added for both Fetch and Node.js adapters to validate that responses without content-type are not compressed, and that the custom filter correctly enables or disables compression regardless of the default content type rules.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request enhances the compression plugin by allowing the filter option to override the default content-type based filtering logic. The changes are consistently applied to both the Fetch and Node.js server adapters. The implementation is solid, with updated documentation and comprehensive new tests that cover the override behavior, including important edge cases like event streams. I have one suggestion to improve code consistency in the Node.js adapter.

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Copy link

codecov bot commented Aug 14, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link

pkg-pr-new bot commented Aug 14, 2025

More templates

@orpc/arktype

npm i https://pkg.pr.new/@orpc/arktype@880

@orpc/client

npm i https://pkg.pr.new/@orpc/client@880

@orpc/contract

npm i https://pkg.pr.new/@orpc/contract@880

@orpc/experimental-durable-event-iterator

npm i https://pkg.pr.new/@orpc/experimental-durable-event-iterator@880

@orpc/hey-api

npm i https://pkg.pr.new/@orpc/hey-api@880

@orpc/interop

npm i https://pkg.pr.new/@orpc/interop@880

@orpc/json-schema

npm i https://pkg.pr.new/@orpc/json-schema@880

@orpc/nest

npm i https://pkg.pr.new/@orpc/nest@880

@orpc/openapi

npm i https://pkg.pr.new/@orpc/openapi@880

@orpc/openapi-client

npm i https://pkg.pr.new/@orpc/openapi-client@880

@orpc/otel

npm i https://pkg.pr.new/@orpc/otel@880

@orpc/react

npm i https://pkg.pr.new/@orpc/react@880

@orpc/react-query

npm i https://pkg.pr.new/@orpc/react-query@880

@orpc/server

npm i https://pkg.pr.new/@orpc/server@880

@orpc/shared

npm i https://pkg.pr.new/@orpc/shared@880

@orpc/solid-query

npm i https://pkg.pr.new/@orpc/solid-query@880

@orpc/standard-server

npm i https://pkg.pr.new/@orpc/standard-server@880

@orpc/standard-server-aws-lambda

npm i https://pkg.pr.new/@orpc/standard-server-aws-lambda@880

@orpc/standard-server-fetch

npm i https://pkg.pr.new/@orpc/standard-server-fetch@880

@orpc/standard-server-node

npm i https://pkg.pr.new/@orpc/standard-server-node@880

@orpc/standard-server-peer

npm i https://pkg.pr.new/@orpc/standard-server-peer@880

@orpc/svelte-query

npm i https://pkg.pr.new/@orpc/svelte-query@880

@orpc/tanstack-query

npm i https://pkg.pr.new/@orpc/tanstack-query@880

@orpc/trpc

npm i https://pkg.pr.new/@orpc/trpc@880

@orpc/valibot

npm i https://pkg.pr.new/@orpc/valibot@880

@orpc/vue-colada

npm i https://pkg.pr.new/@orpc/vue-colada@880

@orpc/vue-query

npm i https://pkg.pr.new/@orpc/vue-query@880

@orpc/zod

npm i https://pkg.pr.new/@orpc/zod@880

commit: c0a4f66

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/server/src/adapters/node/compression-plugin.ts (1)

28-28: Good call: switched to hasHeader for Content-Disposition detection

Using res.hasHeader('content-disposition') is clearer and avoids undefined checks. Matches prior feedback.

🧹 Nitpick comments (7)
packages/server/src/adapters/node/compression-plugin.ts (2)

9-13: Clarify “never compress Event Iterator responses” vs. code’s Content-Disposition exception

Docs say “never compressed,” but the implementation only blocks compression for event-stream when Content-Disposition is absent. If the Content-Disposition header is present, compression may proceed. Please align the comment with the actual behavior or remove the Content-Disposition exception in code.

Would you like me to update the JSDoc to: “Never compress Event Iterator responses (identified as text/event-stream without a Content-Disposition header)”?


29-29: Make Content-Type handling robust and case-insensitive

To avoid edge cases (header arrays and casing), normalize the header and lower-case before checking for event-stream.

Apply this diff:

-        const contentType = res.getHeader('content-type')?.toString()
+        const ct = res.getHeader('content-type')
+        const contentType = Array.isArray(ct)
+          ? ct[0]?.toLowerCase()
+          : ct?.toString().toLowerCase()
 
-        if (!hasContentDisposition && contentType?.startsWith('text/event-stream')) {
+        if (!hasContentDisposition && contentType?.startsWith('text/event-stream')) {
           return false
         }

Also applies to: 34-34

packages/server/src/adapters/node/compression-plugin.test.ts (2)

145-165: Potential flakiness: ensure the filter is invoked deterministically

With a small response body (“output”), the compression middleware’s internal threshold logic can short-circuit before calling the filter in some implementations. To make this test robust, force filter invocation by setting threshold: 0 (or returning a large body).

Apply this diff:

-          new CompressionPlugin({ filter }),
+          new CompressionPlugin({ filter, threshold: 0 }),

If you’d rather keep the default threshold, consider returning largeText in the handler to guarantee the decision path.


8-8: Add a Node test for “no Content-Type” to mirror Fetch behavior

Fetch tests assert no compression when Content-Type is missing. After aligning the Node plugin’s default behavior, add a similar test here to avoid divergence across adapters.

Example to add:

it('should not compress when response has no content-type', async () => {
  const res = await request(async (req: IncomingMessage, res: ServerResponse) => {
    const handler = new RPCHandler(os.handler(() => 'output'), {
      plugins: [new CompressionPlugin()],
      interceptors: [
        async (options) => {
          const result = await options.next()
          if (!result.matched) return result
          return {
            ...result,
            response: {
              ...result.response,
              headers: {
                ...result.response.headers,
              },
              body: new Blob(['output']), // omit type -> no content-type
            },
          }
        },
      ],
    })
    await handler.handle(req, res)
  })
    .post('/')
    .set('accept-encoding', 'gzip, deflate')
    .send({ input: 'test' })

  expect(res.status).toBe(200)
  expect(res.headers['content-encoding']).toBeUndefined()
  expect(res.headers['content-type']).toBeUndefined()
  expect(res.text).toContain('output')
})

Also applies to: 105-143, 145-165, 225-226

packages/server/src/adapters/fetch/compression-plugin.ts (2)

31-35: Clarify doc to match detection heuristic

Docs say “never compress Event Iterator responses,” while code only blocks when Content-Disposition is absent and Content-Type starts with text/event-stream. Consider updating wording accordingly to avoid confusion.

Would you like me to update the JSDoc here as well for parity with the Node adapter?


54-68: Normalize Content-Type to lowercase for robust matching

Lower-casing avoids casing edge cases for event-stream detection and keeps behavior consistent.

Apply this diff:

-    this.filter = (request, response) => {
-      const hasContentDisposition = response.headers.has('content-disposition')
-      const contentType = response.headers.get('content-type')
+    this.filter = (request, response) => {
+      const hasContentDisposition = response.headers.has('content-disposition')
+      const contentType = response.headers.get('content-type')?.toLowerCase()
 
       /**
        * Never compress Event Iterator responses.
        */
       if (!hasContentDisposition && contentType?.startsWith('text/event-stream')) {
         return false
       }
 
       return options.filter
         ? options.filter(request, response)
         : isCompressibleContentType(contentType)
     }
packages/server/src/adapters/fetch/compression-plugin.test.ts (1)

525-567: Nit: test name grammar

Consider “should override filter and compress if filter returns true” for consistency with the next test.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d53d856 and 583fd98.

📒 Files selected for processing (4)
  • packages/server/src/adapters/fetch/compression-plugin.test.ts (3 hunks)
  • packages/server/src/adapters/fetch/compression-plugin.ts (3 hunks)
  • packages/server/src/adapters/node/compression-plugin.test.ts (3 hunks)
  • packages/server/src/adapters/node/compression-plugin.ts (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
packages/server/src/adapters/node/compression-plugin.test.ts (2)
packages/server/src/builder.ts (2)
  • handler (273-280)
  • os (336-352)
packages/server/src/adapters/node/compression-plugin.ts (1)
  • CompressionPlugin (21-98)
packages/server/src/adapters/fetch/compression-plugin.ts (3)
packages/server/src/adapters/node/compression-plugin.ts (1)
  • CompressionPluginOptions (7-15)
packages/standard-server-peer/src/client.ts (1)
  • request (73-176)
packages/standard-server-peer/src/server.ts (1)
  • response (146-186)
packages/server/src/adapters/fetch/compression-plugin.test.ts (1)
packages/server/src/adapters/fetch/compression-plugin.ts (1)
  • CompressionPlugin (46-128)
🔇 Additional comments (8)
packages/server/src/adapters/node/compression-plugin.test.ts (3)

8-8: Fixture LGTM

Using 2KB ensures threshold is crossed for compression-related tests.


105-143: Override behavior test looks solid

Validates that user filter can force compression for a non-default-compressible type (image/jpeg). Good coverage and assertion that filter is invoked.


225-226: Event Iterator invariant retained

Good to see that even with a permissive filter, event-stream responses are not compressed.

packages/server/src/adapters/fetch/compression-plugin.ts (2)

49-49: Good: filter is always defined

Simplifies guard logic and centralizes decision-making.


110-110: LGTM: simplified guard

Since filter is always defined, this is the right simplification.

packages/server/src/adapters/fetch/compression-plugin.test.ts (3)

316-344: Great: explicit “no Content-Type” behavior covered

Confirms the new default filter semantics for missing content-type.


568-592: Filter-false behavior covered well

Asserts both no compression and that the filter was invoked with (Request, Response).


600-601: Event Iterator invariant with explicit filter

Keeps “no compression for event-stream” intact even when a permissive filter is provided.

@unnoq unnoq merged commit b77809d into main Aug 14, 2025
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
size:L This PR changes 100-499 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant