Skip to content

[Refactoring]: Introduce HttpServerResponseBuilder for Server Transports #119

Description

@MisterVVP

Context

PR #116 added required-extension validation and response header echoing for REST and JSON-RPC server transports.
That change exposed repeated HttpServerResponse setup across transports:

  • HTTP status assignment.
  • JSON content type header.
  • SSE content type and cache-control headers.
  • A2A-Version response header.
  • Optional A2A-Extensions response header for activated extensions.
  • Conversion from transport-level responses into HttpServerResponse.

The current duplication is small but protocol-sensitive. It is easy to miss a required header when adding a new response path, especially for streaming/SSE responses.

Problem

src/server/rest_server_transport.cpp and src/server/json_rpc_server_transport.cpp both manually construct
HttpServerResponse objects and set common headers. For example:

HttpServerResponse response;
response.status_code = core::http::kStatusOk;
response.headers[std::string(core::http::kContentTypeHeaderName)] =
    std::string(core::http::kContentTypeApplicationJson);
response.headers[std::string(core::Version::kHeaderName)] = core::Version::HeaderValue();
AddActivatedExtensionsHeader(activated_extensions, &response);

Similar setup exists for SSE:

HttpServerResponse response;
response.status_code = core::http::kStatusOk;
response.headers["Content-Type"] = "text/event-stream";
response.headers["Cache-Control"] = "no-cache";
response.headers[std::string(core::Version::kHeaderName)] = core::Version::HeaderValue();

This creates three issues:

  • Header behavior is duplicated and can drift between REST and JSON-RPC transports.
  • Activated extension handling is duplicated through local helper functions.
  • Some protocol literals are still written directly instead of using shared constants.

Proposal

Introduce a lightweight HttpServerResponseBuilder for assembling HttpServerResponse values with consistent common
headers.

This should be a builder, not an abstract factory:

  • We are building one concrete value type: HttpServerResponse.
  • We do not currently need multiple runtime response creation strategies.
  • A virtual factory would add indirection without a demonstrated need.
  • The builder keeps construction explicit while removing repeated boilerplate.

Suggested API Shape

The exact API can be adjusted during implementation, but it should stay simple and value-oriented:

auto response = HttpServerResponseBuilder()
                    .WithStatus(core::http::kStatusOk)
                    .WithJsonContentType()
                    .WithA2aVersion()
                    .WithActivatedExtensions(activated_extensions)
                    .WithBody(body)
                    .Build();

For SSE responses:

auto response = HttpServerResponseBuilder()
                    .WithStatus(core::http::kStatusOk)
                    .WithSseContentType()
                    .WithCacheControlNoCache()
                    .WithA2aVersion()
                    .WithActivatedExtensions(activated_extensions)
                    .Build();

For adapting REST transport responses:

auto response = HttpServerResponseBuilder::FromRestResponse(rest_response)
                    .WithA2aVersion()
                    .WithActivatedExtensions(activated_extensions)
                    .Build();

Scope

Add a new server-side builder component, for example:

  • include/a2a/server/http_server_response_builder.h
  • src/server/http_server_response_builder.cpp

Use it in:

  • src/server/rest_server_transport.cpp
  • src/server/json_rpc_server_transport.cpp
  • optionally src/server/transport_mux.cpp, if it improves consistency without widening the change too much.

The builder should support:

  • Setting status code.
  • Setting body.
  • Setting stream writer.
  • Setting or preserving existing headers.
  • Adding JSON content type.
  • Adding SSE content type.
  • Adding Cache-Control: no-cache.
  • Adding A2A-Version.
  • Adding A2A-Extensions only when activated extensions are non-empty.
  • Converting or wrapping an existing RestResponse.

Constants

Use existing constants where available:

  • core::http::kContentTypeHeaderName
  • core::http::kContentTypeApplicationJson
  • core::http::kContentTypeTextEventStream
  • core::Version::kHeaderName
  • core::Extensions::kHeaderName

Add shared constants if they do not exist yet:

  • Cache-Control
  • no-cache

Avoid introducing new magic strings in production or test code.

Non-Goals

  • Do not introduce a virtual abstract factory.
  • Do not add dependency injection for response creation unless a real use case appears.
  • Do not move JSON-RPC envelope serialization into the builder.
  • Do not move REST error body serialization into the builder.
  • Do not change response bodies, HTTP status codes, streaming behavior, or TCK semantics.
  • Do not refactor unrelated transport logic.

Acceptance Criteria

  • No local AddActivatedExtensionsHeader helper remains in individual transports.
  • Common JSON/SSE response header setup is centralized in HttpServerResponseBuilder.
  • REST and JSON-RPC successful responses still echo activated extensions when required-extension validation succeeds.
  • SSE responses still include:
    • Content-Type: text/event-stream
    • Cache-Control: no-cache
    • A2A-Version
    • A2A-Extensions when activated extensions are present.
  • Existing REST and JSON-RPC behavior remains unchanged.
  • New or updated unit tests cover the builder.
  • Existing functional tests for required-extension behavior continue to pass.
  • ./scripts/verify_changes.sh passes.
  • Mandatory TCK conformance remains 100%.

Suggested Tests

Add focused unit tests for HttpServerResponseBuilder:

  • JSON response includes status, body, JSON content type, and A2A-Version.
  • SSE response includes status, SSE content type, Cache-Control: no-cache, and A2A-Version.
  • Activated extensions are omitted when the activated extension list is empty.
  • Activated extensions are formatted and included when present.
  • Existing headers are preserved when wrapping or adapting a transport response.
  • Builder can preserve a stream writer when adapting a streaming response.

Update existing transport tests only where needed to verify integration with the builder.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions