Skip to content

Critical bug: Request/Response state leaks across requests causing persistent status codes and redirects #88

@3m1n3nc3

Description

@3m1n3nc3

Description

There is a critical issue in H3ravel where HTTP request and response state persist across multiple requests, leading to incorrect behaviour until the dev server is restarted.

Example Scenario

  1. User submits a form with validation.
  2. Validation fails → user is redirected back (expected).
  3. User corrects the form and submits again.
  4. Unexpected behaviour: user is still redirected back indefinitely.

Other observed effects:

  • If the first request returns a 200 or 201, all subsequent responses return the same status.
  • If the first request hits a 404, all subsequent requests (even to valid routes) return 404.
  • This persists until the dev server is restarted.

Impact

  • Makes form validation unusable
  • Breaks routing correctness
  • Completely invalidates HTTP lifecycle guarantees
  • Forces server restarts during development

This is a blocking bug for real-world usage.


Root Cause (Suspected)

The issue appears to stem from request, response, and context reuse across requests, specifically:

  • HttpContext is cached on the H3Event and reused:

    if (!fresh && (event as any)._h3ravelContext)
        return (event as any)._h3ravelContext
  • this.h3Event, this.context, and container bindings (http.request, http.response) are overwritten and reused globally.

  • The response object (or its prepared state) leaks into subsequent requests.

  • Kernel termination does not reset or fully isolate per-request state.

This violates the expectation that every HTTP request must have a fresh request, response, and context lifecycle.


Relevant Code

framework/core/Application.ts

this.h3App?.all('/**', async (event) => {
    this.context = (event) => this.buildContext(event, config)
    this.h3Event = event

    const context = await this.context!(event)

    const kernel = this.make(IKernel)

    this.bind('http.context', () => context)
    this.bind('http.request', () => context.request)
    this.bind('http.response', () => context.response)

    const response = await kernel.handle(context.request)

    if (response) this.bind('http.response', () => response)

    kernel.terminate(context.request, response!)
})
if (!fresh && (event as any)._h3ravelContext)
    return (event as any)._h3ravelContext

Expected Behavior

  • Every HTTP request must:

    • Receive a fresh Request instance
    • Receive a fresh Response instance
    • Have an isolated HttpContext
  • Response status, headers, and body must never leak into subsequent requests.

  • Validation failures must not affect future requests once corrected.


Tasks

  • Identify all points where request, response, or context is cached or reused.

  • Ensure a new Response instance is created per request.

  • Ensure container bindings for http.request and http.response are request-scoped, not global.

  • Prevent _h3ravelContext from persisting across unrelated requests.

  • Review kernel lifecycle and termination cleanup.

  • Add regression tests covering:

    • Validation failure → success
    • 404 followed by valid route
    • Mixed response status codes across requests

Acceptance Criteria

  • Each request has an isolated lifecycle.
  • Status codes do not persist across requests.
  • Redirects stop once validation passes.
  • No server restart required to reset state.
  • Regression tests reproduce and confirm the fix.

Difficulty

🔴 High (Core lifecycle bug)

Metadata

Metadata

Assignees

No one assigned

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions