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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions