different-ai/openwork · error · ProbeFailure

SHUTDOWN

SHUTDOWN

Error message

Session DELETE returned HTTP ${deleteResponse.status}

What it means

During cleanup the probe DELETEs the negotiated MCP session (expecting HTTP 204 per the MCP streamable-HTTP session lifecycle). When the caller set requireValidDelete (strict shutdown validation), any status other than 204 is raised as a SHUTDOWN ProbeFailure naming the actual status. With non-strict cleanup the DELETE failure is swallowed so it never masks a primary diagnostic failure.

Source

Thrown at packages/enterprise-mcp-mock-server/src/testing/probe.ts:447

  let revocationClientSecret = options.credentials?.clientSecret ?? ""
  let negotiatedProtocolHeader = ""
  let revocationEndpoint: URL | null = null
  const cleanup = async (requireValidDelete: boolean): Promise<void> => {
    const cleanupDeadline = Date.now() + 2_000
    if (sessionId && accessToken && negotiatedProtocolHeader) {
      try {
        const deleteResponse = await fetchStep(mcpUrl, {
          method: "DELETE",
          headers: {
            authorization: `Bearer ${accessToken}`,
            origin: baseUrl.origin,
            "mcp-session-id": sessionId,
            "mcp-protocol-version": negotiatedProtocolHeader,
          },
        }, "SHUTDOWN", cleanupDeadline)
        await discardResponseBody(deleteResponse, "SHUTDOWN", "mcp_session")
        if (requireValidDelete && deleteResponse.status !== 204) {
          throw new ProbeFailure("SHUTDOWN", "mcp_session", `Session DELETE returned HTTP ${deleteResponse.status}`)
        }
      } catch (error) {
        if (requireValidDelete) throw error
      }
    }
    if (revocationEndpoint && accessToken) {
      try {
        const revocationResponse = await fetchStep(revocationEndpoint, {
          method: "POST",
          headers: { "content-type": "application/x-www-form-urlencoded" },
          body: new URLSearchParams({
            token: accessToken,
            client_id: revocationClientId,
            ...(revocationClientSecret ? { client_secret: revocationClientSecret } : {}),
          }),
        }, "SHUTDOWN", cleanupDeadline)
        await discardResponseBody(revocationResponse, "SHUTDOWN", "oauth_token")
      } catch {

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Make the server return HTTP 204 with no body for authenticated session DELETE requests.
  2. Check for middleware/proxies that intercept DELETE and return 405/200, and exempt the MCP endpoint.
  3. If you do not need strict teardown conformance, run the probe in a mode that does not require a valid DELETE so cleanup failures are non-fatal.
  4. Ensure the mcp-session-id header sent on DELETE matches the session established during initialize (no stale session reuse).

Example fix

// before (server route)
app.delete(endpointPath, (req, res) => { sessions.delete(req.headers["mcp-session-id"]); res.sendStatus(200) })
// after
app.delete(endpointPath, (req, res) => { sessions.delete(req.headers["mcp-session-id"]); res.sendStatus(204) })
Defensive patterns

Strategy: try-catch

Try / catch

try {
  const result = await probeEnterpriseMcpMockServer({ baseUrl, mode: "fixture-conformance" })
} catch (e) {
  if (e instanceof ProbeFailure && e.phase === "SHUTDOWN") {
    // Session teardown issue; primary phases may still have passed.
    console.warn("Non-fatal shutdown issue:", e.message)
  } else throw e
}

Prevention

When it happens

Trigger: Running the probe in a mode where valid session teardown is required and the server responds to DELETE /<endpointPath> with mcp-session-id and Bearer token using a status like 200, 404 (session already gone), or 405 instead of 204.

Common situations: Targeting a non-conformant or older MCP server that returns 200 on session delete, a gateway/proxy stripping the DELETE or rewriting it, or a server that already expired the session so DELETE returns 404.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/1aebd2dc32a565ae. Report an issue: GitHub.