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.
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.
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.
- 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.
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-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-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.
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 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-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-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.
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"}}
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.
Code | HTTP Status | Description |
---|---|---|
canceled | 408 Request Timeout | RPC canceled, usually by the caller. |
unknown | 500 Internal Server Error | Catch-all for errors of unclear origin and errors without a more appropriate code. |
invalid_argument | 400 Bad Request | Request is invalid, regardless of system state. |
deadline_exceeded | 408 Request Timeout | Deadline expired before RPC could complete or before the client received the response. |
not_found | 404 Not Found | User requested a resource (for example, a file or directory) that can't be found. |
already_exists | 409 Conflict | Caller attempted to create a resource that already exists. |
permission_denied | 403 Forbidden | Caller isn't authorized to perform the operation. |
resource_exhausted | 429 Too Many Requests | Operation can't be completed because some resource is exhausted. Use unavailable if the server is temporarily overloaded and the caller should retry later. |
failed_precondition | 412 Precondition Failed | Operation can't be completed because the system isn't in the required state. |
aborted | 409 Conflict | The operation was aborted, often because of concurrency issues like a database transaction abort. |
out_of_range | 400 Bad Request | The operation was attempted past the valid range. |
unimplemented | 404 Not Found | The operation isn't implemented, supported, or enabled. |
internal | 500 Internal Server Error | An invariant expected by the underlying system has been broken. Reserved for serious errors. |
unavailable | 503 Service Unavailable | The service is currently unavailable, usually transiently. Clients should back off and retry idempotent operations. |
data_loss | 500 Internal Server Error | Unrecoverable data loss or corruption. |
unauthenticated | 401 Unauthorized | Caller 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 Status | Inferred Code |
---|---|
400 Bad Request | invalid_argument |
401 Unauthorized | unauthenticated |
403 Forbidden | permission_denied |
404 Not Found | unimplemented |
408 Request Timeout | deadline_exceeded |
409 Conflict | aborted |
412 Precondition Failed | failed_precondition |
413 Payload Too Large | resource_exhausted |
415 Unsupported Media Type | internal |
429 Too Many Requests | unavailable |
431 Request Header Fields Too Large | resource_exhausted |
502 Bad Gateway | unavailable |
503 Service Unavailable | unavailable |
504 Gateway Timeout | unavailable |
all others | unknown |
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 {}
.
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.
- 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.
Typo with gRPC spec, which is using cancelled with two l: https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc