Skip to content

Conversation

mydea
Copy link
Member

@mydea mydea commented Jun 23, 2025

Hopefully fixes #16491

I could not get a reproducing test there for this 🤔 I figure this is "safe" to do, but not quite sure how/when this would happen. I would assume this was "introduced" by #15995, maybe @Fryuni has a clue how/when that could happen and if this is a reasonable change 🤔

@mydea mydea self-assigned this Jun 23, 2025
@Fryuni
Copy link
Contributor

Fryuni commented Jun 23, 2025

The error description indicates that calling enqueue on a closed controller fails, not close. close should be a no-op on an already closed controller.

The controller may be closed by downstream code for multiple reasons, like a canceled request or a closed connection.
In theory once the request is closed we should stop the iterations and close the original stream. But we could just consume it to the end discarding everything.

I think (would have to look at the docs) there is a .closed property we could check before calling enqueue

@mydea
Copy link
Member Author

mydea commented Jun 24, 2025

ahh I see, that makes sense. Should we maybe simply not send errors from this to sentry, then 🤔 is there a value in doing this? So just:

try {
                    for await (const chunk of originalBody) {
                      const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
                      const modifiedHtml = addMetaTagToHead(html);
                      controller.enqueue(new TextEncoder().encode(modifiedHtml));
                    }
                  } catch (e) {
                    controller.error(e);
                  } finally {
                    controller.close();
                  }

would lose some visibility, but probably ok...? We could just logger.error the error instead, I suppose... this does not feel actionable for a user, I'd say 🤔 cc @AbhiPrasad

@Fryuni
Copy link
Contributor

Fryuni commented Jun 24, 2025

A bit tricky 🤔

The iterator can fail with a user error from a component, but the body of the loop can error out of the user's control.

We'd be switching from reporting unactionable errors to not reporting actionable ones.

If we hand roll the iteration using the interface and a while loop we could have the try/catch on each part with different error handlers.

@Fryuni
Copy link
Contributor

Fryuni commented Jun 24, 2025

Here is one way to avoid that:

// Assign to a new variable to avoid TS losing the narrower type checked above.
const body = originalBody;

async function* bodyReporter(): AsyncGenerator<string | Buffer> {
  try {
    for await (const chunk of body) {
      yield chunk;
    }
  } catch (e) {
    // Report stream errors coming from user code or Astro rendering.
    sendErrorToSentry(e);
    throw e;
  }
}

const reportedBody = bodyReporter();

try {
  for await (const chunk of reportedBody) {
    const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
    const modifiedHtml = addMetaTagToHead(html);
    controller.enqueue(new TextEncoder().encode(modifiedHtml));
  }
} catch (e) {
  controller.error(e);
} finally {
  controller.close();
}

@mydea
Copy link
Member Author

mydea commented Jun 25, 2025

nice, thank you, that makes sense to me! is there a good way to test that this actually works? 😅

@mydea mydea force-pushed the fn/astro-controller branch from 98aeae9 to 32e798f Compare June 25, 2025 11:26
@mydea mydea changed the title fix(astro): Try-catch controller.close() usage fix(astro): Handle errors in middlewares better Jun 25, 2025
@mydea mydea force-pushed the fn/astro-controller branch from 32e798f to bd0f08c Compare June 27, 2025 08:42
@Fryuni
Copy link
Contributor

Fryuni commented Jun 27, 2025

Hum... A little tricky because this is deeply inside of Astro.

You can make a component with await timers.setTimeout(5000) in the frontmatter and then close the connection while waiting for it. It should cause the enqueue on a closed controller. On a unit test you just need to close the response body and produce a new chunk from the inner response after that.

@mydea
Copy link
Member Author

mydea commented Jul 3, 2025

Hmm I will just merge this as is for now, maybe we can add a test later!

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.

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

4 participants