Skip to content
Created by

Get Requests and Caching

Connect supports performing idempotent, side-effect free requests using an HTTP GET-based protocol. This makes it easier to cache certain kinds of requests in the browser, on your CDN, or in proxies and other middleboxes.

If you are using clients to make query-style requests, you may want the ability to use Connect HTTP GET request support. To opt-in for a given procedure, you must mark it as being side-effect free using the MethodOptions.IdempotencyLevel option:

service GreetService {
rpc Greet(GreetRequest) returns (GreetResponse) {
option idempotency_level = NO_SIDE_EFFECTS;
}
}

Handlers will automatically support GET requests using this option.

It is still necessary to opt-in to HTTP GET on your client, as well. If you are using a Go client, you would specify the connecthttp.WithHTTPGet option when creating the transport.

client := greetv1connect.NewGreetServiceClient(
connect.NewClient(
connecthttp.NewTransport(
http.DefaultClient,
"http://localhost:8080",
connecthttp.WithHTTPGet(),
),
),
)

Methods annotated as side-effect free will use GET requests. All other requests will continue to use POST.

For other clients, see their respective documentation pages:

Using GET requests will not necessarily automatically make browsers or proxies cache your RPCs. To ensure that requests are allowed to be cached, a handler should also set the appropriate headers.

For example, you may wish to set the Cache-Control header with a max-age directive:

callInfo, ok := connect.CallInfoForServerContext(ctx)
if ok {
callInfo.ResponseHeader().Set("Cache-Control", "max-age=604800")
}

This would instruct agents and proxies that the request may be cached for up to 7 days, after which it must be re-requested. There are other Cache-Control Response Directives that may be useful for your application as well; for example, the private directive would specify that the request should only be cached in private caches, such as the user agent itself, and not CDNs or reverse proxies—this would be appropriate, for example, for authenticated requests.

Handlers can also support HTTP conditional requests. Set an Etag header identifying the current version of the response. When a client repeats the request, its cache sends that value back in the If-None-Match header. If the ETag still matches, the handler can skip its work and return connecthttp.NewNotModifiedError() to tell the client its cached copy is still fresh:

callInfo, ok := connect.CallInfoForServerContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeInternal, "no call info in context")
}
callInfo.ResponseHeader().Set("Etag", etag)
serverInfo, ok := connecthttp.ServerInfoForContext(ctx)
if ok && serverInfo.HTTPMethod() == http.MethodGet &&
callInfo.RequestHeader().Get("If-None-Match") == etag {
return nil, connecthttp.NewNotModifiedError()
}
// ...build the response as usual...

“Not modified” is not a failure. Like io.EOF, it’s a sentinel error used as a signal, because a handler has no other way to return without a response message. On the wire it becomes HTTP’s 304 Not Modified status, which caches treat as a successful revalidation. Clients detect the signal with connecthttp.IsNotModifiedError and reuse the cached response.

In some cases, you might want to introduce behavior that only occurs when handling HTTP GET requests. This can be accomplished using the HTTPMethod method on the connecthttp.ServerInfo type in context:

callInfo, ok := connect.CallInfoForServerContext(ctx)
info, httpOK := connecthttp.ServerInfoForContext(ctx)
if ok && httpOK && info.HTTPMethod() == http.MethodGet {
callInfo.ResponseHeader().Set("Cache-Control", "max-age=604800")
}