Skip to content

Framework-agnostic Tunnel Handler#18892

Open
nikolovlazar wants to merge 12 commits intodevelopfrom
nikolovlazar/agnostic-tunnel-handler
Open

Framework-agnostic Tunnel Handler#18892
nikolovlazar wants to merge 12 commits intodevelopfrom
nikolovlazar/agnostic-tunnel-handler

Conversation

@nikolovlazar
Copy link
Member

@nikolovlazar nikolovlazar commented Jan 19, 2026

Update

After talking to the team, we decided to scope this PR only to the core util function, and handle framework adapters in other PRs. For that reason, I've deleted the initial TanStack Start adapter.


original PR description:

Overview

Right now only the Next.js SDK has a built-in tunnel handler on the backend side, but this PR aims to extract the tunnel handling logic in plain JavaScript in @sentry/core, and provide framework-specific adapters.

Framework-specific adapters checklist

  • Next.js
  • Remix
  • SvelteKit
  • Nuxt
  • Astro
  • Solid Start
  • TanStack Start React
  • Nest.js
  • ...?

Questions for the team

  • Is @sentry/core a good home for the framework-agnostic tunnel handler function?
  • What framework-specific adapters am I missing in the checklist? (cloudflare, aws, google cloud services, vercel edge, bun, deno)
  • How do we standardize this across all SDKs, not just JavaScript?

TODOs

  • Finalize framework adapters list
  • E2E tests

Closes #19394 (added automatically)

@github-actions
Copy link
Contributor

github-actions bot commented Jan 19, 2026

node-overhead report 🧳

Note: This is a synthetic benchmark with a minimal express app and does not necessarily reflect the real-world performance impact in an application.
⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Scenario Requests/s % of Baseline Prev. Requests/s Change %
GET Baseline 9,502 - 9,130 +4%
GET With Sentry 1,646 17% 1,655 -1%
GET With Sentry (error only) 6,033 63% 6,160 -2%
POST Baseline 1,205 - 1,195 +1%
POST With Sentry 580 48% 586 -1%
POST With Sentry (error only) 1,051 87% 1,063 -1%
MYSQL Baseline 3,285 - 3,212 +2%
MYSQL With Sentry 486 15% 491 -1%
MYSQL With Sentry (error only) 2,670 81% 2,629 +2%

View base workflow run

@logaretm
Copy link
Member

logaretm commented Jan 19, 2026

Is @sentry/core a good home for the framework-agnostic tunnel handler function?

So the issue with @sentry/core is that adding any code to it increases the bundle size across all the SDKs, so this will increase the bundle size of strictly client-side SDKs (e.g: Vue and React) without providing value to their users.

I think having an agnostic tunnel implementation is worth having, but perhaps we should introduce a @sentry/server-utils SDK similiar to the browser-utils one which would contain this logic, and only SSR SDKs can then use it to implement their tunnel utility.

One thing to keep in mind is different implementations of the tunnel feature depends heavily on the framework itself, in Next.js it is implemented as simple re-write rules, so we almost don't have any runtime logic for it on the serve-side.

cc @Lms24

@timfish
Copy link
Collaborator

timfish commented Jan 20, 2026

So the issue with @sentry/core is that adding any code to it increases the bundle size across all the SDKs

This is not the case because of tree-shaking. We have the ServerRuntimeClient in @sentry/core and it doesn't impact front-end bundle size.

If a helper like this ends up in the Node SDK it won't be usable in non-Node based SDKs.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not sure it's enough to just check the host matches. We should have a list of allowed DSNs and only forward when they match.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah the allowedDsnComponents are passed from the outside and they're exactly that - a list of allowed DSNs.

Copy link
Member Author

Choose a reason for hiding this comment

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

maybe instead of turning them in components we should pass them as string arrays and do a plain string comparison?

Copy link
Member

@Lms24 Lms24 left a comment

Choose a reason for hiding this comment

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

I kinda like this approach over #14137 because it returns the response to the caller. We need to make sure that the client SDK receives Sentry's response to correctly handle rate limits to avoid backpressure build ups of rate-limited data. This way, the client SDK will apply the rate limits on its end and stop sending stuff to the tunnel endpoint. (Tim, let me know if I misinterpreted your PR and we do in fact handle rate limiting)

I have one request though: Let's split this PR up into the core change that adds the helper and then the Tanstack Start stuff on top. We should also add tests for the core handler alone. I'd recommend we take inspiration from #14137 on the integration test front.

Copy link
Member

Choose a reason for hiding this comment

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

l: is this really the case? Do we have to create stubs from all of our server exports in TSS? (cc @nicohrubec)

Copy link
Member

Choose a reason for hiding this comment

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

l: I think we should always assume bytes here, to avoid callers accidentally already strinfifying compressed/binary payloads

Suggested change
body: string | Uint8Array,
body: Uint8Array,

Copy link
Member

Choose a reason for hiding this comment

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

I'd also refactor this a bit to an options object parameter (for future proofing) and as you suggested, make it accept a list of DSN strings. This should be easier for users to use. For us internally, it should make no difference.

@timfish
Copy link
Collaborator

timfish commented Feb 12, 2026

This way, the client SDK will apply the rate limits on its end and stop sending stuff to the tunnel endpoint

No you're totally right. This is a better approach!

I think ideally we also want a wrapper with Request in, Response out which will be easier to use for many frameworks.

If we auto setup tunneling to frameworks (like nextjs does) we should use some random ID rather than /sentry or something static because ad-blockers are going to block that too. The nice thing with auto setup is we can include the server-side DSN as allowed.

@Lms24
Copy link
Member

Lms24 commented Feb 12, 2026

I think ideally we also want a wrapper with Request in, Response out which will be easier to use for many frameworks.

Yup that would be ideal. My thinking was that some frameworks use different kinds of Response objects but turns out the one I had in mind (Sveltekit) actually uses the builtin Response. So @nikolovlazar let's start with Request => Response and if necessary create another more abstract handler later.

If we auto setup tunneling to frameworks (like nextjs does) we should use some random ID rather than /sentry or something static because ad-blockers are going to block that too.

+1 for the random id but let's be careful about full auto setup. AFAIK, we don't auto enable tunnelRoute by default in NextJS. The reason being that this can massively increase users' billing.

…ackage

This will be added in a different PR, separate from the core util
function
@nikolovlazar nikolovlazar marked this pull request as ready for review February 18, 2026 17:10
Copilot AI review requested due to automatic review settings February 18, 2026 17:10
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 pull request extracts the tunnel handler logic from the Next.js SDK into a framework-agnostic core utility function in @sentry/core. The tunnel handler validates incoming Sentry envelopes against allowed DSNs and forwards them to the Sentry ingest endpoint, providing SSRF protection. The implementation supports the broader goal of standardizing tunnel functionality across multiple framework SDKs (Next.js, Remix, SvelteKit, Nuxt, Astro, etc.).

Changes:

  • Adds handleTunnelRequest function to @sentry/core/utils/tunnel.ts as a framework-agnostic tunnel handler
  • Adds comprehensive test suite for the tunnel handler covering various success and error scenarios
  • Exports the new tunnel handler and its options type from @sentry/core
  • Adds a client-side stub for createTunnelHandler in TanStack Start React package

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
packages/core/src/utils/tunnel.ts Core tunnel handler implementation with DSN validation and envelope forwarding
packages/core/test/lib/utils/tunnel.test.ts Test suite covering tunnel handler functionality
packages/core/src/index.ts Exports for the tunnel handler function and options type
packages/tanstackstart-react/src/client/index.ts Client-side no-op stub (appears to be unused/leftover code)

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

@github-actions
Copy link
Contributor

github-actions bot commented Feb 18, 2026

Codecov Results 📊


Generated by Codecov Action

@github-actions
Copy link
Contributor

github-actions bot commented Feb 18, 2026

size-limit report 📦

⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Path Size % Change Change
@sentry/browser 25.56 kB - -
@sentry/browser - with treeshaking flags 24.08 kB - -
@sentry/browser (incl. Tracing) 42.36 kB - -
@sentry/browser (incl. Tracing, Profiling) 47.03 kB - -
@sentry/browser (incl. Tracing, Replay) 81.18 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 70.8 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 85.87 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 98.03 kB - -
@sentry/browser (incl. Feedback) 42.29 kB - -
@sentry/browser (incl. sendFeedback) 30.23 kB - -
@sentry/browser (incl. FeedbackAsync) 35.22 kB - -
@sentry/browser (incl. Metrics) 26.74 kB - -
@sentry/browser (incl. Logs) 26.88 kB - -
@sentry/browser (incl. Metrics & Logs) 27.56 kB - -
@sentry/react 27.33 kB - -
@sentry/react (incl. Tracing) 44.72 kB +0.01% +2 B 🔺
@sentry/vue 30.01 kB - -
@sentry/vue (incl. Tracing) 44.22 kB - -
@sentry/svelte 25.58 kB - -
CDN Bundle 28.11 kB - -
CDN Bundle (incl. Tracing) 43.2 kB - -
CDN Bundle (incl. Logs, Metrics) 28.95 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 44.03 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 68.02 kB - -
CDN Bundle (incl. Tracing, Replay) 80.07 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 80.94 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 85.5 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 86.4 kB - -
CDN Bundle - uncompressed 82.22 kB - -
CDN Bundle (incl. Tracing) - uncompressed 127.93 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 85.05 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 130.76 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 208.71 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 244.81 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 247.63 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 257.61 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 260.42 kB - -
@sentry/nextjs (client) 47.12 kB - -
@sentry/sveltekit (client) 42.81 kB - -
@sentry/node-core 52.15 kB +0.02% +9 B 🔺
@sentry/node 166.53 kB +0.01% +6 B 🔺
@sentry/node - without tracing 93.95 kB +0.02% +12 B 🔺
@sentry/aws-serverless 109.45 kB +0.01% +9 B 🔺

View base workflow run

nikolovlazar and others added 2 commits February 18, 2026 12:44
…est URL

The tunnel handler was manually constructing the Sentry ingest URL without
the required auth query parameters (sentry_key, sentry_version), causing
requests to fail authentication. Use getEnvelopeEndpointWithUrlEncodedAuth
which properly constructs the URL with all required parameters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrap parseEnvelope in try-catch so malformed request bodies return a
400 Bad Request instead of throwing an unhandled error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

Copy link
Member

@Lms24 Lms24 left a comment

Choose a reason for hiding this comment

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

Thanks Lazar! One more cleanup task, then let's merge this!

Copy link
Member

Choose a reason for hiding this comment

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

m: This file is still a leftover from the tanstack start changes, correct? Let's remove it before we merge this.

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.

Framework-agnostic Tunnel Handler

4 participants

Comments