Skip to content

HTTP Client — Common Spec

Spec table of contents | Previous: Channel Messaging | Next: SPOT Messaging

This document defines the boundary for registering and calling an HTTP client in Framework. It owns identity, the fluent builder form, the execution terminator, the combination with the Spot execution context, and the codec and common error model.

The detailed contract (builder, response, redirect/retry/cookie, auth/TLS/proxy, compression, error mapping, regression) is each owned by 01-11 in the same folder.

The exact per-language type and signature is owned by languages/<lang>/. language-interfaces is a non-normative cross-reference table viewing the five languages side by side — it doesn't fix a contract.

1. Identity — A Framework Companion Client

The HTTP client sits in the same place as the STREAM connector. It's distributed as a separate package, but it's a framework-dedicated companion client, and the contract is owned by framework.

STREAM Connector HTTP Client
Package Separate Separate
Contract ownership Framework common spec (32) Framework common spec (this document)
Per-language interface stream-connector/languages/<lang>/ http-client/languages/<lang>/

There's exactly one reason it exists — a framework application must be able to call an external API and legacy API in zlink style. It isn't meant to replace a general-purpose HTTP library.

01 Scope And Architecture §1.3 owns the Framework contracts consumed by the HTTP client and the per-language deliverable boundaries. The Spot execution context is connected through the execution scheduler injection point in §3.2.

2. Fluent Builder

Same form as framework messaging — "select operation → configure → execution-mode terminator".

client.post("/games")               // operation
      .header("x-request-id", ...)  // configure
      .query("region", "kr")
      .body(createGameReq)
      .timeout(3s)
      .submit<CreateGameRes>()      // C++/Java's response completion terminator
                                    // Node uses async<CreateGameRes>()
  • 7 verbs: get / post / put / delete / patch / head / options.
  • Configuration axis: header, query, timeout, body source (typed / raw / streaming / form / multipart).
  • Body sources are mutually exclusive. Mixing them is ProtocolError.

The builder's detailed contract (path format, percent-encoding, per-body-source retry availability, etc.) is owned by 03 Request Builder.

3. Execution Terminator — One-Way And Response Completion (+ Callback)

The HTTP client's completion surface is one-way submission, response completion, and callback. The exact name is .NET's Async, Kotlin wrapper's await, Java/C++'s submit. Node uses submitRaw for raw response, async for typed response and callback, and submit for one-way. TypeScript inheritance signature constraints are owned by each language's exact interface. Yield, which returns the shared Spot gate, is only provided to a server request and Worker call, and isn't included in the HTTP request builder (04 §1.1).

Execution Mode What It Waits For Spot Execution Queue
one-way submission Waits until the HTTP request is submitted at the transport boundary Keeps the current turn. No normal completion value
response completion Waits until the HTTP response arrives Keeps the current turn

Callback is a separate completion path for a caller that doesn't use an awaitable (a CLI, an event-loop-based client). The HTTP client provides that path together.

Using a callback in a Spot execution context proceeds without waiting for the call, and the completion callback is queued as a new turn of that Spot execution queue. If the completion value must continue the same turn's judgment, use the per-language response completion terminator instead of a callback.

3.1 How To Return The Spot Gate While Waiting For External HTTP

An HTTP client call itself doesn't return the shared Spot gate. If another Spot work item must proceed while waiting for an external API during Actor entry/exit, run the HTTP client's response completion terminator in an I/O Worker and wait with the Worker call's Yield.

var profile = await Context
    .RunIoWorker(async workerCancellation =>
        await http.Get($"/players/{id}").Fetch<Profile>(workerCancellation))
    .Yield(ct);

The HTTP request builder doesn't provide a Yield terminal. Gate return and reacquisition is owned by the server runtime's Worker call, so the HTTP package doesn't judge the Spot execution context.

3.2 The Turn Seam — A Single Injection Point

The HTTP client knows framework's error kind and codec, but not the Spot's turn. The only thing that knows the turn is a single injected execution scheduler.

  • The HTTP client puts an execution scheduler injection point as a public contract. The scheduler decides where to resume completion.
  • Framework injects the callback completion scheduler at DI registration. The callback enters as a new turn of the original Spot execution queue.
  • Neither DI nor standalone use exposes Yield on the HTTP request builder. Only the per-language response completion terminator and callback are used.

The C++ HTTP client expresses the same scheduler seam with coroutines(resume_scheduler) and framework_resume_scheduler_t.

3.3 Doesn't Put A Blocking Terminator

Doesn't build a public terminator that synchronously unwraps the completion value (04 §2). A blocking alternative terminator of the same meaning is a contract violation. If a synchronous wait is needed in a test or CLI, the caller wraps it directly with a language idiom (GetAwaiter().GetResult(), runBlocking, .join()).

4. Server Surface And Registration

The HTTP client used in a server (Spot handler/channel handler) is injected through DI. Don't build a client with a static factory inside a handler — it loses the connection pool and turn seam.

  • The application registers it by name. Since baseUrl, auth, timeout, and retry policy differ per service, framework doesn't auto-register one default client.
  • The registration surface's form is the same as channel registration: name and policy are registered together at the configuration stage, and a handler is injected by that name.
  • The static factory entry point stays client-side-only. Used by a CLI and client scenario.
Surface Who Uses It Terminator
Static factory CLI · client scenario response completion / callback
DI-injected client Spot handler · server code one-way / response completion / callback

5. Codec

The HTTP client shares a codec extension with framework, but keeps a separate registry instance (Stream Session §5). The same codec extension object can be registered on both, but registration must be done separately per host.

Typed body encode/decode is handled by that registry. The raw body API doesn't go through the registry.

6. Error Model

The HTTP client doesn't build its own exception hierarchy. It uses the framework common error model's (Framework Error Model) error kind as is. It doesn't create a new HTTP-client-dedicated error kind.

Situation Kind
Configuration/usage error, typed decode, decompression, or redirect format error ProtocolError
Network, DNS, proxy, and target connection failure Unavailable
Configured response body byte limit exceeded CapacityExceeded
Per-attempt timeout exceeded DeadlineExceeded
HTTP status 400 or above, or an unclassifiable execution failure InternalFailure

Detailed mapping is owned by 09 Error Model.

7. Regression Test

Item Verification
Terminator axis Has one-way and response completion and callback completion paths, and doesn't have a blocking-unwrap terminator or an HTTP Yield
Turn preservation While a Spot handler waits for response completion, another callback of the same Spot doesn't start
Turn return Only returns the shared Spot gate when HTTP response completion runs inside RunIoWorker and waits with the Worker Yield
Surface limit Neither DI nor standalone use exposes Yield on the HTTP request builder
Registration The server surface is obtained only through DI injection, and a client isn't built with a static factory inside a handler
Error kind There's no HTTP-client-dedicated kind — only framework common kind is used
builder Mixing body sources fails with ProtocolError