Skip to content

Commit

Permalink
feat: allow to enable Early Hints and better Server Push config
Browse files Browse the repository at this point in the history
  • Loading branch information
dunglas committed Oct 16, 2023
1 parent 6329c52 commit bc91ce2
Show file tree
Hide file tree
Showing 12 changed files with 185 additions and 99 deletions.
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<h1 align="center"><img src="vulcain.svg" alt="Vulcain: Use HTTP/2 Server Push to create fast and idiomatic client-driven REST APIs" title="Use HTTP/2 Server Push to create fast and idiomatic client-driven REST APIs"></h1>

Vulcain is a brand new protocol using HTTP/2 Server Push to create fast and idiomatic **client-driven REST** APIs.
Vulcain is a brand new protocol using Preload hints and the `103 Early Hints` status code to create fast and idiomatic **client-driven REST** APIs.

An open source gateway server which you can put on top of **any existing web API** to instantly turn it into a Vulcain-compatible one is also provided!
An open source gateway server (a module for the [Caddy web server](https://caddyserver.com)), which you can put on top of **any existing web API** to instantly turn it into a Vulcain-compatible API is also provided!

It supports [hypermedia APIs](https://restfulapi.net/hateoas/) but also any "legacy" API by documenting its relations [using OpenAPI](docs/gateway/openapi.md).
It supports [hypermedia APIs](https://restfulapi.net/hateoas/) (e.g. any API created with [API Platform](https://api-platform.com)) but also any "legacy" API by documenting its relations [using OpenAPI](docs/gateway/openapi.md).

[![Plant Tree](https://img.shields.io/badge/dynamic/json?color=brightgreen&label=Plant%20Tree&query=%24.total&url=https%3A%2F%2Fpublic.offset.earth%2Fusers%2Ftreeware%2Ftrees)](https://plant.treeware.earth/dunglas/vulcain)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/dunglas/vulcain/gateway)](https://pkg.go.dev/github.com/dunglas/vulcain)
Expand All @@ -20,7 +20,7 @@ Grab What You Need... Burn The REST!
* [Pushing Relations](#pushing-relations)
* [Filtering Resources](#filtering-resources)
* [Gateway Server](docs/gateway/)
* [Caddy Web Server Module](docs/caddy.md)
* [Caddy Web Server Module](docs/gateway/caddy.md)
* [Mapping a Non-Hypermedia API using OpenAPI](docs/gateway/openapi.md)
* [Legacy Standalone Server](docs/gateway/install.md)
* [Legacy Configuration](docs/gateway/config.md)
Expand All @@ -33,7 +33,7 @@ Grab What You Need... Burn The REST!

The protocol has been published as [an Internet Draft](https://datatracker.ietf.org/doc/draft-dunglas-vulcain/) that [is maintained in this repository](spec/vulcain.md).

A reference, production-grade, implementation [**gateway server**](docs/gateway/install.md) is also available in this repository.
A reference, production-grade, implementation [**gateway server**](docs/gateway/caddy.md) is also available in this repository.
It's free software (AGPL) written in Go. A Docker image is provided.

## Introduction
Expand Down Expand Up @@ -90,14 +90,14 @@ Considering the following resources:
}
```

The `Preload` HTTP header introduced by Vulcain can be used to ask the server to immediately push resources related to the requested one using HTTP/2 Server Push:
The `Preload` HTTP header introduced by Vulcain can be used to ask the server to immediately push resources related to the requested one using 103 Early Hints or HTTP/2 Server Push:

```http
GET /books/ HTTP/2
Preload: "/member/*/author"
```

In addition to `/books`, a Vulcain server will use HTTP/2 Server Push to push the `/books/1`, `/books/2` and `/authors/1` resources!
In addition to `/books`, a Vulcain server will push the `/books/1`, `/books/2` and `/authors/1` resources!

Example in JavaScript:

Expand All @@ -112,14 +112,15 @@ const authorResp = await fetch(bookJSON.author);

[Full example, including collections](fixtures/static/main.js), see also [use GraphQL as query language for Vulcain](docs/graphql.md#using-graphql-as-query-language-for-vulcain).

Thanks to [HTTP/2 multiplexing](https://stackoverflow.com/a/36519379/1352334), pushed responses will be sent in parallel.
Thanks to [HTTP/2+ multiplexing](https://stackoverflow.com/a/36519379/1352334), pushed responses will be sent in parallel.

When the client will follow the links and issue a new HTTP request (for instance using `fetch()`), the corresponding response will already be in cache, and will be used instantly!

For non-hypermedia APIs (when the identifier of the related resource is a simple string or int), [use an OpenAPI specification to configure links between resources](docs/gateway/openapi.md).
Tip: the easiest way to create a hypermedia API is to use [the API Platform framework](https://api-platform.com) (by the same author as Vulcain).

[More than 90% of users](https://caniuse.com/#feat=http2) have devices supporting HTTP/2. However, for the remaining 10%, and for cases where using HTTP/2 Server Push isn't allowed such as when resources are [served by different authorities](https://tools.ietf.org/html/rfc7540#section-10.1), Vulcain allows to gracefully fallback to [`preload` links](https://www.w3.org/TR/preload/), which can be used together with [the 103 status code](https://tools.ietf.org/html/rfc8297).
When possible, we recommend using [Early Hints](https://tools.ietf.org/html/rfc8297) (the 103 HTTP status code) to push the relations.
Vulcain allows to gracefully fallback to [`preload` links](https://www.w3.org/TR/preload/) in the headers of the final response or to [HTTP/2 Server Push](https://tools.ietf.org/html/rfc7540#section-10.1) when the 103 status code isn't supported.

### Query Parameter

Expand Down
27 changes: 19 additions & 8 deletions caddy/caddy.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ type Vulcain struct {
OpenAPIFile string `json:"openapi_file,omitempty"`
// Maximum number of resources to push
MaxPushes int `json:"max_pushes,omitempty"`
// To eable 103 Early Hints responses
EarlyHints bool `json:"early_hints,omitempty"`

vulcain *vulcain.Vulcain
logger *zap.Logger
Expand All @@ -53,11 +55,17 @@ func (v *Vulcain) Provision(ctx caddy.Context) error {

v.logger = ctx.Logger(v)

v.vulcain = vulcain.New(
options := []vulcain.Option{
vulcain.WithOpenAPIFile(v.OpenAPIFile),
vulcain.WithMaxPushes(v.MaxPushes),
vulcain.WithLogger(ctx.Logger(v)),
)
}

if v.EarlyHints {
options = append(options, vulcain.WithEarlyHints())
}

v.vulcain = vulcain.New(options...)

return nil
}
Expand Down Expand Up @@ -106,12 +114,12 @@ func (v Vulcain) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhtt

// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
//
// vulcain {
// # path to the OpenAPI file describing the relations (for non-hypermedia APIs)
// openapi_file <path>
// # Maximum number of pushes to do (-1 for unlimited)
// max_pushes -1
// }
// vulcain {
// # path to the OpenAPI file describing the relations (for non-hypermedia APIs)
// openapi_file <path>
// # Maximum number of pushes to do (-1 for unlimited)
// max_pushes -1
// }
func (v *Vulcain) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
for d.NextBlock(0) {
Expand All @@ -134,6 +142,9 @@ func (v *Vulcain) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}

v.MaxPushes = maxPushes

case "early_hints":
v.EarlyHints = true
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions caddy/fixtures/Caddyfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
debug
order vulcain before request_header
}

localhost:3000

route {
localhost:3000 {
vulcain {
openapi_file ../../fixtures/openapi.yaml
max_pushes 100
early_hints
}
reverse_proxy localhost:8080
}
44 changes: 0 additions & 44 deletions docs/caddy.md

This file was deleted.

59 changes: 59 additions & 0 deletions docs/gateway/caddy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Vulcain for Caddy

The Vulcain Gateway Server can be put in front of **any existing REST API** to transform it in a Vulcain-enabled API.
It works with hypermedia APIs ([JSON-LD](https://json-ld.org), [JSON:API](https://jsonapi.org/), [HAL](https://tools.ietf.org/html/draft-kelly-json-hal), [Siren](https://github.com/kevinswiber/siren) ...) as well as [with other non-hypermedia APIs by configuring the server using a subset of the OpenAPI specification](openapi.md).

Tip: the easiest way to create a hypermedia API is to use [the API Platform framework](https://api-platform.com) (by the same author than Vulcain).

The Gateway Server is a module for [the Caddy server](https://caddyserver.com): a powerful, enterprise-ready, open source web server with automatic HTTPS written in Go.

The Vulcain module for Caddy allows to turn any existing web API in a one supporting all features of Vulcain in a few minutes.

## Install

1. Go on [the Caddy server download page](https://caddyserver.com/download)
2. Select the `github.com/dunglas/vulcain/caddy` module
3. Select other modules you're interested in such as [the cache module](https://github.com/caddyserver/cache-handler) or [Mercure](https://mercure.rocks) (optional)
4. Download and enjoy!

Alternatively, you can use [xcaddy](https://github.com/caddyserver/xcaddy) to create a custom build of Caddy containing the Vulcain module: `xcaddy build --with github.com/dunglas/vulcain/caddy`

## Configuration

Example configuration:

```caddyfile
{
order vulcain before request_header
}
example.com {
vulcain {
openapi_file my-openapi-description.yaml # optional
max_pushes 100 # optional
early_hints # optional, usually not necessary
}
reverse_proxy my-api:8080 # all other handlers such as the static file server and custom handlers are also supported
}
```

## Start the Server

Just run `./caddy run`.

## Cache Handler

Vulcain is best used with an HTTP cache server. The Caddy and the Vulcain team maintain together a [distributed HTTP cache module](github.com/caddyserver/cache-handler) built on top of [Souin](https://github.com/darkweak/souin) supporting most of the RFC.

## 103 Early Hints

The gateway server can trigger 103 "Early Hints" responses including Preload hints automatically.
However, enabling this feature is usually useless because the gateway server doesn't supports JSON streaming (yet).

Consequently the server will have to wait for the full JSON response to be received from upstream before being able to compute the Link headers to send.

When the full response is available, we can send the final response directly.

For best performance, better send Early Hints responses as soon as possible, directly from the upstream application.

The gateway server will automatically and instantly forward all 103 responses coming from upstream, even if the `early_hints` directive is not set.
3 changes: 3 additions & 0 deletions docs/gateway/config.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# Gateway Server Configuration

**This is the documentation of the legacy standalone Gateway Server. This server is deprecated and will be removed at some point. You should use [the Caddy module instead](caddy.md).**

The Vulcain Gateway Server follows [the twelve-factor app methodology](https://12factor.net/) and is configurable using [environment variables](https://en.wikipedia.org/wiki/Environment_variable):

| Variable | Description |
|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `UPSTREAM` | the URL of the API |
| `OPENAPI_FILE` | the path to an OpenAPI v3 file containing Link definitions |
| `MAX_PUSHES` | the maximum number of resources to push (`0` to disabled and only generate Link preload headers) |
| `EARLY_HINTS` | instructs the gateway server to send Preload hints in 103 Early Hints response. Enabling this setting is usually useless because the gateway server doesn't supports JSON streaming yet, consequently the server will have to wait for the full JSON response to be received from upstream before being able to compute the Link headers to send. When the full response is available, we can send the final response directly. Better send Early Hints responses as soon as possible, directly from the upstream application. The proxy will forward them even if this option is not enabled. |
| `ACME_CERT_DIR` | the directory where to store Let's Encrypt certificates |
| `ACME_HOSTS` | a comma separated list of hosts for which Let's Encrypt certificates must be issued |
| `ADDR` | the address to listen on (example: `127.0.0.1:3000`, default to `:http` or `:https` depending if HTTPS is enabled or not). Note that Let's Encrypt only supports the default port: to use Let's Encrypt, **do not set this variable**. |
Expand Down
4 changes: 2 additions & 2 deletions docs/gateway/install.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Install The Gateway Server

**This is the documentation of the legacy standalone Gateway Server. This server is deprecated and will be removed at some point. You should use [the Caddy module instead](caddy.md).**

The Vulcain Gateway Server can be put in front of **any existing REST API** to transform it in a Vulcain-enabled API.
It works with hypermedia APIs ([JSON-LD](https://json-ld.org), [JSON:API](https://jsonapi.org/), [HAL](https://tools.ietf.org/html/draft-kelly-json-hal), [Siren](https://github.com/kevinswiber/siren) ...) as well as [with other non-hypermedia APIs by configuring the server using a subset of the OpenAPI specification](openapi.md).

Tip: the easiest way to create a hypermedia API is to use [the API Platform framework](https://api-platform.com) (by the same author than Vulcain).

**The Gateway Server is still experimental**

## Prebuilt Binary

First, download the archive corresponding to your operating system and architecture [from the release page](https://github.com/dunglas/vulcain/releases), extract the archive and open a shell in the resulting directory.
Expand Down
7 changes: 6 additions & 1 deletion server.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,14 @@ func NewServer(options *ServerOptions) *server {
panic(err)
}

opt := []Option{WithOpenAPIFile(options.OpenAPIFile), WithMaxPushes(options.MaxPushes), WithLogger(logger)}
if options.EarlyHints {
opt = append(opt, WithEarlyHints())
}

return &server{
options: options,
vulcain: New(WithOpenAPIFile(options.OpenAPIFile), WithMaxPushes(options.MaxPushes), WithLogger(logger)),
vulcain: New(opt...),
}
}

Expand Down
4 changes: 4 additions & 0 deletions server_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type ServerOptions struct {
Debug bool
Addr string
Upstream *url.URL
EarlyHints bool
MaxPushes int
AcmeHosts []string
AcmeCertDir string
Expand Down Expand Up @@ -60,10 +61,13 @@ func NewOptionsFromEnv() (*ServerOptions, error) {
}
}

earlyHints := os.Getenv("EARLY_HINTS")

o := &ServerOptions{
os.Getenv("DEBUG") == "1",
os.Getenv("ADDR"),
upstream,
earlyHints != "" && earlyHints != "0",
maxPushes,
splitVar(os.Getenv("ACME_HOSTS")),
os.Getenv("ACME_CERT_DIR"),
Expand Down
2 changes: 2 additions & 0 deletions server_options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
func TestNewOptionsFromEnv(t *testing.T) {
testEnv := map[string]string{
"UPSTREAM": "http://example.com",
"EARLY_HINTS": "1",
"MAX_PUSHES": "-1",
"ACME_CERT_DIR": "/tmp",
"ACME_HOSTS": "example.com,example.org",
Expand All @@ -35,6 +36,7 @@ func TestNewOptionsFromEnv(t *testing.T) {
true,
"127.0.0.1:8080",
u,
true,
-1,
[]string{"example.com", "example.org"},
"/tmp",
Expand Down

0 comments on commit bc91ce2

Please sign in to comment.