Wire Contracts

Status: DRAFT v0.1

Everything in this document crosses a process boundary. These are the contracts that make two Benzene implementations — in any two languages, on any two vendors — interoperable. From spec 1.0, changes here are breaking changes.

All JSON field names below are camelCase unless stated otherwise. All header keys are case-insensitive on read and SHOULD be written lower-case.

1. The Benzene message envelope

The transport-neutral message format, used whenever a Benzene client sends to a Benzene service over a transport with no richer native contract (direct function invocation, queues without attribute support, the generic BenzeneMessage entry point).

1.1 Request

{
  "topic": "order:create",
  "headers": { "x-correlation-id": "…", "traceparent": "…" },
  "body": "{ …serialized message… }"
}
Field Type Rules
topic string Required. The topic id (see core-concepts §2). Version, when used, travels as a header.
headers object (string→string) Required, may be empty. Flat string map — no nested values.
body string Required. The message payload, pre-serialized as a string (JSON by default), not an inline object. This keeps the envelope schema fixed regardless of payload schema.

(Informative: earlier .NET versions had the outbound Lambda client sending this field as message while the inbound entry point read body — corrected to body on both sides; body is normative.)

1.2 Response

{
  "statusCode": "Ok",
  "headers": { },
  "body": "{ …serialized response… }"
}
Field Type Rules
statusCode string A status vocabulary value (§3) — the Benzene status, not an HTTP code. Clients MAY additionally tolerate numeric HTTP codes here for interop with older or HTTP-shaped services, but MUST NOT write them.
headers object (string→string) Response headers, including content-type when set.
body string Pre-serialized response payload: on success, the handler's response payload; on failure, the error payload (§1.3).

1.3 Error payload

When a result is unsuccessful, the response body is the serialized error payload — a problem-details-shaped object:

{
  "status": "NotFound",
  "detail": "No handler found for topic order:create"
}
Field Type Rules
status string The Benzene status, repeated from the envelope.
detail string The result's error messages, joined with ", ".
type, title, instance string? Reserved (RFC 7807 alignment); writers MAY emit them as null or omit them.

Clients recover errors from detail; a missing/empty detail yields an error-free failed result.

2. Header conventions

Headers are the portable metadata channel. Every transport binding maps its native metadata (HTTP headers, gRPC metadata, SQS/SNS message attributes, Kafka headers, the envelope's headers field) to and from this flat string→string dictionary.

Header Direction Meaning
traceparent, tracestate both W3C Trace Context, verbatim per the W3C spec. This is Benzene's cross-service correlation contract.
x-correlation-id outbound Legacy correlation value, written by the outbound correlation client decorator when the application populates one. Implementations are NOT required to read it inbound (the legacy inbound pickup middleware was removed pre-1.0); honoring a partner's correlation header is application middleware, not a framework contract.
topic inbound (queue transports) On transports where the envelope isn't used but native attributes exist (SQS/SNS message attributes), the topic travels as an attribute named topic.
_benzeneHeaders both (EventBridge) On transports with no native per-message attributes (EventBridge), wire headers travel as a reserved string→string object named _benzeneHeaders at the top level of the payload (detail), embedded by the sender only when headers exist and the payload is a JSON object, and lifted back out by the receiver.
benzene-status outbound (gRPC trailer) See §4.2.
content-type outbound Response content type where the transport has no native slot for it.
benzene-version both Draft, not yet implemented — the request/response payload's schema version, for topics using payload versioning. See versioning.md.

Binary metadata (e.g. gRPC -bin keys) is excluded from the dictionary in both directions. Duplicate keys: last value wins.

3. Status vocabulary

The closed set of framework-defined statuses. The strings below are the wire values — they are PascalCase and case-sensitive.

Status Success? Meaning
Ok yes Handled successfully
Created yes Resource created
Accepted yes Accepted for asynchronous processing
Updated yes Resource updated
Deleted yes Resource deleted
Ignored yes Deliberately not processed (e.g. filtered); not an error
BadRequest no Malformed or invalid request
ValidationError no Semantically invalid request (validation rules failed)
Unauthorized no Caller not authenticated
Forbidden no Caller authenticated but not permitted
NotFound no Target not found (including: no handler registered for the topic)
Conflict no State conflict
TooManyRequests no Throttled / rate limited; transient — back off and retry
Timeout no A downstream deadline elapsed; transient, but the operation may or may not have been applied, so blind retries are only safe for idempotent operations
NotImplemented no Recognized but unsupported operation
ServiceUnavailable no Transient infrastructure failure; retryable. Also the mapping for uncaught handler exceptions and client-side send failures.
UnexpectedError no Unclassified failure

Applications MAY use additional status strings; every mapping table below routes unknown statuses to its generic-error row.

4. Per-protocol status mappings

4.1 HTTP

Benzene status HTTP
Ok, Ignored 200
Created 201
Accepted 202
Updated, Deleted 204
BadRequest 400
Unauthorized 401
Forbidden 403
NotFound 404
Conflict 409
ValidationError 422
TooManyRequests 429
UnexpectedError, unknown, missing 500
NotImplemented 501
ServiceUnavailable 503
Timeout 504

Reverse (HTTP → Benzene, used by HTTP clients): 200→Ok, 201→Created, 202→Accepted, 204→Deleted, 400→BadRequest, 401→Unauthorized, 403→Forbidden, 404→NotFound, 408→Timeout, 409→Conflict, 422→ValidationError, 429→TooManyRequests, 501→NotImplemented, 502→ServiceUnavailable, 503→ServiceUnavailable, 504→Timeout, anything else→UnexpectedError.

4.2 gRPC

Forward (server):

Benzene status gRPC StatusCode
Ok, Ignored, Created, Accepted, Updated, Deleted OK
BadRequest, ValidationError InvalidArgument
Unauthorized Unauthenticated
Forbidden PermissionDenied
NotFound NotFound
Conflict AlreadyExists
NotImplemented Unimplemented
ServiceUnavailable Unavailable
TooManyRequests ResourceExhausted
Timeout DeadlineExceeded
UnexpectedError, unknown, missing Internal

The benzene-status trailer: because several Benzene statuses collapse to one gRPC code, a Benzene gRPC server MUST attach a response trailer benzene-status carrying the raw status string verbatim, on success and failure alike. A missing result maps the trailer value to Unknown. Non-OK outcomes are surfaced as a gRPC error with the mapped code and a detail string of the joined errors (or the raw status if errors is empty).

Reverse (client): a benzene-status trailer, when present, wins verbatim. Otherwise: OKOk, InvalidArgumentBadRequest, UnauthenticatedUnauthorized, PermissionDeniedForbidden, NotFoundNotFound, AlreadyExistsConflict, UnimplementedNotImplemented, Unavailable/CancelledServiceUnavailable, ResourceExhaustedTooManyRequests, DeadlineExceededTimeout, anything else→UnexpectedError.

Cancellation: a cancelled invocation maps to gRPC DeadlineExceeded if the call's deadline has passed, else Cancelled.

5. Health check response

Returned for the reserved topic healthcheck (and any app-configured alias):

{
  "isHealthy": true,
  "healthChecks": {
    "Database": {
      "status": "ok",
      "type": "Database",
      "data": { "CanConnect": true }
    }
  }
}

gRPC hosts additionally expose the same aggregate over standard grpc.health.v1: SERVING iff no check failed (a warning maps to a degraded-but-serving state).

6. Serialization defaults