Skip to content

Improve HTTP Codec: SSE GET Request Generation and POST Endpoint Routing #176

@bettercallsaulj

Description

@bettercallsaulj

Issue Details

Problem Description

The HTTP codec filter still couldn't properly communicate with MCP servers because:

  1. No SSE GET request generation: The HTTP codec didn't know how to generate a GET /sse request with proper SSE headers (Accept: text/event-stream, Cache-Control: no-cache)
  2. Hardcoded POST path: POST requests always went to /rpc regardless of the message endpoint received from the server
  3. No URL path extraction: When the server sends endpoint event with full URL like http://host:8080/api/message, the codec couldn't extract just the path /api/message
  4. SSE body chunks not forwarded: For SSE streams (which never "complete"), body data chunks weren't being forwarded to callbacks, causing messages to be lost
  5. State machine blocking: The codec blocked requests while receiving response body, but SSE streams have continuous body data

Root Cause

The HttpCodecFilter::onWrite() method was designed for simple request/response patterns:

  • Always generated POST requests
  • Used hardcoded path /rpc and host localhost
  • Required body data (empty buffer = early return)
  • Blocked new requests while receiving response body

SSE pattern requires:

  • Initial GET request with no body
  • Configurable path/host
  • POST to different path than GET
  • Ability to send requests while SSE stream is active

Technical Flow (Before Fix)

Client                                Server
  |                                     |
  |-- POST /rpc (wrong!) -------------->|  ❌ Should be GET /sse
  |                                     |
  |   (even if endpoint event received) |
  |-- POST /rpc ----------------------->|  ❌ Should be POST /api/message
  |                                     |

Technical Flow (After Fix)

Client                                Server
  |                                     |
  |-- GET /sse HTTP/1.1 --------------->|  ✅ SSE initialization
  |   Accept: text/event-stream         |
  |   Cache-Control: no-cache           |
  |                                     |
  |<-- event: endpoint -----------------|  (handled by filter chain)
  |    data: http://host/api/message    |
  |                                     |
  |-- POST /api/message HTTP/1.1 ------>|  ✅ Correct path extracted
  |   {"jsonrpc":"2.0",...}             |

How to Reproduce

Prerequisites

  • gopher-mcp client with HTTP/SSE transport
  • SSE-based MCP server that sends endpoint events

Steps to Reproduce

1. Configure client for SSE endpoint:

HttpCodecFilter filter(callbacks, dispatcher, false /* client mode */);

// Before fix: These methods didn't exist or didn't work
filter.setClientEndpoint("/sse", "localhost:8080");
filter.setUseSseGet(true);

2. Attempt to send initial request:

OwnedBuffer buffer;
// Empty buffer should trigger SSE GET
filter.onWrite(buffer, false);

// Before fix: buffer is empty, returns immediately
// After fix: buffer contains "GET /sse HTTP/1.1\r\n..."

3. Observe the failure (before fix):

Expected: GET /sse HTTP/1.1
          Accept: text/event-stream
          ...

Actual:   (nothing - early return on empty buffer)

4. After receiving endpoint event, send POST:

filter.setMessageEndpoint("http://localhost:8080/api/message");

OwnedBuffer post_buffer;
post_buffer.add(R"({"jsonrpc":"2.0","method":"test","id":1})");
filter.onWrite(post_buffer, false);

// Before fix: POST /rpc HTTP/1.1 (hardcoded path)
// After fix: POST /api/message HTTP/1.1 (extracted from endpoint)

Minimal Reproduction Test

TEST(HttpCodecFilter, SseGetAndPostRouting) {
  HttpCodecFilter filter(callbacks, dispatcher, false);
  filter.setClientEndpoint("/sse", "localhost:8080");
  filter.setUseSseGet(true);

  // Step 1: Generate SSE GET
  OwnedBuffer get_buffer;
  filter.onWrite(get_buffer, false);

  std::string get_req = get_buffer.toString();
  // Before fix: FAILS - get_req is empty
  EXPECT_NE(get_req.find("GET /sse HTTP/1.1"), std::string::npos);
  EXPECT_NE(get_req.find("Accept: text/event-stream"), std::string::npos);

  // Step 2: Set endpoint (simulating endpoint event)
  filter.setMessageEndpoint("http://localhost:8080/api/message");

  // Step 3: Send POST
  OwnedBuffer post_buffer;
  post_buffer.add(R"({"jsonrpc":"2.0"})");
  filter.onWrite(post_buffer, false);

  std::string post_req = post_buffer.toString();
  // Before fix: FAILS - uses /rpc instead of /api/message
  EXPECT_NE(post_req.find("POST /api/message HTTP/1.1"), std::string::npos);
}

Key Changes in the Fix

Component Change
onWrite() early return Allow empty buffers when use_sse_get_ is true and GET not yet sent
State machine check Allow requests in ReceivingResponseBody state for SSE streams
SSE GET generation Generate proper GET request with SSE headers when triggered
POST path routing Extract path from message_endpoint_ URL, fall back to client_path_
URL path extraction Parse http://host:port/path to extract just /path
Body forwarding Call onBody() callback immediately in client mode for SSE chunks

New Filter Methods

// Configure client endpoint for requests
void setClientEndpoint(const std::string& path, const std::string& host);

// Store POST endpoint from SSE "endpoint" event
void setMessageEndpoint(const std::string& endpoint);
bool hasMessageEndpoint() const;
const std::string& getMessageEndpoint() const;

// Enable SSE GET mode
void setUseSseGet(bool use_sse_get);
bool hasSentSseGetRequest() const;

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions