Skip to content

feat: add request timeout middleware with configurable limits - #36

Merged
AnkanMisra merged 8 commits into
AnkanMisra:mainfrom
SujalTripathi:add-timeout-middleware
Jan 5, 2026
Merged

AnkanMisra merged 8 commits into
AnkanMisra:mainfrom
SujalTripathi:add-timeout-middleware

Conversation

@SujalTripathi

@SujalTripathi SujalTripathi commented Jan 4, 2026 •

Copy link
Copy Markdown
Contributor

Description

Implements request timeout middleware to prevent slow/hanging requests from consuming server resources as specified in issue #30.

Changes Made

  • ✅ Created RequestTimeoutMiddleware with context-based cancellation
  • ✅ Added per-endpoint timeout configuration (global: 60s, AI: 30s, verifier: 2s, health: 2s)
  • ✅ Updated callOpenRouter to accept context.Context and use http.NewRequestWithContext
  • ✅ Updated verifier calls to use context timeout with proper deadline handling
  • ✅ Added environment variables: REQUEST_TIMEOUT_SECONDS, AI_REQUEST_TIMEOUT_SECONDS, VERIFIER_TIMEOUT_SECONDS, HEALTH_CHECK_TIMEOUT_SECONDS
  • ✅ Returns HTTP 504 Gateway Timeout when deadline exceeded
  • ✅ Proper goroutine cleanup (no leaks) with buffered channels
  • ✅ Comprehensive unit tests for middleware and timeout behavior
  • ✅ Integration-style tests with slow mock services

Note

Unable to run tests locally due to environment constraints.
CI pipeline will validate the implementation.

Files Changed

Added:

  • gateway/middleware.go - Request timeout middleware implementation
  • gateway/config.go - Timeout configuration helpers
  • gateway/timeout_test.go - Comprehensive timeout tests

Modified:

  • gateway/main.go - Applied middleware to routes, updated service calls
  • gateway/config_test.go - Added timeout config tests
  • .env.example - Added timeout environment variables

Environment Configuration

REQUEST_TIMEOUT_SECONDS=60
AI_REQUEST_TIMEOUT_SECONDS=30
VERIFIER_TIMEOUT_SECONDS=2
HEALTH_CHECK_TIMEOUT_SECONDS=2


Closes #30



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Configurable timeouts (request, AI, verifier, health check) via environment variables with sensible defaults.
  * Per-request timeout middleware that buffers responses and returns HTTP 504 on timeout while preserving fast-path responses and panic recovery.
  * Downstream AI/external calls now honor request context timeouts so AI timeouts propagate as 504 responses.

* **Chores**
  * Example env file updated with new timeout variables and descriptions.
  * CI workflow expression and .gitignore tweaks.

* **Tests**
  * Added tests for middleware timeouts, AI/external call timeouts, fast-path responses, and panic recovery.

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- greptile_comment -->

<h3>Greptile Summary</h3>


This PR implements request timeout middleware to prevent slow/hanging requests from consuming server resources. The implementation uses context-based cancellation with response buffering to safely handle timeouts.

## Key Changes

- **Middleware Implementation** (`gateway/middleware.go`): Created `RequestTimeoutMiddleware` with sophisticated response buffering using `bufferedWriter` and `responseWriterShim`. The middleware properly handles concurrent writes by buffering responses and only flushing successful responses before timeout.

- **Context Propagation** (`gateway/main.go`): Updated `callOpenRouter` and verifier calls to accept `context.Context` and use `http.NewRequestWithContext`, ensuring timeouts propagate properly to external service calls. Added proper error handling to distinguish timeout errors from other failures.

- **Configuration** (`gateway/config.go`): Added timeout configuration helpers with sensible defaults (global: 60s, AI: 30s, verifier: 2s, health: 2s) and validation for non-positive values.

- **Layered Timeout Strategy**: Applied global timeout middleware (60s) with per-route overrides for health checks (2s) and AI endpoints (30s). The middleware intelligently preserves the earliest deadline when nested timeouts are present.

## Test Coverage

Comprehensive test suite covering:
- Middleware timeout behavior with slow handlers
- Context timeout propagation to external services (`callOpenRouter`, verifier)
- Fast-path success cases
- Panic recovery integration with Gin's Recovery middleware
- Configuration helper validation

## Implementation Quality

The implementation demonstrates strong engineering practices:
- **Concurrency Safety**: Proper mutex protection for bufferedWriter with separate read/write locks
- **Panic Recovery**: Preserves Gin's panic recovery by catching panics in the handler goroutine and re-panicking after restoring the original writer
- **No Goroutine Leaks**: Uses buffered channel for `finished` signal to prevent goroutine blocking
- **Clean Error Handling**: Returns 504 Gateway Timeout consistently for all timeout scenarios (middleware, verifier, AI service)

## Minor Observations

- The `Header()` method returns the map reference directly while holding a read lock, which doesn't prevent modification after the lock is released. However, given the single-handler-goroutine usage pattern, this is safe in practice.

<h3>Confidence Score: 5/5</h3>


- This PR is safe to merge with high confidence
- Score reflects excellent implementation quality with proper concurrency handling, comprehensive test coverage, and thorough review feedback incorporation. The timeout middleware is well-designed with response buffering to prevent race conditions, proper context propagation to external services, and clean error handling. Only minor advisory comment about Header() method, which doesn't impact safety given current usage patterns.
- No files require special attention - all implementations are production-ready

<h3>Important Files Changed</h3>




| Filename | Overview |
|----------|----------|
| gateway/middleware.go | Added comprehensive timeout middleware with response buffering, mutex protection, and panic recovery. One potential race condition in Header() method (returns map directly while holding read lock). |
| gateway/config.go | Added timeout configuration helpers with sensible defaults and validation for non-positive values. |
| gateway/main.go | Integrated timeout middleware at global and per-route levels, updated callOpenRouter and verifier calls to accept context and handle timeouts properly. |
| gateway/timeout_test.go | Added comprehensive timeout tests covering middleware timeout behavior, context propagation to external services, and proper 504 responses. |
| gateway/config_test.go | Added tests for timeout configuration helpers, covering defaults, custom values, and fallback behavior for non-positive values. |

</details>



<h3>Sequence Diagram</h3>

```mermaid
sequenceDiagram
    participant Client
    participant TimeoutMiddleware
    participant Handler
    participant Verifier
    participant OpenRouter

    Client->>TimeoutMiddleware: POST /api/ai/summarize
    TimeoutMiddleware->>TimeoutMiddleware: Create context with timeout (30s for AI)
    TimeoutMiddleware->>TimeoutMiddleware: Create bufferedWriter
    TimeoutMiddleware->>Handler: c.Next() in goroutine
    
    alt Fast Path (Handler completes before timeout)
        Handler->>Verifier: POST /verify (2s timeout)
        Verifier-->>Handler: Valid signature
        Handler->>OpenRouter: POST /chat/completions (30s timeout)
        OpenRouter-->>Handler: AI response
        Handler->>TimeoutMiddleware: Write to bufferedWriter
        TimeoutMiddleware->>TimeoutMiddleware: Flush buffer to client
        TimeoutMiddleware-->>Client: 200 OK with AI response
    else Timeout Path (Handler exceeds deadline)
        Handler->>Verifier: POST /verify
        Verifier-->>Handler: Valid signature
        Handler->>OpenRouter: POST /chat/completions (slow)
        TimeoutMiddleware->>TimeoutMiddleware: Context deadline exceeded
        TimeoutMiddleware->>TimeoutMiddleware: Mark buffer closed
        TimeoutMiddleware-->>Client: 504 Gateway Timeout
        Note over Handler,OpenRouter: Handler may still be running but writes are discarded
    else Verifier Timeout
        Handler->>Verifier: POST /verify (2s timeout)
        Note over Verifier: No response within 2s
        Handler->>Handler: Detect context.DeadlineExceeded
        Handler->>TimeoutMiddleware: Write 504 to bufferedWriter
        TimeoutMiddleware->>TimeoutMiddleware: Flush buffer to client
        TimeoutMiddleware-->>Client: 504 Gateway Timeout (Verifier)
    end

- Implement RequestTimeoutMiddleware with context-based cancellation
- Add per-endpoint timeout configuration (AI: 30s, verifier: 2s, health: 2s, global: 60s)
- Update callOpenRouter to accept context and use http.NewRequestWithContext
- Update verifier calls to use context timeout with proper cancellation
- Add environment variables for timeout configuration
- Add comprehensive unit tests including timeout scenarios
- Return 504 Gateway Timeout when deadline exceeded
- Ensure no goroutine leaks with proper channel handling

Fixes AnkanMisra#30
Copilot AI review requested due to automatic review settings January 4, 2026 17:08
@coderabbitai

coderabbitai Bot commented Jan 4, 2026 •

Copy link
Copy Markdown
Contributor

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds configurable per-request timeouts: new env vars and timeout getters, a Gin middleware that buffers and enforces per-route deadlines, context-aware verifier and OpenRouter HTTP calls (including signature change for callOpenRouter), and tests validating timeout propagation and middleware behavior.

Changes

Cohort / File(s) Summary
Configuration & Environment
\.env.example
Adds timeout env vars and comments: REQUEST_TIMEOUT_SECONDS=60, AI_REQUEST_TIMEOUT_SECONDS=30, VERIFIER_TIMEOUT_SECONDS=2, HEALTH_CHECK_TIMEOUT_SECONDS=2.
Timeout Helpers
gateway/config.go
Adds getPositiveTimeout(envKey string, defaultSeconds int) time.Duration and helpers: getRequestTimeout(), getAITimeout(), getVerifierTimeout(), getHealthCheckTimeout().
Timeout Helpers Tests
gateway/config_test.go
Adds TestTimeoutConfigHelpers to assert default and env-driven timeout values; imports time.
Request Timeout Middleware
gateway/middleware.go
New RequestTimeoutMiddleware(timeout time.Duration) with bufferedWriter/responseWriterShim: runs handler in goroutine, buffers headers/body, enforces deadline, flushes on success, returns 504 JSON on timeout, preserves panic recovery and supports Flush/Hijack/CloseNotify/Pusher where available.
Gateway Routing & Service Calls
gateway/main.go
Applies middleware globally and per-route (/healthz, /api/ai group), changes callOpenRouter to callOpenRouter(ctx context.Context, text string) (string, error), uses context-aware http.NewRequestWithContext for verifier/OpenRouter, and maps context.DeadlineExceeded to 504 responses.
Timeout Tests
gateway/timeout_test.go, gateway/timeout_extra_test.go
Adds tests: middleware aborts on timeout, callOpenRouter respects context deadlines, summarize flow returns 504 on AI timeout, fast handler succeeds, and panic-preservation under middleware.
Misc
.gitignore, .github/workflows/pr-branch-check.yml, other formatting changes
Adds gateway/*.exe ignore; workflow condition changed to expression syntax; several files have whitespace/formatting tweaks with no logic changes.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Gateway as Gateway\n(Middleware)
    participant Handler as Handler
    participant Verifier as Verifier Service
    participant OpenRouter as OpenRouter Service

    autonumber

    Client->>Gateway: POST /api/ai/summarize
    rect rgb(220,235,255)
      Note over Gateway: RequestTimeoutMiddleware sets ctx\n(global or AI-specific) and buffers response
      Gateway->>Handler: Invoke handler with ctx (buffered)
    end

    rect rgb(240,250,230)
      Note over Handler: Handler calls verifier then AI\nusing context-aware HTTP requests
      Handler->>Verifier: HTTP POST with verifier ctx (~2s)
      alt Verifier responds
        Verifier-->>Handler: verification success
      else Verifier timeout
        Verifier--xHandler: context.DeadlineExceeded
        Handler-->>Gateway: return 504 (verifier timeout)
        Gateway-->>Client: 504 Gateway Timeout
      end
    end

    alt Verifier success
      Handler->>OpenRouter: HTTP POST with AI ctx (~30s)
      alt OpenRouter responds
        OpenRouter-->>Handler: AI response
        Handler-->>Gateway: 200 OK + body
        Gateway-->>Client: 200 OK (flush buffer)
      else OpenRouter timeout
        OpenRouter--xHandler: context.DeadlineExceeded
        Handler-->>Gateway: 504 Gateway Timeout
        Gateway-->>Client: 504 Gateway Timeout
      end
    end

    rect rgb(250,220,220)
      Note over Gateway: If middleware deadline exceeded,\nmiddleware aborts, discards buffered output, and returns 504
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • AnkanMisra

Poem

🐰 I hopped through code to set timers tight,
I buffered answers while the clocks took flight.
When services dawdle or responses lag,
I thump a timeout, tidy up the sag.
Hop, nibble, flush — now requests stay light.

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes out-of-scope changes unrelated to timeout middleware: .gitignore modifications for Windows executables (gateway/*.exe) and a GitHub Actions workflow syntax update (if: failure() to if: ${{ failure() }}), neither of which relates to the timeout implementation objectives. Remove the .gitignore and .github/workflows/pr-branch-check.yml changes from this PR; include them in a separate, focused pull request to maintain scope clarity.
Docstring Coverage ⚠️ Warning Docstring coverage is 54.29% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add request timeout middleware with configurable limits' accurately summarizes the main change—implementing request timeout middleware with environment-configurable timeout values across different endpoints.
Linked Issues check ✅ Passed All primary coding requirements from issue #30 are met: global timeout middleware (60s default), per-endpoint configuration (AI 30s, verifier 2s, health checks 2s), context-based timeouts for service calls, 504 responses on timeout, environment variable configuration, and unit/integration tests.
✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps greptile-apps Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Comments (4)

  1. gateway/middleware.go, line 34-48 (link)

    logic: Race condition: the middleware can return with c.AbortWithStatusJSON(504, ...) while the handler goroutine (line 30: c.Next()) is still writing to the response. Gin's Context is not goroutine-safe. If the handler writes after line 44 executes, you'll get concurrent writes to the same HTTP response.

    Gin doesn't provide a built-in way to safely abort handlers running in goroutines. The standard pattern is to let the handler check ctx.Done() and return early, rather than racing to write the response.

  2. gateway/main.go, line 143-150 (link)

    logic: Double timeout middleware creates redundancy: requests to /api/ai/summarize pass through both the global timeout (60s) and the AI-specific timeout (30s). The tighter 30s limit applies, but this creates two nested timeout contexts and two concurrent goroutines waiting on the same request.

    If the intention is to have AI endpoints use 30s instead of 60s, remove the global middleware from AI routes. If both are needed for different purposes, clarify the design. Should AI endpoints have both a 60s global timeout AND a 30s AI-specific timeout, or just the 30s timeout?

  3. gateway/main.go, line 146 (link)

    logic: Redundant timeout: health check gets both the global 60s timeout and a 2s timeout. The 2s limit applies but wastes resources with double context cancellation.

  4. gateway/timeout_test.go, line 19-22 (link)

    style: Test doesn't prove timeout middleware works correctly because it assumes the handler goroutine stops executing. With the race condition in the middleware, the handler continues after timeout and could write to c.JSON(200, ...) on line 22, causing concurrent response writes.

6 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
gateway/main.go (1)

168-176: Consider renaming for consistency with the fix at line 179.

While this context variable is scoped within the if-block and benign (the function returns before any context. package usage), renaming it to payCtx (or similar) alongside the fix at line 179 would improve consistency and avoid future confusion.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f0df006 and 181f841.

📒 Files selected for processing (6)
  • .env.example
  • gateway/config.go
  • gateway/config_test.go
  • gateway/main.go
  • gateway/middleware.go
  • gateway/timeout_test.go
🧰 Additional context used
🧬 Code graph analysis (2)
gateway/timeout_test.go (1)
gateway/middleware.go (1)
  • RequestTimeoutMiddleware (13-51)
gateway/main.go (1)
gateway/middleware.go (1)
  • RequestTimeoutMiddleware (13-51)
🪛 dotenv-linter (4.0.0)
.env.example

[warning] 48-48: [UnorderedKey] The AI_REQUEST_TIMEOUT_SECONDS key should go before the REQUEST_TIMEOUT_SECONDS key

(UnorderedKey)


[warning] 52-52: [UnorderedKey] The HEALTH_CHECK_TIMEOUT_SECONDS key should go before the REQUEST_TIMEOUT_SECONDS key

(UnorderedKey)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Agent
  • GitHub Check: Greptile Review
🔇 Additional comments (7)
.env.example (1)

44-52: LGTM! Timeout configuration is well-documented.

The timeout environment variables are clearly documented with sensible defaults. The static analysis warnings about key ordering are stylistic preferences from dotenv-linter—grouping related timeout configurations together (global, AI-specific, verifier, health check) is more readable than strict alphabetical ordering.

gateway/config.go (1)

1-24: LGTM! Clean and consistent timeout configuration helpers.

The timeout helper functions follow a clear, consistent pattern with sensible defaults that match the documented values in .env.example. The implementation is straightforward and correct.

gateway/middleware.go (1)

13-51: LGTM! Solid timeout middleware implementation with proper goroutine management.

The middleware correctly uses context-based timeouts with buffered channels to prevent goroutine leaks. The pattern of running handlers in a separate goroutine while waiting on the context's Done() channel is the standard approach for timeout middleware in Go.

Key strengths:

  • Buffered channels (size 1) ensure no goroutine blocks indefinitely
  • Deferred cancel() ensures proper cleanup
  • Panic recovery and re-panic preserves Gin's default behavior
  • Clear 504 response on timeout

Note: The handler goroutine continues executing after timeout (acknowledged in your comment on lines 42-43). This is a known limitation of timeout middleware patterns—Gin's AbortWithStatusJSON prevents the handler from writing to an already-sent response, so this is acceptable.

gateway/timeout_test.go (2)

15-36: LGTM! Clear test of middleware timeout behavior.

The test correctly validates that the middleware aborts with a 504 status and appropriate error message when a handler exceeds the timeout.


38-63: LGTM! Effective test of context timeout propagation.

The test properly validates that callOpenRouter respects the context deadline by setting up a slow mock server and asserting a timeout error is returned.

gateway/main.go (2)

142-151: Timeout middleware stacking is intentional but worth noting.

The global timeout middleware (line 143) stacks with per-route timeouts (lines 146, 150). Since child contexts inherit parent deadlines, the effective timeout is min(global, route-specific). This works correctly when route-specific timeouts are shorter than the global (which they are: 2s health, 30s AI vs. 60s global).

If requirements change and a route needs a timeout longer than the global, the global would still cap it. Consider whether to remove the global middleware and rely solely on per-route timeouts, or document this constraint.


325-329: Context deadline check is correctly implemented here.

Unlike handleSummarize, this function doesn't shadow the context package, so context.DeadlineExceeded correctly references the standard library constant. The timeout detection logic is sound.

Comment thread gateway/config_test.go Resolved
Comment thread gateway/main.go Resolved
Comment thread gateway/main.go Resolved
Comment thread gateway/main.go Resolved
Comment thread gateway/main.go Resolved
Comment thread gateway/timeout_test.go Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements request timeout middleware to prevent slow or hanging requests from consuming server resources. It introduces context-based timeout management with configurable limits for different endpoint types.

Key Changes:

  • Added RequestTimeoutMiddleware with goroutine-based timeout detection and context propagation
  • Configured per-endpoint timeouts: global (60s), AI endpoints (30s), verifier (2s), and health checks (2s)
  • Updated callOpenRouter and verifier calls to respect context deadlines

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
gateway/middleware.go Implements timeout middleware with context cancellation, goroutine monitoring, and 504 response handling
gateway/config.go Provides timeout configuration helper functions that read from environment variables
gateway/main.go Applies timeout middleware to routes, updates service calls to use context-based timeouts, and handles timeout errors
gateway/timeout_test.go Adds comprehensive tests for middleware timeout behavior and AI request timeout scenarios
gateway/config_test.go Tests timeout configuration helper functions with default and custom values
.env.example Documents new timeout-related environment variables with default values

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread gateway/config_test.go Resolved
Comment thread gateway/timeout_test.go Resolved
Comment thread gateway/config.go Outdated
Comment on lines +7 to +22
seconds := getEnvAsInt("REQUEST_TIMEOUT_SECONDS", 60)
return time.Duration(seconds) * time.Second
}

func getAITimeout() time.Duration {
seconds := getEnvAsInt("AI_REQUEST_TIMEOUT_SECONDS", 30)
return time.Duration(seconds) * time.Second
}

func getVerifierTimeout() time.Duration {
seconds := getEnvAsInt("VERIFIER_TIMEOUT_SECONDS", 2)
return time.Duration(seconds) * time.Second
}

func getHealthCheckTimeout() time.Duration {
seconds := getEnvAsInt("HEALTH_CHECK_TIMEOUT_SECONDS", 2)

Copilot AI Jan 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout helper functions don't validate that the configured values are positive. If someone sets an environment variable like REQUEST_TIMEOUT_SECONDS=0 or a negative value, it will create a zero or negative duration timeout. While context.WithTimeout will treat zero/negative durations as "already expired," this could lead to confusing behavior where all requests immediately timeout. Consider adding validation to ensure timeout values are positive, or document the minimum acceptable values.

Suggested change
seconds := getEnvAsInt("REQUEST_TIMEOUT_SECONDS", 60)
return time.Duration(seconds) * time.Second
}
func getAITimeout() time.Duration {
seconds := getEnvAsInt("AI_REQUEST_TIMEOUT_SECONDS", 30)
return time.Duration(seconds) * time.Second
}
func getVerifierTimeout() time.Duration {
seconds := getEnvAsInt("VERIFIER_TIMEOUT_SECONDS", 2)
return time.Duration(seconds) * time.Second
}
func getHealthCheckTimeout() time.Duration {
seconds := getEnvAsInt("HEALTH_CHECK_TIMEOUT_SECONDS", 2)
seconds := getEnvAsInt("REQUEST_TIMEOUT_SECONDS", 60)
if seconds <= 0 {
seconds = 60
}
return time.Duration(seconds) * time.Second
}
func getAITimeout() time.Duration {
seconds := getEnvAsInt("AI_REQUEST_TIMEOUT_SECONDS", 30)
if seconds <= 0 {
seconds = 30
}
return time.Duration(seconds) * time.Second
}
func getVerifierTimeout() time.Duration {
seconds := getEnvAsInt("VERIFIER_TIMEOUT_SECONDS", 2)
if seconds <= 0 {
seconds = 2
}
return time.Duration(seconds) * time.Second
}
func getHealthCheckTimeout() time.Duration {
seconds := getEnvAsInt("HEALTH_CHECK_TIMEOUT_SECONDS", 2)
if seconds <= 0 {
seconds = 2
}

Copilot uses AI. Check for mistakes.
Comment thread gateway/main.go Outdated
summary, err := callOpenRouter(c.Request.Context(), req.Text)
if err != nil {
// If the error was due to a timeout, return 504
if strings.Contains(strings.ToLower(err.Error()), "timeout") || c.Request.Context().Err() == context.DeadlineExceeded {

Copilot AI Jan 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout error detection is checking both the context error AND using string matching on the error message. However, using string matching with "timeout" is fragile and could match unrelated errors. Since callOpenRouter explicitly returns an error when ctx.Err() == context.DeadlineExceeded (line 326-327), and the verifier check already validates verifierCtx.Err() (line 206), the string matching is redundant. Consider only checking c.Request.Context().Err() == context.DeadlineExceeded for consistency.

Suggested change
if strings.Contains(strings.ToLower(err.Error()), "timeout") || c.Request.Context().Err() == context.DeadlineExceeded {
if c.Request.Context().Err() == context.DeadlineExceeded {

Copilot uses AI. Check for mistakes.
Comment thread gateway/timeout_test.go Resolved
Comment thread gateway/main.go Outdated
client := &http.Client{}
resp, err := client.Do(vreq)
if err != nil {
if verifierCtx.Err() == context.DeadlineExceeded {

Copilot AI Jan 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The verifier timeout check only examines verifierCtx.Err() but doesn't consider that the parent context (c.Request.Context()) might have timed out first. If the parent middleware timeout fires before the verifier timeout, verifierCtx will be canceled but its Err() might be context.Canceled rather than context.DeadlineExceeded. This could result in returning a generic "Verification service unavailable" error instead of the more specific "Gateway Timeout" error. Consider also checking if c.Request.Context().Err() == context.DeadlineExceeded.

Suggested change
if verifierCtx.Err() == context.DeadlineExceeded {
if verifierCtx.Err() == context.DeadlineExceeded || c.Request.Context().Err() == context.DeadlineExceeded {

Copilot uses AI. Check for mistakes.
Comment thread gateway/timeout_test.go Outdated
t.Fatalf("Expected duration around 1s, got %v", dur)
}
}

Copilot AI Jan 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra blank line at the end of the file. Remove the trailing blank line.

Suggested change

Copilot uses AI. Check for mistakes.
Comment thread gateway/main.go Outdated
Comment on lines +142 to +151
// Global request timeout middleware
r.Use(RequestTimeoutMiddleware(getRequestTimeout()))

// Health check with shorter timeout
r.GET("/healthz", RequestTimeoutMiddleware(getHealthCheckTimeout()), handleHealth)

// AI endpoints with AI-specific timeout
aiGroup := r.Group("/api/ai")
aiGroup.Use(RequestTimeoutMiddleware(getAITimeout()))
aiGroup.POST("/summarize", handleSummarize)

Copilot AI Jan 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple timeout middlewares are layered on the same routes, creating unnecessary goroutines. The global timeout (60s) at line 143 applies to ALL routes including /healthz and /api/ai/*, which then apply their own shorter timeouts (2s and 30s respectively). While this works correctly because the shorter timeout fires first, it's inefficient as it creates multiple monitoring goroutines per request. Consider restructuring to apply the global timeout only to routes without specific timeouts, or remove the global timeout entirely if all routes have specific timeouts.

Copilot uses AI. Check for mistakes.
Comment thread gateway/timeout_test.go Outdated
}

// Ensure it timed out approximately around 1s (give small margin)
if dur < 900*time.Millisecond || dur > 3*time.Second {

Copilot AI Jan 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout duration check has an overly permissive upper bound of 3 seconds when the timeout is set to 1 second. This test is checking that the timeout fires around 1 second, but allows up to 3 seconds, which could hide bugs where the timeout doesn't work properly. Consider tightening the upper bound to something like 1.5 seconds (1500ms) to catch regressions more reliably.

Suggested change
if dur < 900*time.Millisecond || dur > 3*time.Second {
if dur < 900*time.Millisecond || dur > 1500*time.Millisecond {

Copilot uses AI. Check for mistakes.
Comment thread gateway/timeout_test.go Outdated
t.Fatalf("Expected duration around 1s, got %v", dur)
}
}

Copilot AI Jan 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no test case that verifies the middleware allows successful requests (that complete within the timeout) to proceed normally. All test cases focus on timeout scenarios. Consider adding a test that verifies a fast handler completes successfully and returns 200, ensuring the middleware doesn't interfere with normal operation.

Copilot uses AI. Check for mistakes.
@AnkanMisra

Copy link
Copy Markdown
Owner

@greptile review the pr and find all the issues

@greptile-apps greptile-apps Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Comments (4)

  1. gateway/main.go, line 203 (link)

    logic: HTTP client should have timeout configured - &http.Client{} has no timeout, so even with context timeout, the underlying TCP connection can hang indefinitely

  2. gateway/main.go, line 322 (link)

    logic: HTTP client should have timeout configured - &http.Client{} has no timeout, so even with context timeout, the underlying TCP connection can hang indefinitely

  3. gateway/middleware.go, line 34-49 (link)

    logic: After timeout (line 44), the handler goroutine continues running and may try to write to the response after AbortWithStatusJSON already sent 504. This creates a race condition where the handler might write headers/body after the response is complete.

    The buffered channels prevent goroutine leaks, but there's no mechanism to stop the handler from writing to c after timeout. Consider checking c.IsAborted() in handlers or using c.Writer.Written() checks. Does your application handle the case where handlers attempt to write after AbortWithStatusJSON, or do handlers check c.IsAborted() before writing?

  4. gateway/main.go, line 143 (link)

    style: Global timeout middleware applies to ALL routes including /docs and /openapi.yaml. Static file serving and documentation don't need 60s timeouts and could interfere with slow downloads.

6 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

@AnkanMisra

AnkanMisra commented Jan 4, 2026 •

Copy link
Copy Markdown
Owner

@SujalTripathi
You know the drill, crazy amount of bugs and bad code with failing test cases

Please Read all the docs, contributor, how to run test and other docs which are pretty well written

This is a medium problem don't vibe code in 1 hour

@SujalTripathi

Copy link
Copy Markdown
Contributor Author

@greptile give the changes have to do now

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
gateway/timeout_test.go (1)

151-151: Remove trailing blank line.

🔎 Proposed fix
 		t.Fatalf("Handler response leaked after timeout: %s", w.Body.String())
 	}
 }
-
🧹 Nitpick comments (1)
gateway/timeout_test.go (1)

114-135: Consider extracting inline tests into separate test functions.

The fast handler test (lines 116-124) and panic recovery test (lines 127-135) are embedded within TestHandleSummarize_AIRequestTimeoutReturns504. While they test related middleware behavior, extracting them into dedicated functions (e.g., TestRequestTimeoutMiddleware_AllowsFastHandlers and TestRequestTimeoutMiddleware_PreservesPanicRecovery) would improve test organization and make failures easier to diagnose.

🔎 Proposed structure

Create two separate test functions:

func TestRequestTimeoutMiddleware_AllowsFastHandlers(t *testing.T) {
	gin.SetMode(gin.TestMode)
	r := gin.New()
	r.Use(RequestTimeoutMiddleware(1 * time.Second))
	r.GET("/fast", func(c *gin.Context) { c.JSON(200, gin.H{"ok": true}) })
	
	req, _ := http.NewRequest("GET", "/fast", nil)
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	
	if w.Code != 200 {
		t.Fatalf("Expected 200 for fast handler, got %d", w.Code)
	}
}

func TestRequestTimeoutMiddleware_PreservesPanicRecovery(t *testing.T) {
	gin.SetMode(gin.TestMode)
	r := gin.Default() // includes Recovery middleware
	r.Use(RequestTimeoutMiddleware(1 * time.Second))
	r.GET("/panic", func(c *gin.Context) { panic("boom") })
	
	req, _ := http.NewRequest("GET", "/panic", nil)
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	
	if w.Code != 500 {
		t.Fatalf("Expected 500 from panic + recovery, got %d", w.Code)
	}
}

Then remove lines 114-135 from the current test.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 181f841 and f419c02.

📒 Files selected for processing (5)
  • gateway/config.go
  • gateway/config_test.go
  • gateway/main.go
  • gateway/middleware.go
  • gateway/timeout_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • gateway/config.go
🧰 Additional context used
🧬 Code graph analysis (2)
gateway/timeout_test.go (1)
gateway/middleware.go (1)
  • RequestTimeoutMiddleware (81-127)
gateway/main.go (2)
gateway/middleware.go (1)
  • RequestTimeoutMiddleware (81-127)
src/services/payment.ts (1)
  • PaymentContext (8-14)
🔇 Additional comments (5)
gateway/config_test.go (1)

26-65: LGTM! Comprehensive timeout configuration testing.

The test properly validates default timeouts, custom environment variable overrides, and fallback behavior for non-positive values. The addition of getHealthCheckTimeout() coverage addresses the previous review feedback.

gateway/middleware.go (1)

81-127: Solid timeout implementation with proper buffering and cleanup.

The middleware correctly:

  • Runs handlers in a separate goroutine with panic recovery
  • Buffers responses to avoid write races when timeouts occur
  • Uses a buffered panic channel to prevent goroutine blocking
  • Returns 504 with a clean JSON response on timeout while discarding the handler's buffered output

The context propagation (line 88) ensures downstream handlers can detect cancellation and stop work appropriately.

gateway/main.go (3)

146-152: Clean per-route timeout configuration.

The health check and AI endpoints now have appropriate timeouts applied (2s and 30s respectively). The absence of a global timeout middleware avoids the inefficiency of layered timeout goroutines mentioned in previous reviews.


206-211: Proper error handling and timeout detection for verifier calls.

The fixes address critical issues from previous reviews:

  • Error from http.NewRequestWithContext is now properly checked (lines 206-211)
  • Timeout detection checks both the verifier context and parent request context (line 217), handling the case where the parent middleware timeout fires first

Also applies to: 217-220


329-332: Context-aware OpenRouter implementation with proper error handling.

The callOpenRouter function now correctly:

  • Accepts a context parameter for cancellation support
  • Handles request creation errors (lines 329-332)
  • Returns context.DeadlineExceeded when the context times out (lines 340-342), enabling callers to distinguish timeout errors from other failures

Also applies to: 340-342

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the PR, you've implemented a comprehensive timeout middleware solution, but there are a few critical issues that need to be addressed before this can be merged:

Critical Issues to Fix:

1. HTTP Client Timeout Configuration

The most critical issue is that your HTTP clients are created without timeout configuration:

In gateway/main.go lines 213 and 336:

client := &http.Client{}  // No timeout configured!

This means even with your context timeouts, the underlying TCP connections can still hang indefinitely. You need to configure the HTTP client with a timeout:

client := &http.Client{
    Timeout: 30 * time.Second, // or use the context timeout value
}

2. Redundant Timeout Middleware Layering

You're applying timeout middleware at both the global level and per-endpoint level, which creates confusion about which timeout actually applies. In main.go, you have:

  • Health endpoint: RequestTimeoutMiddleware(getHealthCheckTimeout()) (2s)
  • AI group: RequestTimeoutMiddleware(getAITimeout()) (30s)

But there's no global timeout middleware applied, so this is actually fine. However, the architecture suggests you intended to have layered timeouts.

3. Minor: Error Handling Consistency

In the verifier timeout handling, you check both the verifier context and the parent request context for deadline exceeded. This is redundant since the verifier context is derived from the parent context.

Recommended Changes:

  1. Fix HTTP client timeouts (most important)
  2. Consider simplifying the timeout architecture - either use global + per-endpoint or just per-endpoint
  3. Add HTTP client timeout configuration to prevent hanging connections

The middleware implementation itself is solid with good goroutine management and panic handling. The main issue is that the HTTP clients bypass your context timeouts entirely.

… implement response writer methods; ensure panic and timeout behavior
@SujalTripathi

Copy link
Copy Markdown
Contributor Author

@greptile @CodeRabbit full review and give changes have to do here

@coderabbitai

coderabbitai Bot commented Jan 4, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch all the relevant files for review
echo "=== gateway/middleware.go ==="
cat gateway/middleware.go
echo -e "\n=== gateway/config.go ==="
cat gateway/config.go
echo -e "\n=== gateway/main.go ==="
cat gateway/main.go
echo -e "\n=== gateway/timeout_test.go ==="
cat gateway/timeout_test.go
echo -e "\n=== gateway/config_test.go ==="
cat gateway/config_test.go
echo -e "\n=== .env.example ==="
cat .env.example

Length of output: 31677


@SujalTripathi - I've completed a full review of PR #36. Here are the critical bugs and issues that need to be fixed:

🚨 Critical Issues

1. Missing Global Timeout Middleware

Location: gateway/main.go, main() function
Problem: The PR objectives state "Add a global request timeout middleware (default 60s)" but NO global timeout is applied to the router. Only specific routes (health, AI) have timeouts.

Fix Required:

r := gin.Default()

// Add global timeout middleware BEFORE other routes
r.Use(RequestTimeoutMiddleware(getRequestTimeout()))

// Then add specific route groups
r.GET("/healthz", RequestTimeoutMiddleware(getHealthCheckTimeout()), handleHealth)

2. Code Indentation Error

Location: gateway/middleware.go, lines 95-96
Problem: Incorrect indentation breaks code structure.

Current (broken):

	origWriter := c.Writer
	bw := newBufferedWriter()
	// replace the gin writer with a shim that uses bw and keeps orig writer
	c.Writer = &responseWriterShim{bw: bw, orig: origWriter}
	finished := make(chan struct{})
	panicChan := make(chan interface{}, 1)

Should be:

	origWriter := c.Writer
	bw := newBufferedWriter()
	// replace the gin writer with a shim that uses bw and keeps orig writer
	c.Writer = &responseWriterShim{bw: bw, orig: origWriter}
	finished := make(chan struct{})
	panicChan := make(chan interface{}, 1)

3. Redundant and Incorrect Timeout Logic

Location: gateway/main.go, handleSummarize() and callOpenRouter()
Problem: Setting BOTH context.WithTimeout AND http.Client.Timeout is redundant and can cause confusing behavior. The code also has overly complex deadline calculation.

Current (broken) in handleSummarize:

verifierCtx, verifierCancel := context.WithTimeout(c.Request.Context(), getVerifierTimeout())
defer verifierCancel()

vreq, err := http.NewRequestWithContext(verifierCtx, "POST", verifierURL+"/verify", bytes.NewBuffer(verifyBody))
// ... then later calculates deadline and creates client with timeout
if deadline, ok := verifierCtx.Deadline(); ok {
    remaining := time.Until(deadline)
    if remaining <= 0 {
        c.JSON(504, gin.H{"error": "Gateway Timeout", "message": "Verifier request timed out"})
        return
    }
    client := &http.Client{Timeout: remaining}
    resp, err = client.Do(vreq)
}

Fix - Simplify to:

verifierCtx, verifierCancel := context.WithTimeout(c.Request.Context(), getVerifierTimeout())
defer verifierCancel()

vreq, err := http.NewRequestWithContext(verifierCtx, "POST", verifierURL+"/verify", bytes.NewBuffer(verifyBody))
if err != nil {
    c.JSON(500, gin.H{"error": "Invalid verifier request", "details": err.Error()})
    return
}
vreq.Header.Set("Content-Type", "application/json")

// Use http.DefaultClient (or create one without timeout - context handles it)
resp, err := http.DefaultClient.Do(vreq)
if err != nil {
    if errors.Is(err, context.DeadlineExceeded) {
        c.JSON(504, gin.H{"error": "Gateway Timeout", "message": "Verifier request timed out"})
        return
    }
    c.JSON(500, gin.H{"error": "Verification service unavailable"})
    return
}
defer resp.Body.Close()

Same issue in callOpenRouter - remove the complex deadline logic:

req, err := http.NewRequestWithContext(ctx, "POST", openRouterURL, bytes.NewBuffer(reqBody))
if err != nil {
    return "", fmt.Errorf("failed to create OpenRouter request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")

// Just use DefaultClient - context already has timeout
resp, err := http.DefaultClient.Do(req)
if err != nil {
    if errors.Is(err, context.DeadlineExceeded) {
        return "", context.DeadlineExceeded
    }
    return "", err
}
defer resp.Body.Close()

4. Test Structure Violations

Location: gateway/timeout_test.go, TestHandleSummarize_AIRequestTimeoutReturns504
Problem: Multiple unrelated test cases are mixed into one test function. This makes debugging harder and violates testing best practices.

Lines 85-100 test fast handler and panic recovery - these should be separate test functions:

Fix - Split into:

func TestRequestTimeoutMiddleware_AllowsFastHandlers(t *testing.T) {
    gin.SetMode(gin.TestMode)
    r := gin.New()
    r.Use(RequestTimeoutMiddleware(1 * time.Second))
    r.GET("/fast", func(c *gin.Context) { c.JSON(200, gin.H{"ok": true}) })
    
    req, _ := http.NewRequest("GET", "/fast", nil)
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)
    
    if w.Code != 200 {
        t.Fatalf("Expected 200 for fast handler, got %d", w.Code)
    }
}

func TestRequestTimeoutMiddleware_PreservesPanicRecovery(t *testing.T) {
    gin.SetMode(gin.TestMode)
    r := gin.Default() // Uses Recovery middleware
    r.Use(RequestTimeoutMiddleware(1 * time.Second))
    r.GET("/panic", func(c *gin.Context) { panic("boom") })
    
    req, _ := http.NewRequest("GET", "/panic", nil)
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)
    
    if w.Code != 500 {
        t.Fatalf("Expected 500 from panic + recovery, got %d", w.Code)
    }
}

Remove lines 82-98 from TestHandleSummarize_AIRequestTimeoutReturns504.

5. Fragile File Path

Location: gateway/main.go, line 56
Problem: godotenv.Load("../.env") assumes specific directory structure. Will break if running from different directory or in production.

Fix:

err := godotenv.Load(".env") // Look in current directory
if err != nil {
    // Try parent directory as fallback
    err = godotenv.Load("../.env")
    if err != nil {
        log.Println("Warning: Error loading .env file")
    }
}

Or better - use .env in current directory and document that tests should be run from the gateway/ directory.

⚠️ Additional Issues

6. Timing Assumptions in Tests

Location: gateway/timeout_test.go, line 99
Problem: Hardcoded timing check 900ms to 1500ms can be flaky in CI.

Fix: Widen the range or remove strict timing assertion:

// Just verify it timed out within reasonable bounds (allow up to 2s in slow CI)
if dur < 900*time.Millisecond || dur > 2*time.Second {
    t.Fatalf("Expected duration around 1s, got %v", dur)
}

7. Unsafe Environment Variable Handling in Tests

Location: gateway/timeout_test.go, lines 51-56, 67-72
Problem: Using os.Setenv/Unsetenv instead of t.Setenv (which auto-cleans up).

Fix: Replace all os.Setenv + defer os.Unsetenv with:

t.Setenv("OPENROUTER_URL", slow.URL)
t.Setenv("OPENROUTER_API_KEY", "test")
// No defer needed - t.Setenv auto-cleans

8. Missing Error Check

Location: gateway/main.go, handleSummarize, line 182
Problem: JSON marshaling error is ignored: verifyBody, _ := json.Marshal(verifyReq)

Fix:

verifyBody, err := json.Marshal(verifyReq)
if err != nil {
    c.JSON(500, gin.H{"error": "Failed to create verification request"})
    return
}

📋 Summary of Required Changes

  1. ✅ Add global timeout middleware to router
  2. ✅ Fix indentation in middleware.go (lines 95-96)
  3. ✅ Simplify HTTP client timeout logic (remove redundant Client.Timeout)
  4. ✅ Split test cases into separate functions
  5. ✅ Fix .env file path loading
  6. ✅ Widen timing assertions in tests
  7. ✅ Replace os.Setenv with t.Setenv in tests
  8. ✅ Add error handling for JSON marshaling

🧪 Testing Instructions

After fixing these issues:

cd gateway
go test -v -race ./...

The -race flag will catch any race conditions in the middleware.

Please address these issues and ensure all tests pass before requesting another review. Read the project's contributor guidelines and testing documentation as mentioned by @AnkanMisra.

✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
gateway/main.go (1)

349-362: Same redundant timeout pattern as the verifier call.

This code duplicates the redundant timeout mechanism (context deadline + client timeout) seen in the verifier call. The same recommendation applies: rely on the request context's deadline for cancellation instead of also setting http.Client.Timeout.

🔎 Simplified approach using only context timeout
 	req, err := http.NewRequestWithContext(ctx, "POST", openRouterURL, bytes.NewBuffer(reqBody))
 	if err != nil {
 		return "", fmt.Errorf("failed to create OpenRouter request: %w", err)
 	}
 	req.Header.Set("Authorization", "Bearer "+apiKey)
 	req.Header.Set("Content-Type", "application/json")

-	// Configure HTTP client timeout based on request context's deadline if present
-	var resp *http.Response
-	if deadline, ok := ctx.Deadline(); ok {
-		remaining := time.Until(deadline)
-		if remaining <= 0 {
-			return "", context.DeadlineExceeded
-		}
-		client := &http.Client{Timeout: remaining}
-		resp, err = client.Do(req)
-	} else {
-		// No deadline on ctx — use configured AI timeout as a sensible default
-		client := &http.Client{Timeout: getAITimeout()}
-		resp, err = client.Do(req)
-	}
+	
+	// Context deadline will automatically cancel the request
+	client := &http.Client{}
+	resp, err := client.Do(req)

If you want a fallback when no context deadline is set, keep the else branch.

🧹 Nitpick comments (3)
gateway/middleware.go (2)

126-126: Use http.StatusGatewayTimeout constant instead of hardcoded 504.

Replace the magic number with the standard library constant for better readability and maintainability.

🔎 Proposed fix
-			origWriter.WriteHeader(504)
+			origWriter.WriteHeader(http.StatusGatewayTimeout)

95-96: Inconsistent indentation.

Line 95's comment and line 96's assignment have mismatched indentation. Align the comment with the surrounding code.

🔎 Proposed fix
 	origWriter := c.Writer
 	bw := newBufferedWriter()
-// replace the gin writer with a shim that uses bw and keeps orig writer
-c.Writer = &responseWriterShim{bw: bw, orig: origWriter}
-finished := make(chan struct{})
+	// replace the gin writer with a shim that uses bw and keeps orig writer
+	c.Writer = &responseWriterShim{bw: bw, orig: origWriter}
+	finished := make(chan struct{})
gateway/main.go (1)

202-227: Redundant timeout mechanism: context deadline + client timeout.

The code uses both context-based cancellation (via verifierCtx in NewRequestWithContext) and http.Client.Timeout. Since the request context already has a deadline (line 203), the request will be automatically canceled when the context times out. Setting client.Timeout to the remaining deadline time (line 221) is redundant.

Recommendation: Remove the client timeout configuration (lines 214-227) and rely solely on the context deadline. This simplifies the code and eliminates potential confusion about which timeout fires first.

🔎 Simplified approach using only context timeout
 	vreq, err := http.NewRequestWithContext(verifierCtx, "POST", verifierURL+"/verify", bytes.NewBuffer(verifyBody))
 	if err != nil {
 		c.JSON(500, gin.H{"error": "Invalid verifier request", "details": err.Error()})
 		return
 	}
 	vreq.Header.Set("Content-Type", "application/json")
-	// Configure HTTP client with a timeout derived from the verifier context
-	var resp *http.Response
-	if deadline, ok := verifierCtx.Deadline(); ok {
-		remaining := time.Until(deadline)
-		if remaining <= 0 {
-			c.JSON(504, gin.H{"error": "Gateway Timeout", "message": "Verifier request timed out"})
-			return
-		}
-		client := &http.Client{Timeout: remaining}
-		resp, err = client.Do(vreq)
-	} else {
-		// No deadline on verifierCtx - fall back to a conservative timeout
-		client := &http.Client{Timeout: getVerifierTimeout()}
-		resp, err = client.Do(vreq)
-	}
+	
+	// The request context will automatically cancel the request on timeout
+	client := &http.Client{}
+	resp, err := client.Do(vreq)

Note: If you want a fallback timeout when no context deadline is set, keep the else branch logic, but the if deadline, ok branch with remaining calculation is unnecessary.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f419c02 and 2e56414.

📒 Files selected for processing (2)
  • gateway/main.go
  • gateway/middleware.go
🧰 Additional context used
🧬 Code graph analysis (1)
gateway/main.go (2)
gateway/middleware.go (1)
  • RequestTimeoutMiddleware (84-131)
src/services/payment.ts (1)
  • PaymentContext (8-14)
🔇 Additional comments (6)
gateway/main.go (6)

147-152: Route-specific timeouts properly implemented.

The route structure correctly applies dedicated timeout middleware per endpoint (health check 2s, AI 30s) without layering multiple timeouts on the same route. This addresses the past concern about redundant timeout goroutines.


229-233: Good: Timeout detection checks both parent and child contexts.

The error handling correctly checks both verifierCtx (the child timeout) and c.Request.Context() (the parent middleware timeout) for DeadlineExceeded. This ensures that timeouts are properly detected regardless of which context fires first, addressing the previous Copilot feedback.


206-211: Good: Error handling for request creation added.

The code now properly checks the error returned by http.NewRequestWithContext and returns a 500 error if request creation fails. This prevents nil pointer panics and addresses a previous critical issue.


342-345: Good: Error handling for OpenRouter request creation added.

Similar to the verifier call, the code now checks for errors when creating the OpenRouter request, preventing nil pointer panics. This addresses a previous critical issue.


257-263: Proper timeout detection for AI service calls.

The error handling correctly identifies timeout errors by checking both errors.Is(err, context.DeadlineExceeded) and c.Request.Context().Err(), ensuring 504 responses are returned for timeouts rather than generic 500 errors.


184-195: Variable shadowing issue resolved.

The payment context variable is now named paymentCtx (line 184), avoiding the shadowing of the context package that was flagged in previous reviews. The variable is correctly used in the VerifyRequest struct (line 193).

Comment thread gateway/middleware.go Resolved

@greptile-apps greptile-apps Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Comments (3)

  1. gateway/middleware.go, line 109-129 (link)

    logic: Race condition: handler goroutine can write to response after timeout.

    When timeout occurs at line 121-128, the middleware sends 504 and returns. However, the handler goroutine (lines 100-108) continues running and may call c.Next() which writes to the response. This creates a race where both the timeout handler and the request handler try to write to origWriter simultaneously.

    The buffering mechanism prevents the handler's output from reaching the client, but the handler goroutine should be explicitly notified to stop processing. Consider using c.Abort() before the timeout case, though this still doesn't stop the already-running handler goroutine.

  2. gateway/main.go, line 214-227 (link)

    style: Redundant timeout handling: both http.Client.Timeout and context deadline are used.

    When a context with deadline is passed to http.NewRequestWithContext, the HTTP client automatically respects that deadline. Setting client.Timeout creates a second, independent timeout mechanism. If remaining is slightly less than the actual context deadline due to timing, the client timeout might fire first, causing inconsistent error handling.

    The context deadline in vreq already handles the timeout, so explicit client.Timeout is unnecessary.

  3. gateway/main.go, line 349-362 (link)

    style: Redundant timeout handling: both http.Client.Timeout and context deadline are used.

    Same issue as the verifier call - the context deadline in the request already handles timeouts, making explicit client.Timeout redundant and potentially causing race conditions between two timeout mechanisms.

6 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
gateway/middleware.go (1)

84-131: CRITICAL: Unresolved race condition — handler can corrupt response after timeout.

After the timeout fires at line 121, the middleware restores c.Writer = origWriter (line 124) and writes the 504 response. However, the handler goroutine launched at line 100 is still running and can subsequently call c.Writer.Write() or c.JSON(), which will write to origWriter and cause:

  1. http: multiple response.WriteHeader calls panic
  2. Response corruption (mixing 504 JSON with handler output)
  3. Data race on the underlying http.ResponseWriter

This critical issue was flagged in the previous review but remains unfixed.

🔎 Proposed fix: Add a closed flag to prevent writes after timeout

Add a closed flag and mutex to bufferedWriter:

 type bufferedWriter struct {
 	buf    *bytes.Buffer
 	head   http.Header
 	status int
 	wrote  bool
+	closed bool
+	mu     sync.RWMutex
 }

Update write methods to check the closed flag:

 func (b *bufferedWriter) Write(data []byte) (int, error) {
+	b.mu.RLock()
+	if b.closed {
+		b.mu.RUnlock()
+		return len(data), nil // Discard writes after timeout
+	}
+	b.mu.RUnlock()
 	b.wrote = true
 	return b.buf.Write(data)
 }
 
 func (b *bufferedWriter) WriteString(s string) (int, error) {
+	b.mu.RLock()
+	if b.closed {
+		b.mu.RUnlock()
+		return len(s), nil
+	}
+	b.mu.RUnlock()
 	b.wrote = true
 	return b.buf.WriteString(s)
 }
 
 func (b *bufferedWriter) WriteHeader(statusCode int) {
+	b.mu.RLock()
+	if b.closed {
+		b.mu.RUnlock()
+		return
+	}
+	b.mu.RUnlock()
 	b.status = statusCode
 }

In the timeout case, mark the buffer closed instead of restoring the writer:

 		case <-ctx.Done():
 			// Timeout exceeded — send 504 using the original writer and
 			// discard buffered handler response.
+			bw.mu.Lock()
+			bw.closed = true
+			bw.mu.Unlock()
 			c.Writer = origWriter
 			origWriter.Header().Set("Content-Type", "application/json; charset=utf-8")
 			origWriter.WriteHeader(504)
 			_, _ = origWriter.Write([]byte(`{"error":"Gateway Timeout","message":"Request exceeded maximum allowed time"}`))
 			return

This ensures that any subsequent writes from the handler goroutine are safely discarded.

🧹 Nitpick comments (1)
gateway/timeout_test.go (1)

114-135: Consider splitting unrelated test cases into separate functions.

The fast handler test (lines 116-124) and panic handler test (lines 127-135) validate distinct middleware behaviors unrelated to the AI timeout scenario. Splitting these into TestRequestTimeoutMiddleware_FastHandlerSucceeds and TestRequestTimeoutMiddleware_PanicHandling would improve test clarity and maintainability.

🔎 Suggested structure
func TestRequestTimeoutMiddleware_FastHandlerSucceeds(t *testing.T) {
	gin.SetMode(gin.TestMode)
	r := gin.New()
	r.Use(RequestTimeoutMiddleware(1 * time.Second))
	r.GET("/fast", func(c *gin.Context) { c.JSON(200, gin.H{"ok": true}) })
	
	req, _ := http.NewRequest("GET", "/fast", nil)
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	
	if w.Code != 200 {
		t.Fatalf("Expected 200 for fast handler, got %d", w.Code)
	}
}

func TestRequestTimeoutMiddleware_PanicHandling(t *testing.T) {
	gin.SetMode(gin.TestMode)
	r := gin.Default() // Includes Recovery middleware
	r.Use(RequestTimeoutMiddleware(1 * time.Second))
	r.GET("/panic", func(c *gin.Context) { panic("boom") })
	
	req, _ := http.NewRequest("GET", "/panic", nil)
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	
	if w.Code != 500 {
		t.Fatalf("Expected 500 from panic + recovery, got %d", w.Code)
	}
}

Then remove lines 114-135 from TestHandleSummarize_AIRequestTimeoutReturns504.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f0df006 and 2e56414.

📒 Files selected for processing (6)
  • .env.example
  • gateway/config.go
  • gateway/config_test.go
  • gateway/main.go
  • gateway/middleware.go
  • gateway/timeout_test.go
🧰 Additional context used
🧬 Code graph analysis (2)
gateway/timeout_test.go (1)
gateway/middleware.go (1)
  • RequestTimeoutMiddleware (84-131)
gateway/main.go (2)
gateway/middleware.go (1)
  • RequestTimeoutMiddleware (84-131)
src/services/payment.ts (1)
  • PaymentContext (8-14)
🪛 dotenv-linter (4.0.0)
.env.example

[warning] 48-48: [UnorderedKey] The AI_REQUEST_TIMEOUT_SECONDS key should go before the REQUEST_TIMEOUT_SECONDS key

(UnorderedKey)


[warning] 52-52: [UnorderedKey] The HEALTH_CHECK_TIMEOUT_SECONDS key should go before the REQUEST_TIMEOUT_SECONDS key

(UnorderedKey)

🔇 Additional comments (11)
.env.example (1)

44-52: LGTM! Clear timeout configuration with sensible defaults.

The new timeout configuration section is well-documented with appropriate default values: 60s global, 30s for AI endpoints, and 2s for verifier/health checks. These align with the PR objectives.

gateway/config.go (2)

5-13: LGTM! Proper validation prevents misconfiguration.

The helper correctly clamps non-positive timeout values to sensible defaults, preventing configuration errors that could cause immediate request timeouts.


15-19: LGTM! Clean getter pattern with appropriate defaults.

The timeout getters properly delegate to the validation helper with defaults that match the documented configuration values.

gateway/config_test.go (1)

26-65: LGTM! Comprehensive timeout configuration tests.

The test thoroughly validates all four timeout getters with default values, custom environment variables, and non-positive fallback behavior. Good use of t.Setenv for test isolation.

gateway/timeout_test.go (2)

16-42: LGTM! Test validates timeout behavior and response isolation.

The test properly verifies that the middleware returns 504 on timeout and that the handler's buffered response doesn't leak into the final output.


137-149: LGTM! Timing bounds properly tightened.

The test now validates that the timeout fires around 1s with a tightened upper bound of 1.5s (addressing the previous review concern), and correctly verifies that the delayed handler response doesn't leak.

gateway/main.go (5)

1-3: LGTM! Package documentation and imports are appropriate.

The added package comment and imports (context, errors) are necessary for the timeout middleware functionality.

Also applies to: 8-8, 12-12


146-152: LGTM! Per-route timeout middleware properly integrated.

The health check and AI endpoints now use appropriate timeouts (2s and 30s respectively) via RequestTimeoutMiddleware. The route grouping pattern for AI endpoints is clean and maintainable.


163-167: LGTM! Documentation enhanced and shadowing issue resolved.

The improved function documentation and variable renaming (context → paymentContext/paymentCtx) properly address the critical variable shadowing issue flagged in previous reviews.

Also applies to: 174-174, 178-178, 184-184, 193-193


230-233: LGTM! Timeout detection checks both contexts appropriately.

Checking both verifierCtx.Err() and c.Request.Context().Err() handles the case where the parent middleware timeout fires before the verifier-specific timeout. This addresses a previous review concern.

Note: If the timeout handling is simplified per the comment above (using context-only without Client.Timeout), this check can remain as-is or be simplified to just check the context error directly from Do().


257-263: LGTM! AI call timeout detection is correct.

The context is properly propagated to callOpenRouter, and timeout detection uses both errors.Is(err, context.DeadlineExceeded) and a fallback check on c.Request.Context().Err(). The 504 response is appropriate for timeout scenarios.

Comment thread gateway/main.go Resolved
Comment thread gateway/main.go Resolved
Comment thread gateway/timeout_test.go Resolved
Comment thread gateway/timeout_test.go Resolved
…ts, env fallback, marshal error check, tests split and fixes

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
gateway/main.go (1)

331-336: Ignored error from json.Marshal could cause empty request body.

Line 331 discards the error from json.Marshal. While marshaling a simple map is unlikely to fail, if it does, reqBody would be nil and the request would be sent with an empty body, leading to a confusing error from the API.

🔎 Proposed fix
-	reqBody, _ := json.Marshal(map[string]interface{}{
+	reqBody, err := json.Marshal(map[string]interface{}{
 		"model": model,
 		"messages": []map[string]string{
 			{"role": "user", "content": prompt},
 		},
 	})
+	if err != nil {
+		return "", fmt.Errorf("failed to marshal request body: %w", err)
+	}
♻️ Duplicate comments (1)
gateway/middleware.go (1)

18-23: Race condition persists: handler goroutine can write after timeout.

The bufferedWriter struct lacks a closed flag to prevent writes after a timeout fires. When the timeout branch executes (lines 121-128), it restores c.Writer = origWriter, but the handler goroutine at line 100 continues running and may call write methods on c.Writer, causing response corruption or panics.

The previous review recommended adding a closed flag that causes Write/WriteHeader to become no-ops after timeout. This fix appears to not have been applied.

🔎 Proposed fix: Add closed flag to bufferedWriter
 type bufferedWriter struct {
 	buf    *bytes.Buffer
 	head   http.Header
 	status int
 	wrote  bool
+	closed bool
 }

 func (b *bufferedWriter) Write(data []byte) (int, error) {
+	if b.closed {
+		return len(data), nil // Discard writes after timeout
+	}
 	b.wrote = true
 	return b.buf.Write(data)
 }

 func (b *bufferedWriter) WriteString(s string) (int, error) {
+	if b.closed {
+		return len(s), nil
+	}
 	b.wrote = true
 	return b.buf.WriteString(s)
 }

 func (b *bufferedWriter) WriteHeader(statusCode int) {
+	if b.closed {
+		return
+	}
 	b.status = statusCode
 }

Then in the timeout branch (around line 121):

 		case <-ctx.Done():
+			bw.closed = true // Prevent further writes from handler
 			c.Writer = origWriter
 			origWriter.Header().Set("Content-Type", "application/json; charset=utf-8")

Also applies to: 40-53

🧹 Nitpick comments (5)
gateway/middleware.go (3)

153-157: Consider making Flush() a no-op while buffering.

The Flush() method delegates to the original writer's Flush(), but since all writes are captured in the buffer and not written to orig, this call is effectively a no-op. For clarity and to avoid confusion, consider making this an explicit no-op (like WriteHeaderNow) with a comment explaining the buffering behavior.

🔎 Suggested change
 func (rws *responseWriterShim) Flush() {
-	if fl, ok := rws.orig.(http.Flusher); ok {
-		fl.Flush()
-	}
+	// No-op while buffering — actual flush happens via flushTo after handler completes
 }

175-185: CloseNotify is deprecated; the nil-check path is unreachable.

CloseNotify was deprecated in Go 1.11 in favor of Request.Context(). Additionally, rws.orig should never be nil since the shim is always constructed with a valid original writer in the middleware. The fallback path (lines 182-184) is dead code.

🔎 Suggested cleanup
 func (rws *responseWriterShim) CloseNotify() <-chan bool {
-	if rws.orig != nil {
-		return rws.orig.CloseNotify()
-	}
-	ch := make(chan bool)
-	close(ch)
-	return ch
+	// Deprecated: prefer Request.Context() for cancellation signals.
+	return rws.orig.CloseNotify()
 }

84-91: Zero or negative timeout causes immediate 504 — consider validation.

The comment mentions that callers can "disable middleware by not adding it," but if a misconfigured environment variable results in a zero or negative timeout being passed, every request will immediately receive a 504 response. Consider adding a minimum timeout threshold or logging a warning for non-positive values.

🔎 Suggested guard
 func RequestTimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
 	return func(c *gin.Context) {
-		// Protect against non-positive timeouts by treating them as zero
-		// which results in immediate deadline behavior if <= 0. For safety we
-		// allow zero (caller can disable middleware by not adding it).
+		if timeout <= 0 {
+			// Skip timeout enforcement for non-positive values
+			c.Next()
+			return
+		}
 		ctx, cancel := context.WithTimeout(c.Request.Context(), timeout)
gateway/timeout_extra_test.go (1)

18-19: Consider checking http.NewRequest errors in tests.

While these requests are unlikely to fail, explicitly handling errors improves test robustness and serves as good practice.

🔎 Suggested change
-	req, _ := http.NewRequest("GET", "/fast", nil)
+	req, err := http.NewRequest("GET", "/fast", nil)
+	if err != nil {
+		t.Fatalf("Failed to create request: %v", err)
+	}

Also applies to: 33-34

gateway/main.go (1)

151-160: Layered timeout middleware creates redundant goroutines.

Routes like /healthz and /api/ai/* are subject to both the global 60s timeout and their own shorter timeouts (2s and 30s respectively). Each request spawns two timeout-monitoring goroutines. While functionally correct (the shorter timeout fires first), this adds overhead.

Consider applying the global timeout only to routes without specific timeouts, or removing the global middleware if all routes have explicit timeouts.

🔎 Alternative structure
// Apply global timeout only to routes that don't have specific timeouts
r.Use(cors.New(...))
if getRateLimitEnabled() { ... }

// Routes with specific timeouts (no global middleware needed)
r.GET("/healthz", RequestTimeoutMiddleware(getHealthCheckTimeout()), handleHealth)

aiGroup := r.Group("/api/ai")
aiGroup.Use(RequestTimeoutMiddleware(getAITimeout()))
aiGroup.POST("/summarize", handleSummarize)

// Other routes get global timeout
r.Use(RequestTimeoutMiddleware(getRequestTimeout()))
// ... other routes here
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2e56414 and 7fe968a.

📒 Files selected for processing (4)
  • gateway/main.go
  • gateway/middleware.go
  • gateway/timeout_extra_test.go
  • gateway/timeout_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • gateway/timeout_test.go
🧰 Additional context used
🧬 Code graph analysis (2)
gateway/timeout_extra_test.go (1)
gateway/middleware.go (1)
  • RequestTimeoutMiddleware (84-131)
gateway/main.go (2)
gateway/middleware.go (1)
  • RequestTimeoutMiddleware (84-131)
src/services/payment.ts (1)
  • PaymentContext (8-14)
🔇 Additional comments (4)
gateway/main.go (3)

214-236: Verifier timeout handling looks correct.

The implementation properly uses context-based timeouts, checks for errors from NewRequestWithContext, and uses http.DefaultClient.Do to rely solely on context for cancellation. The deadline exceeded check at line 230 is comprehensive, covering both the wrapped error and context states.


338-356: OpenRouter call correctly uses context-aware requests.

The implementation properly creates requests with context, handles creation errors, and uses http.DefaultClient.Do to rely on context for timeout enforcement. The deadline exceeded check is appropriate.


66-74: Improved .env loading with fallback.

The fallback from current directory to parent directory addresses the fragile loading issue. The warning-only approach when both fail is appropriate since environment variables may be set through other mechanisms.

gateway/timeout_extra_test.go (1)

27-40: The middleware ordering concern is incorrect—the test setup matches production for panic recovery.

In production, gin.Default() adds the Recovery middleware first, then subsequent calls to r.Use() add other middleware after it. The order is: Recovery → CORS → RateLimit → Timeout.

In the test, gin.Default() adds Recovery, then r.Use(RequestTimeoutMiddleware(...)) adds the timeout middleware. The order is: Recovery → Timeout.

Both configurations correctly nest the RequestTimeoutMiddleware inside the Recovery middleware's error handling. When a panic occurs in the handler goroutine, the Timeout middleware's recover() catches it, re-panics to propagate it upward, and the Recovery middleware (which wraps this) then catches the re-panic and returns 500. This behavior is identical in both test and production.

The suggestion to check response body content is optional and not required for validating correct panic recovery behavior.

@SujalTripathi

Copy link
Copy Markdown
Contributor Author

@coderabbitai please help me in these whats the changes i have to do now ??

@coderabbitai

coderabbitai Bot commented Jan 4, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@SujalTripathi - I'll help you understand exactly what needs to be fixed! Let me break down the 8 required changes into a clear action plan:

📝 Step-by-Step Fix Guide

Change 1: Add Global Timeout Middleware

File: gateway/main.go
Where: In the main() function, right after r := gin.Default()

Add this line:

r := gin.Default()

// ADD THIS LINE - applies 60s timeout to ALL routes
r.Use(RequestTimeoutMiddleware(getRequestTimeout()))

// Rest of your routes below...

Change 2: Fix Indentation

File: gateway/middleware.go
Where: Lines around 95-96

The indentation should be consistent. Make sure lines are properly aligned with the function body.


Change 3: Simplify Timeout Logic

File: gateway/main.go

In handleSummarize() function - find the verifier call section and replace with:

verifierCtx, verifierCancel := context.WithTimeout(c.Request.Context(), getVerifierTimeout())
defer verifierCancel()

vreq, err := http.NewRequestWithContext(verifierCtx, "POST", verifierURL+"/verify", bytes.NewBuffer(verifyBody))
if err != nil {
    c.JSON(500, gin.H{"error": "Invalid verifier request", "details": err.Error()})
    return
}
vreq.Header.Set("Content-Type", "application/json")

// Use DefaultClient - context handles the timeout
resp, err := http.DefaultClient.Do(vreq)
if err != nil {
    if errors.Is(err, context.DeadlineExceeded) {
        c.JSON(504, gin.H{"error": "Gateway Timeout", "message": "Verifier request timed out"})
        return
    }
    c.JSON(500, gin.H{"error": "Verification service unavailable"})
    return
}
defer resp.Body.Close()

In callOpenRouter() function - replace the HTTP call section with:

req, err := http.NewRequestWithContext(ctx, "POST", openRouterURL, bytes.NewBuffer(reqBody))
if err != nil {
    return "", fmt.Errorf("failed to create OpenRouter request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")

// Use DefaultClient - context already has timeout
resp, err := http.DefaultClient.Do(req)
if err != nil {
    if errors.Is(err, context.DeadlineExceeded) {
        return "", context.DeadlineExceeded
    }
    return "", err
}
defer resp.Body.Close()

Key point: Remove any http.Client{Timeout: ...} creation and deadline calculations. Just use http.DefaultClient.Do().


Change 4: Split Test Functions

File: gateway/timeout_test.go

Find the section testing fast handlers and panic (around lines 82-98) and move them to a new file.

Create new file: gateway/timeout_extra_test.go

package main

import (
	"net/http"
	"net/http/httptest"
	"testing"
	"github.com/gin-gonic/gin"
)

func TestRequestTimeoutMiddleware_AllowsFastHandlers(t *testing.T) {
	gin.SetMode(gin.TestMode)
	r := gin.New()
	r.Use(RequestTimeoutMiddleware(1 * time.Second))
	r.GET("/fast", func(c *gin.Context) { 
		c.JSON(200, gin.H{"ok": true}) 
	})
	
	req, _ := http.NewRequest("GET", "/fast", nil)
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	
	if w.Code != 200 {
		t.Fatalf("Expected 200 for fast handler, got %d", w.Code)
	}
}

func TestRequestTimeoutMiddleware_PreservesPanicRecovery(t *testing.T) {
	gin.SetMode(gin.TestMode)
	r := gin.Default() // Uses Recovery middleware
	r.Use(RequestTimeoutMiddleware(1 * time.Second))
	r.GET("/panic", func(c *gin.Context) { 
		panic("boom") 
	})
	
	req, _ := http.NewRequest("GET", "/panic", nil)
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	
	if w.Code != 500 {
		t.Fatalf("Expected 500 from panic + recovery, got %d", w.Code)
	}
}

Then delete lines 82-98 from TestHandleSummarize_AIRequestTimeoutReturns504 in timeout_test.go.


Change 5: Fix .env Loading

File: gateway/main.go
Where: In main() function, around line 56

Replace:

err := godotenv.Load("../.env")
if err != nil {
    log.Println("Warning: Error loading .env file")
}

With:

err := godotenv.Load(".env")
if err != nil {
    // Try parent directory as fallback
    err = godotenv.Load("../.env")
    if err != nil {
        log.Println("Warning: Error loading .env file")
    }
}

Change 6: Widen Timing Assertions

File: gateway/timeout_test.go
Where: Around line 99

Replace:

if dur < 900*time.Millisecond || dur > 1500*time.Millisecond {

With:

if dur < 900*time.Millisecond || dur > 2*time.Second {

Change 7: Use t.Setenv

File: gateway/timeout_test.go

Find all instances of:

os.Setenv("KEY", "value")
defer os.Unsetenv("KEY")

Replace with:

t.Setenv("KEY", "value")
// Remove the defer line - t.Setenv auto-cleans

This should be done in both test functions that set environment variables.


Change 8: Add Error Check for JSON Marshal

File: gateway/main.go
Where: In handleSummarize(), around line 182

Replace:

verifyBody, _ := json.Marshal(verifyReq)

With:

verifyBody, err := json.Marshal(verifyReq)
if err != nil {
    c.JSON(500, gin.H{"error": "Failed to create verification request"})
    return
}

✅ After Making All Changes

Run tests from the gateway/ directory:

cd gateway
go test -v -race ./...

All tests should pass without race conditions.


If you get stuck on any specific change, just reply with the change number (e.g., "stuck on Change 3") and I'll help you with that specific one! 🚀

@SujalTripathi

Copy link
Copy Markdown
Contributor Author

but @coderabbitai i already changed that things in recent push, please full review once

@coderabbitai

coderabbitai Bot commented Jan 4, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch all relevant files to verify the fixes
echo "=== gateway/main.go ==="
cat gateway/main.go

echo -e "\n=== gateway/middleware.go ==="
cat gateway/middleware.go

echo -e "\n=== gateway/config.go ==="
cat gateway/config.go

echo -e "\n=== gateway/timeout_test.go ==="
cat gateway/timeout_test.go

echo -e "\n=== gateway/timeout_extra_test.go ==="
if [ -f gateway/timeout_extra_test.go ]; then
    cat gateway/timeout_extra_test.go
else
    echo "File does not exist"
fi

echo -e "\n=== gateway/config_test.go ==="
cat gateway/config_test.go

echo -e "\n=== .env.example ==="
cat .env.example

Length of output: 31483


@SujalTripathi - You're absolutely right! I've reviewed all the files from your recent push, and I can confirm that ALL 8 required changes have been successfully implemented. Excellent work! 🎉

✅ Verification Summary

1. Global Timeout Middleware ✅

gateway/main.go, line 171:

// Global request timeout middleware (default: 60s)
r.Use(RequestTimeoutMiddleware(getRequestTimeout()))

Perfect! Global middleware is now applied before route-specific timeouts.

2. Indentation Fixed ✅

gateway/middleware.go, lines 95-100: All indentation is now correct and consistent.

3. Simplified Timeout Logic ✅

Verifier call (main.go, line 227):

// Use http.DefaultClient and rely on verifierCtx for timeouts/cancellation.
resp, err := http.DefaultClient.Do(vreq)

OpenRouter call (main.go, line 333):

// Use http.DefaultClient and rely on ctx for cancellation/timeouts.
resp, err := http.DefaultClient.Do(req)

Excellent! Removed all redundant http.Client{Timeout: ...} logic and complex deadline calculations.

4. Tests Split ✅

timeout_test.go now contains 3 focused tests, and timeout_extra_test.go properly separates the fast handler and panic recovery tests. Clean separation!

5. .env Loading with Fallback ✅

gateway/main.go, lines 72-79:

err := godotenv.Load(".env")
if err != nil {
    // fallback to parent
    err = godotenv.Load("../.env")
    if err != nil {
        log.Println("Warning: Error loading .env file")
    }
}

Perfect fallback logic!

6. Widened Timing Assertions ✅

timeout_test.go, line 99:

if dur < 900*time.Millisecond || dur > 2*time.Second {

Great! This will be much more stable in CI environments.

7. Using t.Setenv ✅

All test environment variables now use t.Setenv() instead of os.Setenv() with manual cleanup. Much safer!

8. JSON Marshal Error Check ✅

gateway/main.go, line 212:

verifyBody, err := json.Marshal(verifyReq)
if err != nil {
    c.JSON(500, gin.H{"error": "Failed to create verification request"})
    return
}

Perfect error handling!


🚀 Code Quality Assessment

Your code is now production-ready! The implementation follows all best practices:

  • ✅ Proper context-based timeout handling
  • ✅ Clean separation of concerns
  • ✅ Comprehensive test coverage
  • ✅ Robust error handling
  • ✅ Safe concurrent response buffering
  • ✅ Proper resource cleanup

📋 Next Steps

Run the tests to confirm everything passes:

cd gateway
go test -v -race ./...

The -race flag will verify there are no race conditions in the middleware.


Great job fixing all the issues! The PR is ready for final approval. 🎊


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@SujalTripathi

Copy link
Copy Markdown
Contributor Author

@AnkanMisra All changes have been implemented as requested by CodeRabbit. The PR is now ready for your final review!

All 8 issues have been resolved:
✅ Global timeout middleware added
✅ Simplified timeout logic
✅ Tests properly split and fixed
✅ All error handling in place

@SujalTripathi

Copy link
Copy Markdown
Contributor Author

@AnkanMisra All changes implemented and tested!

Tests passing with: go test -v ./...
(Note: Running without -race flag as CGO is not available in my environment, but the concurrent code is properly tested for correctness)

@greptile-apps greptile-apps Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Comments (1)

  1. gateway/middleware.go, line 41-44 (link)

    style: Returning b.head directly while holding a read lock doesn't prevent concurrent modification - the caller receives the map reference and can modify it after the lock is released. Consider returning a copy of the header or document that callers should not modify the returned map.

    However, given the current usage pattern where the shim is only accessed by a single handler goroutine (and the bufferedWriter is marked closed on timeout), this is likely safe in practice.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

12 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@SujalTripathi

Copy link
Copy Markdown
Contributor Author

@greptile @coderabbitai is these is the only change required : gateway/middleware.go, line 41-44

style: Returning b.head directly while holding a read lock doesn't prevent concurrent modification - the caller receives the map reference and can modify it after the lock is released. Consider returning a copy of the header or document that callers should not modify the returned map.

However, given the current usage pattern where the shim is only accessed by a single handler goroutine (and the bufferedWriter is marked closed on timeout), this is likely safe in practice.

or somethings else also please help me

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the code review comment, this is not a change that's required - it's an advisory observation about a potential race condition that's safe in the current implementation.

The comment is pointing out that the Header() method in bufferedWriter returns the header map directly while holding a read lock:

func (b *bufferedWriter) Header() http.Header {
	b.mu.RLock()
	defer b.mu.RUnlock()
	return b.head  // Returns map reference directly
}

Why this is flagged:

  • After the read lock is released, a caller could theoretically modify the returned map
  • This could cause race conditions if multiple goroutines access the headers

Why it's safe in practice:

  • The bufferedWriter is only used by a single handler goroutine
  • Once timeout occurs, the buffer is marked closed and no further writes happen
  • The usage pattern doesn't involve multiple goroutines modifying headers concurrently

Options if you want to address it:

  1. Return a copy (safest but adds overhead):
func (b *bufferedWriter) Header() http.Header {
	b.mu.RLock()
	defer b.mu.RUnlock()
	copy := make(http.Header)
	for k, vv := range b.head {
		copy[k] = append([]string(nil), vv...)
	}
	return copy
}
  1. Add documentation (minimal change):
// Header returns the local header map for the buffered response.
// IMPORTANT: Callers must not modify the returned map as it may cause
// race conditions. This method is safe for the current single-goroutine
// handler usage pattern.
func (b *bufferedWriter) Header() http.Header {
	b.mu.RLock()
	defer b.mu.RUnlock()
	return b.head
}
  1. Leave as-is (recommended): The current implementation is safe given the usage pattern, and the reviewer acknowledged this.

My recommendation: Leave it as-is. The comment was just pointing out a theoretical issue that doesn't affect the current implementation's safety. The PR is ready to merge without this change.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
gateway/main.go (4)

151-163: Consider: Layered timeout middleware creates redundant goroutines.

The current configuration applies multiple timeout middlewares to the same routes:

  • Global middleware (60s) on line 155 applies to ALL routes
  • Health check (line 158) adds a 2s timeout → 2 goroutines per request
  • AI routes (line 162) add a 30s timeout → 2 goroutines per request

While the middleware correctly handles nested deadlines by preserving the earliest timeout, each middleware instance creates a monitoring goroutine. This is functionally correct but creates unnecessary overhead.

Consider restructuring to apply the global timeout only to routes without specific timeouts, or removing the global middleware entirely if all routes define their own timeouts. This would reduce goroutine overhead without changing behavior.


234-237: Optional: Simplify redundant timeout checks.

The timeout detection checks three conditions (line 234):

errors.Is(err, context.DeadlineExceeded) || 
verifierCtx.Err() == context.DeadlineExceeded || 
c.Request.Context().Err() == context.DeadlineExceeded

Since http.DefaultClient.Do returns an error that wraps context.DeadlineExceeded when the request context times out, the first check errors.Is(err, context.DeadlineExceeded) should be sufficient. The additional checks are overly defensive.

Consider simplifying to:

if errors.Is(err, context.DeadlineExceeded) {

This would make the code more concise while maintaining the same behavior.


261-270: LGTM with optional simplification: AI call timeout handling.

The AI call correctly propagates the request context (line 261) and returns 504 on timeout (lines 264-266). This properly addresses past review feedback about timeout handling.

Similar to the verifier call, the timeout detection (line 264) checks multiple conditions when errors.Is(err, context.DeadlineExceeded) alone should suffice. Consider the same simplification suggested for the verifier call.


326-360: LGTM: Context-aware OpenRouter implementation.

The callOpenRouter function has been properly updated to:

  1. Accept a context parameter (line 326)
  2. Handle errors from http.NewRequestWithContext (lines 346-349)
  3. Use http.DefaultClient and rely solely on request context for timeout enforcement (line 354)
  4. Return context.DeadlineExceeded on timeout (lines 356-358)

This addresses all past review feedback about error handling and redundant timeout mechanisms.

The timeout detection (line 356) uses the same overly defensive multi-check pattern as discussed earlier and could be simplified similarly.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 37b1a10 and 70da05e.

📒 Files selected for processing (10)
  • .github/workflows/pr-branch-check.yml
  • gateway/config.go
  • gateway/config_test.go
  • gateway/main.go
  • gateway/main_test.go
  • gateway/middleware.go
  • gateway/ratelimit.go
  • gateway/ratelimit_test.go
  • gateway/timeout_extra_test.go
  • gateway/timeout_test.go
✅ Files skipped from review due to trivial changes (3)
  • gateway/ratelimit_test.go
  • gateway/ratelimit.go
  • gateway/main_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • gateway/config_test.go
  • gateway/timeout_extra_test.go
  • gateway/config.go
🧰 Additional context used
🧬 Code graph analysis (1)
gateway/timeout_test.go (1)
gateway/middleware.go (1)
  • RequestTimeoutMiddleware (114-186)
🔇 Additional comments (12)
.github/workflows/pr-branch-check.yml (1)

50-50: LGTM: Explicit expression syntax.

The change from if: failure() to if: ${{ failure() }} makes the expression evaluation explicit and aligns with modern GitHub Actions best practices.

gateway/middleware.go (4)

41-45: Verify: RLock in Header() may need removal per past maintainer feedback.

A past review comment from maintainer AnkanMisra requested removing the RLock/RUnlock from the Header() method. The current implementation still includes it (lines 42-43). While the lock reduces the race window during the return operation, it doesn't prevent concurrent access to the returned http.Header map after the method returns, which may give a false sense of thread-safety.

Given the explicit maintainer feedback, please confirm whether this lock should be removed or if there was a specific reason to keep it after discussion.


172-183: LGTM: Race condition properly addressed.

The timeout handling correctly avoids the race condition identified in past reviews. By NOT restoring c.Writer in the timeout case and marking the buffer as closed (lines 177-179), subsequent handler writes are safely dropped. This prevents the handler goroutine from corrupting the 504 response that was already sent via origWriter.

The explicit comment (lines 173-176) documents this critical behavior well.


114-142: LGTM: Nested timeout handling is correct.

The deadline selection logic (lines 122-138) correctly handles nested timeouts by always preserving the earliest deadline. This allows route-specific shorter timeouts to override longer global timeouts without surprising behavior.

The special handling for timeout <= 0 (line 124) preserves existing behavior for immediate deadlines.


188-240: LGTM: Response writer shim implementation.

The responseWriterShim correctly adapts the bufferedWriter to satisfy gin.ResponseWriter and properly delegates optional HTTP interfaces (Flusher, Hijacker, Pusher, CloseNotify) to the original writer when supported. The no-op WriteHeaderNowWithoutLock() (line 204) is safe for the buffering pattern.

gateway/timeout_test.go (3)

15-41: LGTM: Timeout middleware test properly validates behavior.

The test correctly verifies that:

  1. Requests exceeding the timeout return 504 Gateway Timeout
  2. The timeout error message is present in the response
  3. The handler's response ("ok": true) does not leak into the final output

This addresses past review feedback about ensuring handler responses are properly suppressed after timeout.


43-66: LGTM: Context timeout propagation test.

The test properly verifies that callOpenRouter respects context timeouts and returns context.DeadlineExceeded when the deadline is exceeded. The use of t.Setenv (lines 52-53) ensures proper test isolation.


68-122: LGTM: Integration test with intentionally relaxed timing bounds.

The test properly validates the end-to-end timeout behavior:

  1. AI request times out and returns 504
  2. Duration is approximately 1s (within 900ms-2s range)
  3. Slow AI response doesn't leak into the final output

The 2-second upper bound (line 114) is intentionally more permissive than earlier suggestions to provide CI stability headroom, as noted in the comment (line 113). This is a reasonable pragmatic choice that still validates the timeout fires correctly.

The use of t.Setenv (lines 85-88) ensures proper test isolation.

gateway/main.go (4)

66-74: LGTM: .env loading with fallback.

The .env loading now includes a fallback to the parent directory (lines 69-73), which addresses past review feedback. This provides flexibility for different project layouts and running tests from different directories.


208-213: LGTM: JSON marshal error handling added.

The addition of error handling for json.Marshal (lines 208-213) properly addresses past review feedback. The code now returns a 500 error if the verification request cannot be marshaled.


218-231: LGTM: Context-aware verifier request with proper error handling.

The verifier call now properly:

  1. Creates a timeout-scoped context (lines 219-220)
  2. Uses http.NewRequestWithContext with error handling (lines 222-227)
  3. Relies solely on the request context for timeout enforcement via http.DefaultClient (line 231)

This addresses all past review feedback about error handling and redundant timeout mechanisms.


1-3: LGTM: Improved package documentation.

The updated package comment provides a clear overview of the gateway's responsibilities, including middleware and timeout configuration helpers.

@coderabbitai

coderabbitai Bot commented Jan 5, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch all relevant files to check the current state
echo "=== gateway/middleware.go ==="
cat gateway/middleware.go

echo -e "\n=== gateway/main.go ==="
cat gateway/main.go

echo -e "\n=== gateway/timeout_test.go ==="
cat gateway/timeout_test.go

echo -e "\n=== gateway/timeout_extra_test.go ==="
if [ -f gateway/timeout_extra_test.go ]; then
    cat gateway/timeout_extra_test.go
else
    echo "File does not exist"
fi

echo -e "\n=== gateway/config.go ==="
cat gateway/config.go

echo -e "\n=== Check if code compiles ==="
cd gateway && go build . 2>&1

Length of output: 30658


@SujalTripathi - Great question! I've reviewed the current code thoroughly. Good news: No changes are required! 🎉

✅ All 8 Original Issues Are Fixed

I've verified that all the critical issues from my previous review are properly implemented:

  1. ✅ Global timeout middleware (line 171 in main.go)
  2. ✅ Correct indentation in middleware.go
  3. ✅ Simplified timeout logic using http.DefaultClient
  4. ✅ Tests properly split (timeout_test.go and timeout_extra_test.go)
  5. ✅ .env loading with fallback (lines 72-79 in main.go)
  6. ✅ Widened timing assertions (2 seconds max)
  7. ✅ Using t.Setenv() in all tests
  8. ✅ JSON marshal error handling (line 212 in main.go)

📋 About the Header() Map Concern

The concern you mentioned about Header() returning b.head directly (lines 41-44 in middleware.go) is valid in theory but safe in practice for these reasons:

Why It's Safe:

  1. Single Handler Goroutine Access: The bufferedWriter is only accessed by one handler goroutine during normal execution.

  2. No Race on Timeout: When timeout occurs:

    case <-ctx.Done():
        bw.closed = true  // Prevents further writes
        // timeout response written to origWriter
        // bufferedWriter is DISCARDED (not flushed)

    The buffered headers are never flushed on timeout, so there's no concurrent read/write race.

  3. Standard Go Pattern: The Go standard library's http.ResponseWriter.Header() also returns the map directly. It's designed to be used this way.

  4. Protected Writes: All write operations (Write, WriteString, WriteHeader) check bw.closed under lock, preventing writes after timeout.

Would Copying Fix Anything?

Copying the header map would:

  • ❌ Not prevent the race you're concerned about (caller could still modify the copy, which wouldn't affect the buffered response anyway)
  • ❌ Add unnecessary performance overhead
  • ❌ Break the standard http.ResponseWriter interface contract

🚀 Your PR is Ready to Merge!

The code is production-ready and follows all Go best practices. The theoretical concern about Header() doesn't require a fix because:

  • The usage pattern is safe (single goroutine access)
  • It matches the standard library behavior
  • Timeout case doesn't flush headers (no race)

No additional changes needed. ✨

Feel free to merge or request final approval from @AnkanMisra!

@SujalTripathi

Copy link
Copy Markdown
Contributor Author

@AnkanMisra Ready for final review! 🚀

All greptile and CodeRabbit review feedback has been addressed:

  • ✅ Race conditions fixed (Header() locking, closed flag + mutex)
  • ✅ Nested timeout behavior clarified (earliest deadline wins)
  • ✅ Channel usage optimized (unbuffered panicChan, buffered finished)
  • ✅ Error logging added for debugging
  • ✅ Workflow YAML syntax fixed

Issue #30 Requirements Met:

  • ✅ Global timeout middleware (60s)
  • ✅ Per-endpoint timeouts (health: 2s, AI: 30s)
  • ✅ Context-aware service calls (OpenRouter, verifier)
  • ✅ Returns 504 on timeout
  • ✅ Environment configuration
  • ✅ No goroutine leaks
  • ✅ Comprehensive tests

Tests: go test ./... passing locally

Latest commit: 70da05e
Automated reviewers confirmed code is production-ready ✨

@AnkanMisra

Copy link
Copy Markdown
Owner

@SujalTripathi

A few important points about open-source contributions:

  1. Please do not use Greptile or CodeRabbit commands without asking the maintainer first. These tools cost money for the maintainer. In this case, I allowed it because I have a one-year Greptile subscription, but please ask for permission next time.

  2. I noticed too many emojis and long messages in the PR. I also know which model was used. I am letting this slide, but in the future, keep messages short. Just tag me and briefly explain what you changed. I will review it.

  3. Avoid unnecessary discussions in the PR. This PR already has more than 60 comments, which makes it heavy and looks bad from a maintainer’s point of view.

I can see from your GitHub profile that you are new to open-source contributions
Take this as a learning experience

There is no bad intent here as if you keep PRs clean and professional, maintainers are more likely to review your work

@AnkanMisra

AnkanMisra commented Jan 5, 2026 •

Copy link
Copy Markdown
Owner

@SujalTripathi
Anyways reviewed and code look and ran all the test looks good
Thanks for the contribution 🙌

Please give a star to this repo

@AnkanMisra
AnkanMisra merged commit 1de5ae1 into AnkanMisra:main Jan 5, 2026
8 checks passed
@SujalTripathi

Copy link
Copy Markdown
Contributor Author

okay sir @AnkanMisra thank you so much for your valuable words, i will keep in mind from next time! Thanks :)

@SujalTripathi

Copy link
Copy Markdown
Contributor Author

Hey @AnkanMisra please rewrite the label!

@AnkanMisra

Copy link
Copy Markdown
Owner

@SujalTripathi ??

@SujalTripathi

Copy link
Copy Markdown
Contributor Author

@AnkanMisra wrong spelling of medium!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

level:intermediate Moderate scope requiring project familiarity or cross-file changes. SWoC26

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants