Skip to content

Conversation

@sedkis
Copy link
Contributor

@sedkis sedkis commented Jan 22, 2026

Response Plugins Run on All Responses

Problem Statement

Response middleware only executes on successful upstream responses. When Tyk returns errors (401 auth, 429 rate limit, etc.), response middleware is bypassed. Customers want to customize responses on a per-API basis for both success and error cases.

Solution: Response Plugins Always Run

Extend response middleware to run on all responses, not just upstream success. The plugin sees every response and decides what to do based on status code.

Success (upstream 2xx)  -> Response Plugin -> Client
Upstream error (5xx)    -> Response Plugin -> Client  (already works)
Tyk error (401/429)     -> Response Plugin -> Client  (NEW)

One plugin, all responses.

Implementation

Primary change: gateway/handler_error.go

In HandleError(), build the error body into a buffer, run the response chain on a constructed http.Response, and only then write the response.

Key points:

  • Create response with status, cloned headers, body, and request.
  • Run handleResponseChain only when configured and not already in an error-response chain (guard).
  • If a custom response hook fails, let its own ErrorHandler response stand and return early.
  • Log response chain errors when not handled.
  • Sync headers by clearing w.Header() and copying from response.Header so deletions apply.
  • Reset response.Body to the final buffer so analytics can read it.

Guard: prevent recursion

When ErrorHandler is invoked from a response hook failure, skip re-running the response chain by setting a request-context flag while the chain is executing.

Edge case: errCustomBodyResponse

Keep existing behavior. GraphQL and similar flows already wrote to the response writer, so there is nothing to intercept.

Tests

  • gateway/handler_error_test.go: global response header add/remove is applied to gateway errors (ex: 401). Confirms response chain runs and header removals are respected.
  • gateway/handler_error_test.go: response hook failure does not re-enter the response chain. Confirms recursion guard and single response write.

Considerations / Follow-ups

  • Ensure response.Body is closed after reading from the response chain in case a hook swaps in a streaming body.
  • Add a regression test for the errCustomBodyResponse path to confirm it bypasses the response chain as intended.
  • Document the behavior change: response plugins now run on gateway-generated errors (401/429/etc.) and can alter status/body/headers.

Summary

What Change
Files modified 1 to 3 (handler + tests + plan)
New config None
New plugin type None

Response plugins become universal response interceptors with safe recursion guards.

…ases (401, 429). Update `HandleError()` to execute response chain for Tyk errors, ensuring customization of status, body, and headers. Add tests for response chain behavior and recursion guard. Document changes in `plan.md`.
@github-actions
Copy link
Contributor

🚨 Jira Linter Failed

Commit: 93af6f5
Failed at: 2026-01-22 19:49:31 UTC

The Jira linter failed to validate your PR. Please check the error details below:

🔍 Click to view error details
failed to validate branch and PR title rules: PR title must contain the Jira ticket ID 'TT-16477'

Next Steps

  • Ensure your branch name contains a valid Jira ticket ID (e.g., ABC-123)
  • Verify your PR title matches the branch's Jira ticket ID
  • Check that the Jira ticket exists and is accessible

This comment will be automatically deleted once the linter passes.

@probelabs
Copy link

probelabs bot commented Jan 22, 2026

This PR enhances the gateway's response middleware functionality by enabling response plugins to execute on all responses, including gateway-generated errors such as 401 (Unauthorized) and 429 (Rate Limit Exceeded). Previously, these plugins would only run on successful or error responses from an upstream service.

Files Changed Analysis

The changes are concentrated in two files:

  • gateway/handler_error.go: The core logic resides here. The HandleError function is significantly refactored to buffer the error response, construct an http.Response object, and then pass it through the response plugin chain (handleResponseChain). This allows plugins to inspect and modify the status, headers, and body of gateway-generated errors before they are sent to the client. A crucial addition is a recursion guard using a request context to prevent infinite loops if a response plugin itself triggers an error.
  • gateway/handler_error_test.go: This file contains a substantial addition of test cases (over 450 lines) to validate the new behavior. The tests cover various scenarios, including header manipulation, body and status code modification by plugins, the effectiveness of the recursion guard, and ensuring analytics correctly record the final response details.

Architecture & Impact Assessment

  • What this PR accomplishes: It unifies the response handling pipeline, providing a single, consistent mechanism for customizing all API responses, regardless of their origin (upstream success, upstream error, or gateway error).

  • Key technical changes:

    1. Error Response Interception: In HandleError, the error response is now built in-memory instead of being written directly to the client.
    2. Plugin Chain Execution: An http.Response object is created from the in-memory error and processed by the existing response middleware chain.
    3. Recursion Guard: A context-based flag (responseChainContextKeyValue) is set during the response chain execution to prevent re-invocation if a plugin fails and calls HandleError again.
    4. State Synchronization: Logic has been added to synchronize headers between the original ResponseWriter and the modified http.Response object to ensure the final output is consistent.
  • Affected system components: The primary component affected is the gateway's error handling middleware. This change impacts all APIs that utilize response plugins, as those plugins will now execute in more scenarios.

  • Flow Diagram:

    sequenceDiagram
        participant Client
        participant Gateway
        participant ResponsePlugins
    
        Client->>Gateway: Request
        Gateway-->>Gateway: Error detected (e.g., Auth Failure)
        Note over Gateway: Old Flow: Write error directly to client
        Gateway--xClient: HTTP 401 Unauthorized
    
        %% New Flow
        Note over Gateway: New Flow
        Gateway->>Gateway: 1. Generate error response in buffer
        Gateway->>ResponsePlugins: 2. Execute response chain with error
        ResponsePlugins-->>Gateway: 3. Return potentially modified response
        Gateway->>Client: 4. Write final response to client
    
    Loading

Scope Discovery & Context Expansion

  • The change is surgically applied to the gateway's error handler but has a broad functional impact. It establishes a new execution path for the handleResponseChain function, which is the central orchestrator for response middleware.
  • This enhancement requires plugin developers to be aware that their code will now process gateway-generated errors. Plugins may need to be updated to inspect the response status code and apply logic conditionally.
  • The new tests confirm that downstream systems like analytics are correctly integrated, logging the final, post-plugin response data, which is critical for maintaining accurate metrics.
Metadata
  • Review Effort: 3 / 5
  • Primary Label: enhancement

Powered by Visor from Probelabs

Last updated: 2026-01-22T19:52:25.171Z | Triggered by: pr_opened | Commit: 93af6f5

💡 TIP: You can chat with Visor using /visor ask <your question>

@github-actions
Copy link
Contributor

API Changes

no api changes detected

@probelabs
Copy link

probelabs bot commented Jan 22, 2026

Security Issues (1)

Severity Location Issue
🟡 Warning gateway/handler_error.go:211-214
The code reads the entire response body from a response plugin using `io.ReadAll` without any size limit. A buggy or malicious plugin could provide an infinitely streaming body, causing the gateway to exhaust its memory and leading to a Denial of Service (DoS). The gateway's error handling mechanism should be resilient against misbehaving plugins.
💡 SuggestionLimit the amount of data read from the plugin-provided response body. Wrap `response.Body` in an `io.LimitedReader` before calling `io.ReadAll` to enforce a maximum size for the error response body. A sane default limit, such as 1MB, should be used.

Architecture Issues (1)

Severity Location Issue
🟡 Warning gateway/handler_error.go:207-210
An error from reading the modified response body from a response plugin is silently ignored. If a plugin provides a faulty response body stream (e.g., an error-producing io.ReadCloser), the gateway will fall back to sending the original error body without any indication that something went wrong in the response chain.
💡 SuggestionLog the error from `io.ReadAll` to provide visibility into failures within response plugins. This will aid in debugging custom plugins that may have issues generating a response body.

Performance Issues (1)

Severity Location Issue
🟡 Warning gateway/handler_error.go:212-215
The response body from the response chain is fully read into memory using `io.ReadAll`. If a response plugin returns a large or streaming body, this will cause high memory consumption and negate the benefits of streaming. While error responses are typically small, plugins can now modify them, potentially making them large.
💡 SuggestionTo support streaming and reduce memory usage, consider using `io.TeeReader` to simultaneously write the response to the client and capture it for analytics, instead of buffering the entire body in memory first. This would involve streaming the body directly to the `http.ResponseWriter` using `io.Copy` after the headers have been written.

Quality Issues (2)

Severity Location Issue
🔴 Critical gateway/handler_error_test.go:170-449
The `HandleError` function is called with a boolean as the last argument, but the function signature in `gateway/handler_error.go` expects `*http.Response`. This will cause a compilation error.
💡 SuggestionThe test calls seem to be based on a previous function signature, for example `handler.HandleError(recorder, req, MsgAuthFieldMissing, http.StatusUnauthorized, true)`. The function signature in `handler_error.go` is `func (e *ErrorHandler) HandleError(w http.ResponseWriter, r *http.Request, errMsg string, errCode int, response *http.Response)`. All calls to this function in the test file need to be updated to pass an `*http.Response` object instead of a boolean. It seems the tests were not updated to reflect the new design where an `http.Response` object is passed in to be populated and processed by the response chain.
🟡 Warning gateway/handler_error_test.go:251-448
The function `ioutil.ReadAll` is used in multiple tests. This function has been deprecated since Go 1.16. The `io.ReadAll` function should be used instead.
💡 SuggestionReplace all instances of `ioutil.ReadAll(resp.Body)` with `io.ReadAll(resp.Body)`. The main application code in `handler_error.go` already uses `io.ReadAll`, so the tests should be consistent.
🔧 Suggested Fix
bodyBytes, err := io.ReadAll(resp.Body)

Powered by Visor from Probelabs

Last updated: 2026-01-22T19:52:28.090Z | Triggered by: pr_opened | Commit: 93af6f5

💡 TIP: You can chat with Visor using /visor ask <your question>

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant