TypeError: Invalid state: Controller is already closed from Sentry Astro middleware
Environment
SaaS (https://sentry.io/)
Steps to Reproduce
- 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
}
});
- Collect issues using Sentry SaaS in a deployed application.
- 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
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
Auto-routing to @getsentry/product-owners-issues for triage ⏲️
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.
Thank you for the reproduction, we're having a look!
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?
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.