Skip to content

open-stack-frame endpoint broken: ERR_HTTP_HEADERS_SENT when Rozenite middleware is active #229

@lukas-preply

Description

@lukas-preply

Summary

When Rozenite middleware is active (WITH_ROZENITE=true), the Metro /open-stack-frame POST endpoint — used by React Native to open source files in the user's editor — is broken. The file never opens, and the Metro console logs:

Could not open trial.spec.ts in the editor.

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
    at ServerResponse.setHeader (node:_http_outgoing:642:11)
    at app.handle (.../node_modules/@rozenite/middleware/node_modules/express/lib/application.js:161:9)
    at handle (.../node_modules/connect/index.js:91:14)
    at call (.../node_modules/connect/index.js:239:7)
    at Immediate.next (.../node_modules/connect/index.js:183:5)
    at process.processImmediate (node:internal/timers:506:21)

Steps to Reproduce

  1. Start Metro with Rozenite enabled:
    WITH_ROZENITE=true react-native start
  2. Send a POST request to open a file in the editor:
    curl -X POST "http://localhost:8081/open-stack-frame" \
      -H "Content-Type: application/json" \
      -d '{"file":"src/some/file.ts","lineNumber":1}'
  3. File does not open; Metro console shows the error above.

Root Cause Analysis

The middleware chain looks like this:

connect → ... → openStackFrameMiddleware → Rozenite Express middleware → ...

The issue is a collision between openStackFrameMiddleware (from @react-native-community/cli-server-api) and Rozenite's Express middleware:

  1. openStackFrameMiddleware handles the POST, calls launchEditor(), writes res.writeHead(200); res.end(), and then falls through to next() — this is a pre-existing bug in the RN CLI middleware (it always calls next() even after ending the response).

  2. Rozenite's Express middleware (getMiddleware() in @rozenite/middleware) receives the request via next() and its app.handle() attempts to set response headers — but the response is already sent.

  3. This causes ERR_HTTP_HEADERS_SENT, and depending on timing, it may also interfere with launchEditor()'s ability to spawn the editor process.

Relevant code

openStackFrameMiddleware.js (RN CLI — note the unconditional next() call):

const handler = (req, res, next) => {
    if (req.method === 'POST') {
      // ... handles request, sends response ...
      res.writeHead(200);
      res.end();
    }
    next(); // ← called even after res.end() for POST requests
};

Rozenite middleware (@rozenite/middleware/dist/index.js):

app.use((req, _, next) => {
    assert(req.url, "req.url is required");
    logger.debug(`Incoming request: ${req.url}`);
    if (req.url.includes("/rozenite")) {
      req.url = req.url.replace("/rozenite", "");
    }
    next();
});
// ... express.static() catch-all that tries to set headers

Expected Behavior

POST /open-stack-frame should open the specified file in the user's editor, regardless of whether Rozenite middleware is active.

Suggested Fix

Rozenite's middleware could guard against already-finished responses:

app.use((req, res, next) => {
    if (res.headersSent) {
        return; // Response already handled by a previous middleware
    }
    // ... existing logic
});

Alternatively, Rozenite could avoid registering as a catch-all middleware and instead only handle routes under its own namespace (/rozenite/*).

Environment

  • @rozenite/middleware: 1.3.0
  • @rozenite/metro: 1.3.0
  • react-native: 0.81.5
  • @react-native-community/cli-server-api: from @react-native-community/cli 20.0.2
  • Node: ≥ 24
  • macOS

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions