vulcain

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 9, 2023 License: AGPL-3.0 Imports: 26 Imported by: 1

README

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 (a module for the Caddy web server), 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 (e.g. any API created with API Platform) but also any "legacy" API by documenting its relations using OpenAPI.

Plant Tree PkgGoDev Build Status codecov Go Report Card

[tabs]

Preload

Vulcain Schema

Preload + Early Hints

Vulcain Schema

Server push

Vulcain Schema

[/tabs]

Grab What You Need... Burn The REST!

The protocol has been published as an Internet Draft that is maintained in this repository.

A reference, production-grade, implementation gateway server is also available in this repository. It's free software (AGPL) written in Go. A Docker image is provided.

Introduction

Over the years, several formats have been created to fix performance bottlenecks impacting web APIs: over fetching, under fetching, the n+1 problem...

Current solutions for these problems (GraphQL, JSON:API's embedded resources and sparse fieldsets, ...) are smart network hacks for HTTP/1. But these hacks come with (too) many drawbacks when it comes to HTTP cache, logs and even security.

Fortunately, thanks to the new features introduced in HTTP/2, it's now possible to create true REST APIs fixing these problems with ease and class! Here comes Vulcain!

See also the comparison between Vulcain and GraphQL and other API formats.

Pushing Relations

[tabs]

Preload

Preload Schema

Preload + Early Hints

Preload Schema

Server push

Preload Schema

[/tabs]

Considering the following resources:

/books

{
    "member": [
        "/books/1",
        "/books/2"
    ]
}

/books/1

{
    "title": "1984",
    "author": "/authors/1"
}

/books/2

{
    "title": "Homage to Catalonia",
    "author": "/authors/1"
}

/authors/1

{
    "givenName": "George",
    "familyName": "Orwell"
}

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:

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

In addition to /books, a Vulcain server will push the /books/1, /books/2 and /authors/1 resources!

Example in JavaScript:

const bookResp = await fetch("/books/1", { headers: { Preload: `"/author"` } });
const bookJSON = await bookResp.json();

// Returns immediately, the resource has been pushed and is already in the push cache
const authorResp = await fetch(bookJSON.author);
// ...

Full example, including collections, see also use GraphQL as query language for Vulcain.

Thanks to HTTP/2+ multiplexing, 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. Tip: the easiest way to create a hypermedia API is to use the API Platform framework (by the same author as Vulcain).

When possible, we recommend using Early Hints (the 103 HTTP status code) to push the relations. Vulcain allows to gracefully fallback to preload links in the headers of the final response or to HTTP/2 Server Push when the 103 status code isn't supported.

Query Parameter

Alternatively to HTTP headers, the preload query parameter can be used:

[tabs]

Preload

Preload Query Schema

Preload + Early Hints

Preload Query Schema

Server push

Preload Query Schema

[/tabs]

Filtering Resources

[tabs]

Preload

Filter Schema

Preload + Early Hints

Filter Schema

Server push

Filter Schema

[/tabs]

The Fields HTTP header allows the client to ask the server to return only the specified fields of the requested resource, and of the preloaded related resources.

Multiple Fields HTTP headers can be passed. All fields matching at least one of these headers will be returned. Other fields of the resource will be omitted.

Considering the following resources:

/books/1

{
    "title": "1984",
    "genre": "novel",
    "author": "/authors/1"
}

/authors/1

{
    "givenName": "George",
    "familyName": "Orwell"
}

And the following HTTP request:

GET /books/1 HTTP/2
Preload: "/author"
Fields: "/author/familyName", "/genre"

A Vulcain server will return a response containing the following JSON document:

{
    "genre": "novel",
    "author": "/authors/1"
}

It will also push the following filtered /authors/1 resource:

{
    "familyName": "Orwell"
}

Query Parameter

Alternatively to HTTP headers, the fields query parameter can be used to filter resources:

[tabs]

Preload

Fields Schema

Preload + early hints

Fields Schema

Server push

Fields Schema

[/tabs]

See Also

tl;dr:

  • proprietary software can implement the Vulcain specification
  • proprietary software can be used behind the Vulcain Gateway Server without having to share their sources
  • modifications made to the Vulcain Gateway Server must be shared
  • alternatively, a commercial license is available for the Vulcain Gateway Server

The specification is available under the IETF copyright policy. The Vulcain specification can be implemented by any software, including proprietary software.

The Vulcain Gateway Server is licensed under AGPL-3.0. This license implies that if you modify the Vulcain Gateway Server, you must share those modifications. However, the AGPL-3.0 license applies only to the gateway server itself, not to software used behind the gateway.

For companies not wanting, or not able to use AGPL-3.0 licensed software, commercial licenses are also available. Contact us for more information.

Treeware

This package is Treeware. If you use it in production, then we ask that you buy the world a tree to thank us for our work. By contributing to the Treeware forest you’ll be creating employment for local families and restoring wildlife habitats.

Credits

Created by Kévin Dunglas. Sponsored by Les-Tilleuls.coop.

Some ideas and code used in Vulcain's reference implementation have been taken from Hades by Gabe Sullice, an HTTP/2 reverse proxy for JSON:API backend.

See also the prior arts.

Documentation

Overview

Package vulcain helps implementing the Vulcain protocol (https://vulcain.rocks) in Go projects. It provides helper functions to parse HTTP requests containing "preload" and "fields" directives, to extract and push the relations of a JSON document matched by the "preload" directive, and to modify the JSON document according to both directives.

This package can be used in any HTTP handler as well as with httputil.ReverseProxy.

Example
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/http/httptest"
	"net/http/httputil"
	"net/url"

	"github.com/dunglas/vulcain"
)

func main() {
	handler := http.NewServeMux()
	handler.Handle("/books.json", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprintln(w, `{
	"title": "1984",
	"genre": "dystopia",
	"author": "/authors/orwell.json"
}`)
	}))
	handler.Handle("/authors/orwell.json", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprintln(w, `{
			"name": "George Orwell",
			"birthDate": "1903-06-25"
		}`)
	}))

	backendServer := httptest.NewServer(handler)
	defer backendServer.Close()

	rpURL, err := url.Parse(backendServer.URL)
	if err != nil {
		log.Fatal(err)
	}

	vulcain := vulcain.New()

	rpHandler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		r := req.WithContext(vulcain.CreateRequestContext(rw, req))
		var wait bool
		defer func() { vulcain.Finish(r, wait) }()

		rp := httputil.NewSingleHostReverseProxy(rpURL)
		rp.ModifyResponse = func(resp *http.Response) error {
			if !vulcain.IsValidRequest(r) || !vulcain.IsValidResponse(r, resp.StatusCode, resp.Header) {
				return nil
			}

			newBody, err := vulcain.Apply(r, rw, resp.Body, resp.Header)
			if newBody == nil {
				return err
			}

			wait = true
			newBodyBuffer := bytes.NewBuffer(newBody)
			resp.Body = io.NopCloser(newBodyBuffer)

			return nil
		}
		rp.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, err error) {
			wait = false
		}

		rp.ServeHTTP(rw, req)
	})

	frontendProxy := httptest.NewServer(rpHandler)
	defer frontendProxy.Close()

	resp, err := http.Get(frontendProxy.URL + `/books.json?preload="/author"&fields="/title","/author"`)
	if err != nil {
		log.Fatal(err)
	}

	b, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}

	// Go's HTTP client doesn't support HTTP/2 Server Push yet, so a Link rel=preload is added as fallback
	// Browsers and other clients supporting Server Push will receive a push instead
	fmt.Printf("%v\n\n", resp.Header.Values("Link"))
	fmt.Printf("%s", b)

}
Output:

[</authors/orwell.json>; rel=preload; as=fetch]

{"author":"/authors/orwell.json","title":"1984"}

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewServer deprecated

func NewServer(options *ServerOptions) *server

NewServer creates a Vulcain server

Deprecated: use the Caddy server module or the standalone library instead

func NewServerFromEnv deprecated

func NewServerFromEnv() (*server, error)

NewServerFromEnv creates a server using the configuration set in env vars

Deprecated: use the Caddy server module or the standalone library instead

Types

type Option

type Option func(o *opt)

Option instances allow to configure the library

func WithApiUrl

func WithApiUrl(apiUrl string) Option

func WithEarlyHints

func WithEarlyHints() Option

WithEarlyHints 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.

func WithLogger

func WithLogger(logger *zap.Logger) Option

WithLogger sets the logger to use

func WithMaxPushes

func WithMaxPushes(maxPushes int) Option

WithMaxPushes sets the maximum number of resources to push There is no limit by default

func WithOpenAPIFile

func WithOpenAPIFile(openAPIFile string) Option

WithOpenAPIFile sets the path to an OpenAPI definition (in YAML or JSON) documenting the relations between resources This option is only useful for non-hypermedia APIs

type ServerOptions deprecated

type ServerOptions struct {
	Debug        bool
	Addr         string
	Upstream     *url.URL
	EarlyHints   bool
	MaxPushes    int
	AcmeHosts    []string
	AcmeCertDir  string
	CertFile     string
	KeyFile      string
	ReadTimeout  time.Duration
	WriteTimeout time.Duration
	Compress     bool
	OpenAPIFile  string
}

ServerOptions stores the server's options

Deprecated: use the Caddy server module or the standalone library instead

func NewOptionsFromEnv deprecated

func NewOptionsFromEnv() (*ServerOptions, error)

NewOptionsFromEnv creates a new option instance from environment It returns an error if mandatory env env vars are missing

Deprecated: use the Caddy server module or the standalone library instead

type Vulcain

type Vulcain struct {
	// contains filtered or unexported fields
}

Vulcain is the entrypoint of the library Use New() to create an instance

func New

func New(options ...Option) *Vulcain

New creates a Vulcain instance

func (*Vulcain) Apply

func (v *Vulcain) Apply(req *http.Request, rw http.ResponseWriter, responseBody io.Reader, responseHeaders http.Header) ([]byte, error)

Apply pushes the requested relations, modifies the response headers and returns a modified response to send to the client. It's the responsibility of the caller to use the updated response body. Apply must not be called if IsValidRequest or IsValidResponse return false.

func (*Vulcain) CreateRequestContext

func (v *Vulcain) CreateRequestContext(rw http.ResponseWriter, req *http.Request) context.Context

CreateRequestContext assign the waitPusher used by other functions to the request context. CreateRequestContext must always be called first.

func (*Vulcain) Finish

func (v *Vulcain) Finish(req *http.Request, wait bool)

Finish cleanups the waitPusher and, if it's the explicit response, waits for all PUSH_PROMISEs to be sent before returning. Finish must always be called, even if IsValidRequest or IsValidResponse returns false. If the current response is the explicit one and wait is false, then the body is sent instantly, even if all PUSH_PROMISEs haven't been sent yet.

func (*Vulcain) IsValidRequest

func (v *Vulcain) IsValidRequest(req *http.Request) bool

IsValidRequest tells if this request contains at least one Vulcain directive. IsValidRequest must always be called before Apply.

func (*Vulcain) IsValidResponse

func (v *Vulcain) IsValidResponse(req *http.Request, responseStatus int, responseHeaders http.Header) bool

IsValidResponse checks if Apply will be able to deal with this response.

Directories

Path Synopsis
caddy module
cmd
api

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL