Skip to main content

Connect Protocol Reference

This document specifies the Connect protocol for making RPCs over HTTP. The protocol does not depend on framing details specific to a particular HTTP version.

Rules use ABNF syntax, but the design goals and summary are approachable for casual readers. There are also examples of unary and streaming RPCs.

Design Goals

This protocol aims to:

  • Be human-readable and debuggable with general-purpose HTTP tools, especially for unary RPCs.
  • Remain conceptually close to gRPC's HTTP/2 protocol, so Connect implementations can support both protocols.
  • Depend only on widely-implemented HTTP features and specify behavior in terms of high-level semantics, so that Connect implementations can easily use off-the-shelf networking libraries.

Summary

When used with Protocol Buffer schemas, the Connect protocol supports unary, client streaming, server streaming, and bidirectional streaming RPCs, with either binary Protobuf or JSON payloads. Bidirectional streaming requires HTTP/2, but the other RPC types also support HTTP/1.1. The protocol doesn't use HTTP trailers at all, so it works with any networking infrastructure.

Unary RPCs use the application/proto and application/json Content-Types and look similar to a stripped-down REST dialect. All requests are POSTs, paths are derived from the Protobuf schema, request and response bodies are valid Protobuf or JSON (without gRPC-style binary framing), and responses have meaningful HTTP status codes. For example:

> POST /buf.greet.v1.GreetService/Greet HTTP/1.1
> Host: demo.connect.build
> Content-Type: application/json
>
> {"name": "Buf"}

< HTTP/1.1 200 OK
< Content-Type: application/json
<
< {"greeting": "Hello, Buf!"}

The unary ABNF rules describe how metadata (like timeouts and compression schemes) are encoded into HTTP headers and explain the details of the error model. The examples below also show a wider variety of scenarios.

Streaming RPCs are naturally a bit more complex: they use the application/connect+proto and application/connect+json Content-Types and look similar to gRPC-Web. Requests are still POSTs and paths are still derived from the Protobuf schema, but each request and response message is wrapped in a few bytes of binary framing data. Responses always have an HTTP status of 200 OK, with any errors sent in the last portion of the body. For example, a client streaming call might look like this:

> POST /buf.greet.v1.GreetService/GreetGroup HTTP/1.1
> Host: demo.connect.build
> Content-Type: application/connect+json
>
> <binary framing: standard message>{"name": "Buf"}
> <binary framing: standard message>{"name": "Connect"}

< HTTP/1.1 200 OK
< Content-Type: application/connect+json
<
< <binary framing: standard message>{"greeting": "Hello, Buf and Connect!"}
< <binary framing: error and trailers>{}

Again, the ABNF rules describe the streaming HTTP headers, error model, and framing data. The examples below show both successes and errors.

Outline

  • Request Unary-Request / Streaming-Request
  • Unary-Request Unary-Request-Headers Bare-Message
  • Streaming-Request Streaming-Request-Headers *Enveloped-Message

Clients send HTTP requests to servers. Unary requests contain exactly one message, while streaming requests contain zero or more messages.

  • Response Unary-Response / Streaming-Response
  • Unary-Response Unary-Response-Headers Bare-Message
  • Streaming-Response Streaming-Response-Headers 1*Enveloped-Message

Servers return HTTP responses to clients. Unary responses contain exactly one message, while streaming responses contain one or more messages.

The rules in this document use HTTP/2-style notation (for example, ":method POST" and ":path /foo/bar" instead of "POST /foo/bar HTTP/1.1"). On the wire, Connect implementations must represent these semantics appropriately for the HTTP version in use.

Unary (Request-Response) RPCs

Most RPCs are unary (or request-response). Structurally, unary RPC is similar to the resource-oriented model of the web. The Connect protocol takes special care to make unary RPCs easy to work with using web browsers, cURL, and other general-purpose HTTP tools.

Unary-Request

  • Unary-Request Unary-Request-Headers Bare-Message
  • Unary-Request-Headers Unary-Call-Specification *Leading-Metadata
  • Unary-Call-Specification Method Path Unary-Content-Type [Timeout] [Content-Encoding] [Accept-Encoding]
  • Method ":method POST" ; see Future Extensions
  • Path ":path" "/" [Routing-Prefix "/"] Procedure-Name ; case-sensitive
  • Routing-Prefix {arbitrary prefix}
  • Procedure-Name {IDL-specific service & method name} ; see Protocol Buffers
  • Unary-Content-Type "content-type" "application/" ("proto" / "json" / {custom})
  • Timeout "connect-timeout-ms" Timeout-Milliseconds
  • Timeout-Milliseconds {positive integer as ASCII string of at most 10 digits}
  • Content-Encoding "content-encoding" Content-Coding
  • Content-Coding "identity" / "gzip" / "br" / "zstd" / {custom}
  • Accept-Encoding "accept-encoding" Content-Coding *("," [" "] Content-Coding) ; subset of HTTP quality value syntax
  • Leading-Metadata Custom-Metadata
  • Custom-Metadata ASCII-Metadata / Binary-Metadata
  • ASCII-Metadata Header-Name ASCII-Value
  • Binary-Metadata {Header-Name "-bin"} {base64-encoded value}
  • Header-Name 1*( %x30-39 / %x61-7A / "_" / "-" / ".") ; 0-9 a-z _ - .
  • ASCII-Value 1*( %x20-%x7E ) ; space & printable ASCII
  • Bare-Message *{binary octet}

Unary-Request-Headers are sent as and have the same semantics as HTTP headers. Servers may respond with an error if the client sends too many headers.

If the server doesn't support the specified Unary-Content-Type, it must respond with an HTTP status code of 415 Unsupported Media Type.

Following standard HTTP semantics, servers must assume "identity" if the client omits Content-Encoding. If the client omits Accept-Encoding, servers must assume that the client accepts the Content-Encoding used for the request. Servers must assume that all clients accept "identity" as their least preferred encoding. Server implementations may choose to accept the full HTTP quality value syntax for Accept-Encoding, but client implementations must restrict themselves to sending the easy-to-parse subset outlined here. Servers should treat Accept-Encoding as an ordered list, with the client's most preferred encoding first and least preferred encoding last. If the client uses an unsupported Content-Encoding, servers should return an error with code "unimplemented" and a message listing the supported encodings.

If Timeout is omitted, the server should assume an infinite timeout. The protocol accommodates timeouts of more than 100 days. Client implementations may set a default timeout for all RPCs, and server implementations may clamp timeouts to an appropriate maximum.

HTTP doesn't allow header values to be arbitrary binary blobs, so Connect differentiates between ASCII-Metadata and Binary-Metadata. Binary headers must use keys ending in "-bin", and implementations should emit unpadded base64-encoded values. Implementations must accept both padded and unpadded values. Because binary and non-ASCII headers are relatively uncommon, implementations may represent HTTP headers using an off-the-shelf type rather than reifying these rules in a custom type. Implementations using an off-the-shelf type should prominently document these rules.

Bare-Message is the RPC request payload, serialized using the codec indicated by Unary-Content-Type and possibly compressed using Content-Encoding. It's sent on the wire as the HTTP request content (often called the body).

Unary-Response

  • Unary-Response Unary-Response-Headers Bare-Message
  • Unary-Response-Headers HTTP-Status Unary-Content-Type [Content-Encoding] [Accept-Encoding] *Leading-Metadata *Prefixed-Trailing-Metadata
  • HTTP-Status ":status" ("200" / {error code translated to HTTP})
  • Prefixed-Trailing-Metadata Prefixed-ASCII-Metadata / Prefixed-Binary-Metadata
  • Prefixed-ASCII-Metadata Prefixed-Header-Name ASCII-Value
  • Prefixed-Binary-Metadata {Prefixed-Header-Name "-bin"} {base64-encoded value}
  • Prefixed-Header-Name "trailer-" Header-Name

Unary-Response-Headers are sent as and have the same semantics as HTTP headers. This includes Prefixed-Trailing-Metadata: though it's sent on the wire alongside Leading-Metadata, support for trailing metadata lets Connect implementations use common interfaces for streaming and unary RPC. Implementations must transparently prefix trailing metadata keys with "trailer-" when writing data to the wire and strip the prefix when reading data from the wire.

If Content-Encoding is omitted, clients must assume "identity". Servers must either respond with an error or use a Content-Encoding supported by the client.

Successful responses have an HTTP-Status of 200. In those cases, Unary-Content-Type is the same as the request's Unary-Content-Type. Bare-Message is the RPC response payload, serialized using the codec indicated by Unary-Content-Type and possibly compressed using Content-Encoding. It's sent on the wire as the HTTP response content (often called the body).

Errors are sent with a non-200 HTTP-Status. In those cases, Unary-Content-Type must be "application/json". Bare-Message is either omitted or a JSON-serialized Error, possibly compressed using Content-Encoding and sent on the wire as the HTTP response content. If Bare-Message is an Error, HTTP-Status must match Error.code as specified in the table below. When reading data from the wire, client implementations must use the HTTP-to-Connect mapping to infer a Connect error code if Bare-Message is missing or malformed.

Examples

Using HTTP/1.1 notation, a simple request and successful response:

> POST /buf.greet.v1.GreetService/Greet HTTP/1.1
> Host: demo.connect.build
> Content-Type: application/json
>
> {"name": "Buf"}

< HTTP/1.1 200 OK
< Content-Type: application/json
<
< {"greeting": "Hello, Buf!"}

The same RPC, but with with a 5s timeout, asymmetric compression, and some custom leading and trailing metadata.

> POST /buf.greet.v1.GreetService/Greet HTTP/1.1
> Host: demo.connect.build
> Content-Type: application/json
> Accept-Encoding: gzip, br
> Connect-Timeout-Ms: 5000
> Acme-Shard-Id: 42
>
> {"name": "Buf"}

< HTTP/1.1 200 OK
< Content-Type: application/json
< Content-Encoding: gzip
< Trailer-Acme-Operation-Cost: 237
<
< <gzipped JSON>

The same RPC again, but with a Protobuf-encoded request and an error response:

> POST /buf.greet.v1.GreetService/Greet HTTP/1.1
> Host: demo.connect.build
> Content-Type: application/proto
>
> <uncompressed binary Protobuf>

< HTTP/1.1 404 Not Found
< Content-Type: application/json
<
< {
< "code": "unimplemented",
< "message": "buf.greet.v1.GreetService/Greet is not implemented"
< }

Streaming RPCs

Streaming RPCs may be half- or full-duplex. In server streaming RPCs, the client sends a single message and the server responds with a stream of messages. In client streaming RPCs, the client sends a stream of messages and the server responds with a single message. In bidirectional streaming RPCs, both the client and server send a stream of messages. Depending on the Connect implementation, IDL, and HTTP version in use, some or all of these streaming RPC types may be unavailable.

Streaming-Request

  • Streaming-Request Streaming-Request-Headers *Enveloped-Message
  • Streaming-Request-Headers Streaming-Call-Specification *Leading-Metadata
  • Streaming-Call-Specification Method Path Streaming-Content-Type [Timeout] [Streaming-Content-Encoding] [Streaming-Accept-Encoding]
  • Streaming-Content-Type "content-type" "application/connect+" ("proto" / "json" / {custom})
  • Streaming-Content-Encoding "connect-content-encoding" Content-Coding
  • Streaming-Accept-Encoding "connect-accept-encoding" Content-Coding *("," [" "] Content-Coding)
  • Enveloped-Message Envelope-Flags Message-Length Message
  • Envelope-Flags %d0-255 ; 8 bitwise flags encoded as 1 byte unsigned integer
  • Message-Length {length of Message} ; encoded as 4 byte unsigned integer, big-endian
  • Message *{binary octet}

If Streaming-Content-Type does not begin with "application/connect+", servers should respond with an HTTP status of 415 Unsupported Media Type. This prevents HTTP clients unaware of Connect's semantics from interpreting a streaming error response, which uses an HTTP Status of 200 OK, as successful.

Servers must interpret Streaming-Content-Encoding and Streaming-Accept-Encoding using the same inference and error-reporting rules as Content-Encoding and Accept-Encoding.

The HTTP request content is a sequence of zero or more Enveloped-Message. The first byte is Envelope-Flags, a set of 8 bitwise flags.

  • If the least significant bit is 1, the Message is compressed using the algorithm specified in Streaming-Content-Encoding. If Streaming-Content-Encoding is omitted or "identity", this bit must be 0. Compression contexts are not maintained over message boundaries.
  • If the next least significant bit is 1, the Message is an EndStreamResponse rather than the response type defined in the service's IDL. Response streams must set this bit on the final Enveloped-Message in the stream, and must leave this bit unset on all other messages in the stream. Request streams must always leave this bit unset. (Note that this is not the same as the gRPC-Web protocol, which uses the most significant bit to mark trailers.)
  • The six most significant bits are reserved for future protocol extensions.

Streaming-Response

  • Streaming-Response Streaming-Response-Headers 1*Enveloped-Message
  • Streaming-Response-Headers ":status 200" Streaming-Content-Type [Streaming-Content-Encoding] [Streaming-Accept-Encoding] *Leading-Metadata

Streaming responses always have an HTTP status of 200 OK. As noted above, server implementations must send an EndStreamResponse as the final message in the stream, and must not send an EndStreamResponse earlier in the stream.

Examples

Using HTTP/1.1 notation and putting each Enveloped-Message on a separate line for readability, a successful client streaming RPC:

> POST /buf.greet.v1.GreetService/GreetGroup HTTP/1.1
> Host: demo.connect.build
> Content-Type: application/connect+json
>
> <flags: 0><length: 15>{"name": "Buf"}
> <flags: 0><length: 19>{"name": "Connect"}

< HTTP/1.1 200 OK
< Content-Type: application/connect+json
<
< <flags: 0><length: 39>{"greeting": "Hello, Buf and Connect!"}
< <flags: 2><length: 2>{}

A failed server streaming RPC:

> POST /buf.greet.v1.GreetService/GreetIndividuals HTTP/1.1
> Host: demo.connect.build
> Content-Type: application/connect+proto
>
> <flags: 0><length: 8><binary proto>

< HTTP/1.1 200 OK
< Content-Type: application/connect+proto
<
< <flags: 2><length: 58>{"error": {"code": "unavailable", "message": "overloaded"}}

Error Codes

Connect represents categories of errors as codes, and each code maps to a specific HTTP status code. The codes and their semantics were chosen to match gRPC. Only the codes below are valid there are no user-defined codes.

CodeHTTP StatusDescription
canceled408 Request TimeoutRPC canceled, usually by the caller.
unknown500 Internal Server ErrorCatch-all for errors of unclear origin and errors without a more appropriate code.
invalid_argument400 Bad RequestRequest is invalid, regardless of system state.
deadline_exceeded408 Request TimeoutDeadline expired before RPC could complete or before the client received the response.
not_found404 Not FoundUser requested a resource (for example, a file or directory) that can't be found.
already_exists409 ConflictCaller attempted to create a resource that already exists.
permission_denied403 ForbiddenCaller isn't authorized to perform the operation.
resource_exhausted429 Too Many RequestsOperation can't be completed because some resource is exhausted. Use unavailable if the server is temporarily overloaded and the caller should retry later.
failed_precondition412 Precondition FailedOperation can't be completed because the system isn't in the required state.
aborted409 ConflictThe operation was aborted, often because of concurrency issues like a database transaction abort.
out_of_range400 Bad RequestThe operation was attempted past the valid range.
unimplemented404 Not FoundThe operation isn't implemented, supported, or enabled.
internal500 Internal Server ErrorAn invariant expected by the underlying system has been broken. Reserved for serious errors.
unavailable503 Service UnavailableThe service is currently unavailable, usually transiently. Clients should back off and retry idempotent operations.
data_loss500 Internal Server ErrorUnrecoverable data loss or corruption.
unauthenticated401 UnauthorizedCaller doesn't have valid authentication credentials for the operation.

When choosing between invalid_argument, failed_precondition, and out_of_range, use invalid_argument if the failure is independent of the system state. For example, attempting to seek to a file offset larger than 2^32-1 on a 32-bit system should return invalid_argument. Use out_of_range as a common sub-category of failed_precondition, so clients iterating through a space can detect when they're done. For example, attempting to seek past the end of a particular file should return out_of_range.

When choosing between unauthorized, permission_denied, resource_exhausted, and not_found, use unauthenticated if the user can't be identified or presents invalid credentials. Use resource_exhausted if a per-user quota (for example, a rate limit) is exhausted. Use not_found if an operation is denied for a class of users (for example, because of a gradual rollout or an undocumented allowlist). Use permission_denied for other authorization-based rejections.

When choosing between failed_precondition, aborted, and unavailable, use unavailable if the client can back off and retry the operation. Use aborted if the client should retry at a higher level (for example, restarting a read-modify-write cycle). Use failed_precondition if the client must explicitly fix the system state before retrying (for example, by emptying a directory before attempting to remove it).

Clients must exercise judgment when deciding which errors to retry there's no set of error codes which are safe to retry for all applications. Typically, services explicitly identify idempotent methods in their IDL.

If clients receive a response with non-200 HTTP status codes and no explicit Connect error code, they should infer a Connect code using the following table. This mapping is a superset of gRPC's: in all cases where gRPC maps an HTTP status code to a specific gRPC status code (that is, something other than unknown or internal), Connect maps to the semantically-equivalent error code.

HTTP to Error Code

HTTP StatusInferred Code
400 Bad Requestinvalid_argument
401 Unauthorizedunauthenticated
403 Forbiddenpermission_denied
404 Not Foundunimplemented
408 Request Timeoutdeadline_exceeded
409 Conflictaborted
412 Precondition Failedfailed_precondition
413 Payload Too Largeresource_exhausted
415 Unsupported Media Typeinternal
429 Too Many Requestsunavailable
431 Request Header Fields Too Largeresource_exhausted
502 Bad Gatewayunavailable
503 Service Unavailableunavailable
504 Gateway Timeoutunavailable
all othersunknown

Error and EndStreamResponse

Connect serializes errors and the block of data at the end of each response stream using JSON. This keeps errors human-readable and easy to debug.

An Error is a code, an optional message, and an optional array of details. Code and message (if present) are UTF-8 strings. When message is omitted or the empty string, clients may synthesize a user-facing message (and thereby avoid representing the message as an optional or nullable type, if such types exist in the implementation language). {"code": null} and {} are invalid. The simplest form of Error contains just a code:

{
"code": "unavailable"
}

Details are an optional mechanism for servers to attach strongly-typed messages to errors. Each detail is an object with "type" and "value" properties and any number of other properties. The "type" field contains the fully-qualified Protobuf message name as a UTF-8 string, and the "value" field contains unpadded, base64-encoded binary Protobuf data. For readability on the wire, server implementations may also serialize the detail to JSON and include the resulting object under the "debug" key. Clients must not depend on data in the "debug" key when deserializing details.

{
"code": "unavailable",
"message": "overloaded: back off and retry",
"details": [
{
"type": "google.rpc.RetryInfo",
"value": "CgIIPA",
"debug": {"retryDelay": "30s"},
}
]
}

An EndStreamMessage is the final message in streaming response. It conveys whether or not the RPC succeeded and any trailing metadata. Trailing metadata is modeled as an object: keys follow Header-Name, and values are arrays of UTF-8 strings. Each string is either an ASCII-Value or a base64-encoded binary value. Semantically, trailing metadata should be treated as HTTP headers: keys are case-insensitive, values for the same key can be joined with commas, and so on. (In practice, Connect implementations typically deserialize trailing metadata into the same data structure used for HTTP headers.)

{
"error": {
"code": "unavailable"
},
"metadata": {
"acme-operation-cost": ["237"]
}
}

Failed RPCs must provide an Error in the "error" property, and successful RPCs must omit the property. {"error": null}, {"error": {}}, and {"error": {"code": null}} are invalid. The "metadata" property is optional. After a successful RPC, EndStreamResponse can be as simple as {}.

Protocol Buffers

When used with Protocol Buffer IDL,

  • Procedure-Name ?( {proto package name} "." ) {service name} "/" {method name}
  • Unary-Content-Type "content-type application/" ("proto" / "json")
  • Streaming-Content-Type "content-type application/connect+" ("proto" / "json")

Choose the "proto" content types for binary serialization and the "json" types to use the canonical JSON mapping.

Protocol Buffers support unary RPCs and all three types of streaming.

Potential Future Extensions

  • Support for GET requests. This would allow Connect to interoperate with standard HTTP caches, but requires some way to deserialize request data from query parameters and URLs. Ideally, this wouldn't require verbose schema annotations or artisanally-crafted HTTP paths. We map the unimplemented error code to 404 Not Found, rather than 501 Not Implemented, in part because HTTP strongly discourages servers from returning 501s for GET requests.