sentry-javascript icon indicating copy to clipboard operation
sentry-javascript copied to clipboard

TypeError: Invalid state: Controller is already closed from Sentry Astro middleware

Open jongjunpark opened this issue 7 months ago • 3 comments

Environment

SaaS (https://sentry.io/)

Steps to Reproduce

  1. Install and configure @sentry/astro in an Astro project.
import node             from '@astrojs/node';
import react            from '@astrojs/react';
import { defineConfig } from 'astro/config';
import sentry from '@sentry/astro';
import { loadEnv } from "vite";
import pkg from "./package.json";


const { PUBLIC_SENTRY_AUTH_TOKEN, PUBLIC_SENTRY_DSN } = loadEnv(process.env.NODE_ENV || 'development', process.cwd(), "");

export default defineConfig({
  integrations: [react(), sentry({
    dsn: PUBLIC_SENTRY_DSN,
    release: `io-shell@${pkg.version}`,
    sourceMapsUploadOptions: {
      project: "my-project",
      org: "my-org",
      authToken: PUBLIC_SENTRY_AUTH_TOKEN,
    },
  })],
  output: 'server',
  adapter: node({
    mode: 'standalone',
  }),
  server: {
    host: '0.0.0.0',
  },
  build: {
    assets: 'io/_astro'
  },
  experimental: {
    svg: true
  }
});
  1. Collect issues using Sentry SaaS in a deployed application.
  2. The following error keeps occurring in the server middleware of @sentry/astro:
    • TypeError: Invalid state: Controller is already closed

Expected Result

no issue in @sentry/astro

Actual Result

TypeError Invalid state: Controller is already closed

issue from node_modules/.pnpm/@[email protected][email protected]_@[email protected][email protected][email protected][email protected]_a7e017124249dc9628fe25e51673e2f3/node_modules/@sentry/astro/build/esm/server/middleware.js

Image

Product Area

Issues

Link

https://imweb-jm.sentry.io/issues/6655871353/?project=4509439058903040&query=is%3Aunresolved%20issue.priority%3A%5Bhigh%2C%20medium%5D&referrer=issue-stream&sort=freq&stream_index=0

DSN

https://ae0f622d57003e90e7b2c6a19ca18a4d@o4509241587597312.ingest.us.sentry.io/4509439058903040

Version

No response

jongjunpark avatar Jun 05 '25 06:06 jongjunpark

Auto-routing to @getsentry/product-owners-issues for triage ⏲️

getsantry[bot] avatar Jun 05 '25 06:06 getsantry[bot]

To identify the root cause of the issue, I created a simplified test scenario that reproduces the error. Here's the code I used:

// Test for error scenario
function testErrorScenario() {
  console.log('Test 1: Reproducing the issue when calling close after error');

  const stream = new ReadableStream({
    start(controller) {
      try {
        // Send some data
        controller.enqueue(new TextEncoder().encode('Data 1'));

        // Intentionally throw an error
        throw new Error('Intentional error');
      }
      catch (e) {
        console.log('Error occurred:', e.message);
        controller.error(e);
      }
      finally {
        try {
          console.log('Attempting to call close in finally block');
          controller.close();
          console.log('close call successful (unexpected)');
        }
        catch (closeError) {
          console.log('close call failed:', closeError.message);
        }
      }
    },
  });

  // Consume the stream
  const reader = stream.getReader();
  reader.read().then(
    ({ done, value }) => console.log('Read result:', done, value),
    error => console.log('Stream error:', error.message),
  );
}

// Execute
testErrorScenario();

When executing this code in Node.js, the console shows the following output:

Test 1: Reproducing the issue when calling close after error
Error occurred: Intentional error
Attempting to call close in finally block
close call failed: Invalid state: Controller is already closed
Stream error: Intentional error

Based on this test, I've identified that the issue occurs because controller.close() is being called in the finally block after controller.error() has already been called in the catch block. Once controller.error() is called, the stream controller is already in a closed state, so attempting to call controller.close() afterwards results in the "Invalid state: Controller is already closed" error.

jongjunpark avatar Jun 05 '25 06:06 jongjunpark

Thank you for the reproduction, we're having a look!

s1gr1d avatar Jun 06 '25 08:06 s1gr1d

Can you share the middleware code you have in your app? I just tried to reproduce this inside of an astro app, but everything seems to be working for me. I added a middleware like this to my astro app:

// src/middleware.js
export const onRequest = async (context, next) => {
  const response = await next();
  const html = await response.text();

  if (html.includes('THROW_IN_MIDDLEWARE')) {
    // return stream that throws an error and closes itself
    // this tests that `controller.close()` being called in user-land is handled by the SDK
    const stream = new ReadableStream({
      start(controller) {
        try {
          throw new Error('THROW_IN_MIDDLEWARE');
        } finally {
          controller.close();
        }
      },
    });

    return new Response(stream, {
      status: 400,
      headers: response.headers,
    });
  }

  return new Response(html, {
    status: 200,
    headers: response.headers,
  });
};

and then this page:

---
import Layout from "../../layouts/Layout.astro";
export const prerender = false;
---

<Layout title="Middleware Error">

  <h1>Page with Middleware Error</h1>

  <p>THROW_IN_MIDDLEWARE</p>

</Layout>

and that seems to work as expected for me 🤔

Also for clarity, could you share the version of Astro & @sentry/astro you are using? Where/how are you using streaming? Are you calling controller.close() somewhere?

mydea avatar Jun 23 '25 14:06 mydea

A PR closing this issue has just been released 🚀

This issue was referenced by PR #16693, which was included in the 9.35.0 release.

github-actions[bot] avatar Jul 04 '25 11:07 github-actions[bot]