Skip to content

Commit

Permalink
feat: send 103 Early Hints responses (#95)
Browse files Browse the repository at this point in the history
* feat: send 103 Early Hints responses

* test: simplify tests

* fix: simplify the code and allow easy testing with Caddy

* fix: skip early hints test for now - inconsistent behavior in Go 1.19

* various fixes

* fix flaky test

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
  • Loading branch information
chalasr and dunglas committed Oct 16, 2023
1 parent e3c19e8 commit 6329c52
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 18 deletions.
30 changes: 24 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,35 @@ Install the dependencies:

$ go get

Run the server with [`xcaddy`](https://github.com/caddyserver/xcaddy):

$ cd caddy
$ PATH=".:$PATH" xcaddy run

Run the fixture API:

# You must run the server too
$ cd ../fixtures/
$ go run main.go

Run Caddy with the Vulcain module as a reverse proxy:

$ cd caddy/
$ go run vulcain/main.go

Alternatively, to use VSCode and its integrated debugger, use this configuration in `.vscode/launch.json`:

```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch the proxy",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/caddy/vulcain",
"args": ["run", "--config", "../fixtures/Caddyfile"]
}
]
}
```

Go on `http://localhost:8081` and enjoy!
The API is available on `https://localhost:3000`.

Expand All @@ -40,7 +58,7 @@ To run the test suite:

$ go test -v -timeout 30s github.com/dunglas/vulcain/caddy

## Start a Demo API and Contribute to the Gateway Server
## Start a Demo API and Contribute to the Legacy Gateway Server

Clone the project:

Expand Down
3 changes: 1 addition & 2 deletions caddy/Caddyfile → caddy/fixtures/Caddyfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
{
debug
experimental_http3
}

localhost:3000

route {
vulcain {
openapi_file ../fixtures/openapi.yaml
openapi_file ../../fixtures/openapi.yaml
max_pushes 100
}
reverse_proxy localhost:8080
Expand Down
1 change: 1 addition & 0 deletions caddy/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ replace (
require (
github.com/caddyserver/caddy/v2 v2.7.5
github.com/dunglas/vulcain v0.5.2
go.uber.org/automaxprocs v1.5.3
go.uber.org/zap v1.26.0
)

Expand Down
4 changes: 4 additions & 0 deletions caddy/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
Expand Down Expand Up @@ -625,6 +627,8 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
Expand Down
22 changes: 22 additions & 0 deletions caddy/vulcain/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package main

import (
"github.com/caddyserver/caddy/v2"
caddycmd "github.com/caddyserver/caddy/v2/cmd"

"go.uber.org/automaxprocs/maxprocs"

// plug in Caddy modules here.
_ "github.com/caddyserver/caddy/v2/modules/standard"
_ "github.com/dunglas/vulcain/caddy"
)

//nolint:gochecknoinits
func init() {
//nolint:errcheck
maxprocs.Set(maxprocs.Logger(caddy.Log().Sugar().Debugf))
}

func main() {
caddycmd.Main()
}
44 changes: 42 additions & 2 deletions server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"io"
"net/http"
"net/http/httptest"
"net/http/httptrace"
"net/textproto"
"net/url"
"os"
"os/exec"
Expand Down Expand Up @@ -242,8 +244,46 @@ func TestPreloadHeader(t *testing.T) {
resp, _ := client.Do(req)
b, _ := io.ReadAll(resp.Body)

assert.Equal(t, []string{"</books/1.jsonld>; rel=preload; as=fetch", "</books/2.jsonld>; rel=preload; as=fetch"}, resp.Header["Link"])
assert.Equal(t, []string{"Fields", "Preload"}, resp.Header["Vary"])
assert.ElementsMatch(t, []string{"</books/1.jsonld>; rel=preload; as=fetch", "</books/2.jsonld>; rel=preload; as=fetch"}, resp.Header["Link"])
assert.ElementsMatch(t, []string{"Preload", "Fields"}, resp.Header["Vary"])
assert.Equal(t, `{"hydra:member":[
"/books/1.jsonld",
"/books/2.jsonld"
]}`, string(b))
}

func TestEarlyHints(t *testing.T) {
upstream, gateway := createServers()
defer upstream.Close()
defer gateway.Close()

expectedLinkHeaders := []string{"</books/1.jsonld>; rel=preload; as=fetch", "</books/2.jsonld>; rel=preload; as=fetch"}

// early hint should be sent when a preload header is set
var earlyHintsCount int
trace := &httptrace.ClientTrace{
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
if code == http.StatusEarlyHints {
assert.ElementsMatch(t, expectedLinkHeaders, header["Link"])
earlyHintsCount++
}

return nil
},
}

client := &http.Client{}
req, _ := http.NewRequest("GET", gateway.URL+"/books.jsonld", nil)
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
req.Header.Add("Fields", `"/hydra:member"`)
req.Header.Add("Preload", `"/hydra:member/*"`)

resp, _ := client.Do(req)
b, _ := io.ReadAll(resp.Body)

assert.Equal(t, 1, earlyHintsCount)
assert.ElementsMatch(t, expectedLinkHeaders, resp.Header["Link"])
assert.ElementsMatch(t, []string{"Fields", "Preload"}, resp.Header["Vary"])
assert.Equal(t, `{"hydra:member":[
"/books/1.jsonld",
"/books/2.jsonld"
Expand Down
29 changes: 21 additions & 8 deletions vulcain.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ func (v *Vulcain) Apply(req *http.Request, rw http.ResponseWriter, responseBody
tree.importPointers(fields, f)

var (
oaRoute *routers.Route
oaRouteTested, addPreloadToVary bool
oaRoute *routers.Route
oaRouteTested, usePreloadLinks bool
)
newBody := v.traverseJSON(currentBody, tree, len(f) > 0, func(n *node, val string) string {
var (
Expand All @@ -214,19 +214,32 @@ func (v *Vulcain) Apply(req *http.Request, rw http.ResponseWriter, responseBody
}

if n.preload {
addPreloadToVary = !v.push(u, req, responseHeaders, n, preloadHeader, fieldsHeader)
usePreloadLinks = !v.push(u, rw, req, responseHeaders, n, preloadHeader, fieldsHeader)
}

return newValue
})

if usePreloadLinks {
h := rw.Header()

// If responseHeaders is not the same as rw.Header() (e.g. when using the built-in reverse proxy)
// temporarly copy Link headers to send the 103 response
_, ok := h["Link"]
if !ok {
h["Link"] = responseHeaders["Link"]
}
rw.WriteHeader(http.StatusEarlyHints)
if !ok {
delete(h, "Link")
}
responseHeaders.Add("Vary", "Preload")
}

responseHeaders.Set("Content-Length", strconv.Itoa(len(newBody)))
if fieldsHeader {
responseHeaders.Add("Vary", "Fields")
}
if addPreloadToVary {
responseHeaders.Add("Vary", "Preload")
}

return newBody, nil
}
Expand All @@ -246,13 +259,13 @@ func (v *Vulcain) addPreloadHeader(h http.Header, link string) {

// push pushes a relation or adds a Link rel=preload header as a fallback.
// TODO: allow to set the nopush attribute using the configuration (https://www.w3.org/TR/preload/#server-push-http-2)
// TODO: send 103 early hints responses (https://tools.ietf.org/html/rfc8297)
func (v *Vulcain) push(u *url.URL, req *http.Request, newHeaders http.Header, n *node, preloadHeader, fieldsHeader bool) bool {
func (v *Vulcain) push(u *url.URL, rw http.ResponseWriter, req *http.Request, newHeaders http.Header, n *node, preloadHeader, fieldsHeader bool) bool {
pusher := req.Context().Value(ctxKey{}).(*waitPusher)

url := u.String()
if pusher == nil || u.IsAbs() {
v.addPreloadHeader(newHeaders, url)

return false
}

Expand Down

0 comments on commit 6329c52

Please sign in to comment.