mirror of
https://github.com/caddyserver/caddy.git
synced 2025-02-02 14:17:01 +01:00
HTTP/2 push support (golang 1.8) (#1215)
* WIP * HTTP2/Push for golang 1.8 * Push plugin completed for review * Correct build tag * Move push plugin position * Add build tags to tests * Gofmt that code * Add header/method validations * Load push plugin * Fixes for wrapping writers * Push after delivering file * Fixes, review changes * Remove build tags, support new syntax * Fix spelling * gofmt -s -w . * Gogland time * Add interface guards * gofmt * After review fixes
This commit is contained in:
parent
579007822f
commit
cdf7cf5c3f
12 changed files with 882 additions and 12 deletions
|
@ -21,6 +21,7 @@ import (
|
||||||
_ "github.com/mholt/caddy/caddyhttp/mime"
|
_ "github.com/mholt/caddy/caddyhttp/mime"
|
||||||
_ "github.com/mholt/caddy/caddyhttp/pprof"
|
_ "github.com/mholt/caddy/caddyhttp/pprof"
|
||||||
_ "github.com/mholt/caddy/caddyhttp/proxy"
|
_ "github.com/mholt/caddy/caddyhttp/proxy"
|
||||||
|
_ "github.com/mholt/caddy/caddyhttp/push"
|
||||||
_ "github.com/mholt/caddy/caddyhttp/redirect"
|
_ "github.com/mholt/caddy/caddyhttp/redirect"
|
||||||
_ "github.com/mholt/caddy/caddyhttp/rewrite"
|
_ "github.com/mholt/caddy/caddyhttp/rewrite"
|
||||||
_ "github.com/mholt/caddy/caddyhttp/root"
|
_ "github.com/mholt/caddy/caddyhttp/root"
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
// ensure that the standard plugins are in fact plugged in
|
// ensure that the standard plugins are in fact plugged in
|
||||||
// and registered properly; this is a quick/naive way to do it.
|
// and registered properly; this is a quick/naive way to do it.
|
||||||
func TestStandardPlugins(t *testing.T) {
|
func TestStandardPlugins(t *testing.T) {
|
||||||
numStandardPlugins := 29 // importing caddyhttp plugs in this many plugins
|
numStandardPlugins := 30 // importing caddyhttp plugs in this many plugins
|
||||||
s := caddy.DescribePlugins()
|
s := caddy.DescribePlugins()
|
||||||
if got, want := strings.Count(s, "\n"), numStandardPlugins+5; got != want {
|
if got, want := strings.Count(s, "\n"), numStandardPlugins+5; got != want {
|
||||||
t.Errorf("Expected all standard plugins to be plugged in, got:\n%s", s)
|
t.Errorf("Expected all standard plugins to be plugged in, got:\n%s", s)
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"errors"
|
||||||
"github.com/mholt/caddy"
|
"github.com/mholt/caddy"
|
||||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||||
)
|
)
|
||||||
|
@ -161,3 +162,17 @@ func (w *gzipResponseWriter) CloseNotify() <-chan bool {
|
||||||
}
|
}
|
||||||
panic(httpserver.NonCloseNotifierError{Underlying: w.ResponseWriter})
|
panic(httpserver.NonCloseNotifierError{Underlying: w.ResponseWriter})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *gzipResponseWriter) Push(target string, opts *http.PushOptions) error {
|
||||||
|
if pusher, hasPusher := w.ResponseWriter.(http.Pusher); hasPusher {
|
||||||
|
return pusher.Push(target, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("push is unavailable (probably chained http.ResponseWriter does not implement http.Pusher)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards
|
||||||
|
var _ http.Pusher = (*gzipResponseWriter)(nil)
|
||||||
|
var _ http.Flusher = (*gzipResponseWriter)(nil)
|
||||||
|
var _ http.CloseNotifier = (*gzipResponseWriter)(nil)
|
||||||
|
var _ http.Hijacker = (*gzipResponseWriter)(nil)
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"errors"
|
||||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -23,7 +24,7 @@ type Headers struct {
|
||||||
// setting headers on the response according to the configured rules.
|
// setting headers on the response according to the configured rules.
|
||||||
func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
replacer := httpserver.NewReplacer(r, nil, "")
|
replacer := httpserver.NewReplacer(r, nil, "")
|
||||||
rww := &responseWriterWrapper{w: w}
|
rww := &responseWriterWrapper{ResponseWriter: w}
|
||||||
for _, rule := range h.Rules {
|
for _, rule := range h.Rules {
|
||||||
if httpserver.Path(r.URL.Path).Matches(rule.Path) {
|
if httpserver.Path(r.URL.Path).Matches(rule.Path) {
|
||||||
for name := range rule.Headers {
|
for name := range rule.Headers {
|
||||||
|
@ -62,20 +63,20 @@ type headerOperation func(http.Header)
|
||||||
// responseWriterWrapper wraps the real ResponseWriter.
|
// responseWriterWrapper wraps the real ResponseWriter.
|
||||||
// It defers header operations until writeHeader
|
// It defers header operations until writeHeader
|
||||||
type responseWriterWrapper struct {
|
type responseWriterWrapper struct {
|
||||||
w http.ResponseWriter
|
http.ResponseWriter
|
||||||
ops []headerOperation
|
ops []headerOperation
|
||||||
wroteHeader bool
|
wroteHeader bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rww *responseWriterWrapper) Header() http.Header {
|
func (rww *responseWriterWrapper) Header() http.Header {
|
||||||
return rww.w.Header()
|
return rww.ResponseWriter.Header()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rww *responseWriterWrapper) Write(d []byte) (int, error) {
|
func (rww *responseWriterWrapper) Write(d []byte) (int, error) {
|
||||||
if !rww.wroteHeader {
|
if !rww.wroteHeader {
|
||||||
rww.WriteHeader(http.StatusOK)
|
rww.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
return rww.w.Write(d)
|
return rww.ResponseWriter.Write(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rww *responseWriterWrapper) WriteHeader(status int) {
|
func (rww *responseWriterWrapper) WriteHeader(status int) {
|
||||||
|
@ -91,7 +92,7 @@ func (rww *responseWriterWrapper) WriteHeader(status int) {
|
||||||
op(h)
|
op(h)
|
||||||
}
|
}
|
||||||
|
|
||||||
rww.w.WriteHeader(status)
|
rww.ResponseWriter.WriteHeader(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
// delHeader deletes the existing header according to the key
|
// delHeader deletes the existing header according to the key
|
||||||
|
@ -109,19 +110,19 @@ func (rww *responseWriterWrapper) delHeader(key string) {
|
||||||
// Hijack implements http.Hijacker. It simply wraps the underlying
|
// Hijack implements http.Hijacker. It simply wraps the underlying
|
||||||
// ResponseWriter's Hijack method if there is one, or returns an error.
|
// ResponseWriter's Hijack method if there is one, or returns an error.
|
||||||
func (rww *responseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
func (rww *responseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
if hj, ok := rww.w.(http.Hijacker); ok {
|
if hj, ok := rww.ResponseWriter.(http.Hijacker); ok {
|
||||||
return hj.Hijack()
|
return hj.Hijack()
|
||||||
}
|
}
|
||||||
return nil, nil, httpserver.NonHijackerError{Underlying: rww.w}
|
return nil, nil, httpserver.NonHijackerError{Underlying: rww.ResponseWriter}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush implements http.Flusher. It simply wraps the underlying
|
// Flush implements http.Flusher. It simply wraps the underlying
|
||||||
// ResponseWriter's Flush method if there is one, or panics.
|
// ResponseWriter's Flush method if there is one, or panics.
|
||||||
func (rww *responseWriterWrapper) Flush() {
|
func (rww *responseWriterWrapper) Flush() {
|
||||||
if f, ok := rww.w.(http.Flusher); ok {
|
if f, ok := rww.ResponseWriter.(http.Flusher); ok {
|
||||||
f.Flush()
|
f.Flush()
|
||||||
} else {
|
} else {
|
||||||
panic(httpserver.NonFlusherError{Underlying: rww.w}) // should be recovered at the beginning of middleware stack
|
panic(httpserver.NonFlusherError{Underlying: rww.ResponseWriter}) // should be recovered at the beginning of middleware stack
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,8 +130,22 @@ func (rww *responseWriterWrapper) Flush() {
|
||||||
// It just inherits the underlying ResponseWriter's CloseNotify method.
|
// It just inherits the underlying ResponseWriter's CloseNotify method.
|
||||||
// It panics if the underlying ResponseWriter is not a CloseNotifier.
|
// It panics if the underlying ResponseWriter is not a CloseNotifier.
|
||||||
func (rww *responseWriterWrapper) CloseNotify() <-chan bool {
|
func (rww *responseWriterWrapper) CloseNotify() <-chan bool {
|
||||||
if cn, ok := rww.w.(http.CloseNotifier); ok {
|
if cn, ok := rww.ResponseWriter.(http.CloseNotifier); ok {
|
||||||
return cn.CloseNotify()
|
return cn.CloseNotify()
|
||||||
}
|
}
|
||||||
panic(httpserver.NonCloseNotifierError{Underlying: rww.w})
|
panic(httpserver.NonCloseNotifierError{Underlying: rww.ResponseWriter})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rww *responseWriterWrapper) Push(target string, opts *http.PushOptions) error {
|
||||||
|
if pusher, hasPusher := rww.ResponseWriter.(http.Pusher); hasPusher {
|
||||||
|
return pusher.Push(target, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("push is unavailable (probably chained http.ResponseWriter does not implement http.Pusher)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards
|
||||||
|
var _ http.Pusher = (*responseWriterWrapper)(nil)
|
||||||
|
var _ http.Flusher = (*responseWriterWrapper)(nil)
|
||||||
|
var _ http.CloseNotifier = (*responseWriterWrapper)(nil)
|
||||||
|
var _ http.Hijacker = (*responseWriterWrapper)(nil)
|
||||||
|
|
|
@ -459,6 +459,7 @@ var directives = []string{
|
||||||
"proxy",
|
"proxy",
|
||||||
"fastcgi",
|
"fastcgi",
|
||||||
"cgi", // github.com/jung-kurt/caddy-cgi
|
"cgi", // github.com/jung-kurt/caddy-cgi
|
||||||
|
"push",
|
||||||
"websocket",
|
"websocket",
|
||||||
"filemanager", // github.com/hacdias/caddy-filemanager
|
"filemanager", // github.com/hacdias/caddy-filemanager
|
||||||
"markdown",
|
"markdown",
|
||||||
|
|
|
@ -2,6 +2,7 @@ package httpserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
@ -95,3 +96,18 @@ func (r *ResponseRecorder) CloseNotify() <-chan bool {
|
||||||
}
|
}
|
||||||
panic(NonCloseNotifierError{Underlying: r.ResponseWriter})
|
panic(NonCloseNotifierError{Underlying: r.ResponseWriter})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Push resource to client
|
||||||
|
func (r *ResponseRecorder) Push(target string, opts *http.PushOptions) error {
|
||||||
|
if pusher, hasPusher := r.ResponseWriter.(http.Pusher); hasPusher {
|
||||||
|
return pusher.Push(target, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("push is unavailable (probably chained http.ResponseWriter does not implement http.Pusher)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards
|
||||||
|
var _ http.Pusher = (*ResponseRecorder)(nil)
|
||||||
|
var _ http.Flusher = (*ResponseRecorder)(nil)
|
||||||
|
var _ http.CloseNotifier = (*ResponseRecorder)(nil)
|
||||||
|
var _ http.Hijacker = (*ResponseRecorder)(nil)
|
||||||
|
|
|
@ -45,6 +45,7 @@ func NewServer(addr string, group []*SiteConfig) (*Server, error) {
|
||||||
sites: group,
|
sites: group,
|
||||||
connTimeout: GracefulTimeout,
|
connTimeout: GracefulTimeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Server.Handler = s // this is weird, but whatever
|
s.Server.Handler = s // this is weird, but whatever
|
||||||
|
|
||||||
// Disable HTTP/2 if desired
|
// Disable HTTP/2 if desired
|
||||||
|
|
70
caddyhttp/push/handler.go
Normal file
70
caddyhttp/push/handler.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package push
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
|
||||||
|
pusher, hasPusher := w.(http.Pusher)
|
||||||
|
|
||||||
|
// No Pusher, no cry
|
||||||
|
if !hasPusher {
|
||||||
|
return h.Next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is request for the pushed resource - it should not be recursive
|
||||||
|
if _, exists := r.Header[pushHeader]; exists {
|
||||||
|
return h.Next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve file first
|
||||||
|
code, err := h.Next.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
if flusher, ok := w.(http.Flusher); ok {
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
outer:
|
||||||
|
for _, rule := range h.Rules {
|
||||||
|
if httpserver.Path(r.URL.Path).Matches(rule.Path) {
|
||||||
|
for _, resource := range rule.Resources {
|
||||||
|
pushErr := pusher.Push(resource.Path, &http.PushOptions{
|
||||||
|
Method: resource.Method,
|
||||||
|
Header: resource.Header,
|
||||||
|
})
|
||||||
|
if pushErr != nil {
|
||||||
|
// If we cannot push (either not supported or concurrent streams are full - break)
|
||||||
|
break outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if links, exists := w.Header()["Link"]; exists {
|
||||||
|
h.pushLinks(pusher, links)
|
||||||
|
}
|
||||||
|
|
||||||
|
return code, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Middleware) pushLinks(pusher http.Pusher, links []string) {
|
||||||
|
outer:
|
||||||
|
for _, link := range links {
|
||||||
|
parts := strings.Split(link, ";")
|
||||||
|
|
||||||
|
if link == "" || strings.HasSuffix(link, "nopush") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
target := strings.TrimSuffix(strings.TrimPrefix(parts[0], "<"), ">")
|
||||||
|
|
||||||
|
err := pusher.Push(target, &http.PushOptions{Method: http.MethodGet})
|
||||||
|
if err != nil {
|
||||||
|
break outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
282
caddyhttp/push/handler_test.go
Normal file
282
caddyhttp/push/handler_test.go
Normal file
|
@ -0,0 +1,282 @@
|
||||||
|
package push
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockedPusher struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
pushed map[string]*http.PushOptions
|
||||||
|
returnedError error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *MockedPusher) Push(target string, options *http.PushOptions) error {
|
||||||
|
if w.pushed == nil {
|
||||||
|
w.pushed = make(map[string]*http.PushOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.pushed[target] = options
|
||||||
|
return w.returnedError
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddlewareWillPushResources(t *testing.T) {
|
||||||
|
|
||||||
|
// given
|
||||||
|
request, err := http.NewRequest(http.MethodGet, "/index.html", nil)
|
||||||
|
writer := httptest.NewRecorder()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not create HTTP request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware := Middleware{
|
||||||
|
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}),
|
||||||
|
Rules: []Rule{
|
||||||
|
{Path: "/index.html", Resources: []Resource{
|
||||||
|
{Path: "/index.css", Method: http.MethodHead, Header: http.Header{"Test": []string{"Value"}}},
|
||||||
|
{Path: "/index2.css", Method: http.MethodGet},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pushingWriter := &MockedPusher{ResponseWriter: writer}
|
||||||
|
|
||||||
|
// when
|
||||||
|
middleware.ServeHTTP(pushingWriter, request)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expectedPushedResources := map[string]*http.PushOptions{
|
||||||
|
"/index.css": {
|
||||||
|
Method: http.MethodHead,
|
||||||
|
Header: http.Header{"Test": []string{"Value"}},
|
||||||
|
},
|
||||||
|
|
||||||
|
"/index2.css": {
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Header: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
comparePushedResources(t, expectedPushedResources, pushingWriter.pushed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddlewareShouldntDoRecursivePush(t *testing.T) {
|
||||||
|
|
||||||
|
// given
|
||||||
|
request, err := http.NewRequest(http.MethodGet, "/index.css", nil)
|
||||||
|
request.Header.Add(pushHeader, "")
|
||||||
|
|
||||||
|
writer := httptest.NewRecorder()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not create HTTP request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware := Middleware{
|
||||||
|
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}),
|
||||||
|
Rules: []Rule{
|
||||||
|
{Path: "/", Resources: []Resource{
|
||||||
|
{Path: "/index.css", Method: http.MethodHead, Header: http.Header{"Test": []string{"Value"}}},
|
||||||
|
{Path: "/index2.css", Method: http.MethodGet},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pushingWriter := &MockedPusher{ResponseWriter: writer}
|
||||||
|
|
||||||
|
// when
|
||||||
|
middleware.ServeHTTP(pushingWriter, request)
|
||||||
|
|
||||||
|
// then
|
||||||
|
if len(pushingWriter.pushed) > 0 {
|
||||||
|
t.Errorf("Expected 0 pushed resources, actual %d", len(pushingWriter.pushed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddlewareShouldStopPushingOnError(t *testing.T) {
|
||||||
|
|
||||||
|
// given
|
||||||
|
request, err := http.NewRequest(http.MethodGet, "/index.html", nil)
|
||||||
|
writer := httptest.NewRecorder()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not create HTTP request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware := Middleware{
|
||||||
|
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}),
|
||||||
|
Rules: []Rule{
|
||||||
|
{Path: "/index.html", Resources: []Resource{
|
||||||
|
{Path: "/only.css", Method: http.MethodHead, Header: http.Header{"Test": []string{"Value"}}},
|
||||||
|
{Path: "/index2.css", Method: http.MethodGet},
|
||||||
|
{Path: "/index3.css", Method: http.MethodGet},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pushingWriter := &MockedPusher{ResponseWriter: writer, returnedError: errors.New("Cannot push right now")}
|
||||||
|
|
||||||
|
// when
|
||||||
|
middleware.ServeHTTP(pushingWriter, request)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expectedPushedResources := map[string]*http.PushOptions{
|
||||||
|
"/only.css": {
|
||||||
|
Method: http.MethodHead,
|
||||||
|
Header: http.Header{"Test": []string{"Value"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
comparePushedResources(t, expectedPushedResources, pushingWriter.pushed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddlewareWillNotPushResources(t *testing.T) {
|
||||||
|
// given
|
||||||
|
request, err := http.NewRequest(http.MethodGet, "/index.html", nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not create HTTP request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware := Middleware{
|
||||||
|
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}),
|
||||||
|
Rules: []Rule{
|
||||||
|
{Path: "/index.html", Resources: []Resource{
|
||||||
|
{Path: "/index.css", Method: http.MethodHead, Header: http.Header{"Test": []string{"Value"}}},
|
||||||
|
{Path: "/index2.css", Method: http.MethodGet},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
writer := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// when
|
||||||
|
_, err2 := middleware.ServeHTTP(writer, request)
|
||||||
|
|
||||||
|
// then
|
||||||
|
if err2 != nil {
|
||||||
|
t.Errorf("Should not return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddlewareShouldInterceptLinkHeader(t *testing.T) {
|
||||||
|
// given
|
||||||
|
request, err := http.NewRequest(http.MethodGet, "/index.html", nil)
|
||||||
|
writer := httptest.NewRecorder()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not create HTTP request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware := Middleware{
|
||||||
|
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
w.Header().Add("Link", "</index.css>; rel=preload; as=stylesheet;")
|
||||||
|
w.Header().Add("Link", "</index2.css>; rel=preload; as=stylesheet;")
|
||||||
|
w.Header().Add("Link", "")
|
||||||
|
w.Header().Add("Link", "</index3.css>")
|
||||||
|
w.Header().Add("Link", "</index4.css>; rel=preload; nopush")
|
||||||
|
return 0, nil
|
||||||
|
}),
|
||||||
|
Rules: []Rule{},
|
||||||
|
}
|
||||||
|
|
||||||
|
pushingWriter := &MockedPusher{ResponseWriter: writer}
|
||||||
|
|
||||||
|
// when
|
||||||
|
_, err2 := middleware.ServeHTTP(pushingWriter, request)
|
||||||
|
|
||||||
|
// then
|
||||||
|
if err2 != nil {
|
||||||
|
t.Errorf("Should not return error")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedPushedResources := map[string]*http.PushOptions{
|
||||||
|
"/index.css": {
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Header: nil,
|
||||||
|
},
|
||||||
|
"/index2.css": {
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Header: nil,
|
||||||
|
},
|
||||||
|
"/index3.css": {
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Header: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
comparePushedResources(t, expectedPushedResources, pushingWriter.pushed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddlewareShouldInterceptLinkHeaderPusherError(t *testing.T) {
|
||||||
|
// given
|
||||||
|
request, err := http.NewRequest(http.MethodGet, "/index.html", nil)
|
||||||
|
writer := httptest.NewRecorder()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not create HTTP request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware := Middleware{
|
||||||
|
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
w.Header().Add("Link", "</index.css>; rel=preload; as=stylesheet;")
|
||||||
|
w.Header().Add("Link", "</index2.css>; rel=preload; as=stylesheet;")
|
||||||
|
return 0, nil
|
||||||
|
}),
|
||||||
|
Rules: []Rule{},
|
||||||
|
}
|
||||||
|
|
||||||
|
pushingWriter := &MockedPusher{ResponseWriter: writer, returnedError: errors.New("Cannot push right now")}
|
||||||
|
|
||||||
|
// when
|
||||||
|
_, err2 := middleware.ServeHTTP(pushingWriter, request)
|
||||||
|
|
||||||
|
// then
|
||||||
|
if err2 != nil {
|
||||||
|
t.Errorf("Should not return error")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedPushedResources := map[string]*http.PushOptions{
|
||||||
|
"/index.css": {
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Header: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
comparePushedResources(t, expectedPushedResources, pushingWriter.pushed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func comparePushedResources(t *testing.T, expected, actual map[string]*http.PushOptions) {
|
||||||
|
if len(expected) != len(actual) {
|
||||||
|
t.Errorf("Expected %d pushed resources, actual: %d", len(expected), len(actual))
|
||||||
|
}
|
||||||
|
|
||||||
|
for target, expectedTarget := range expected {
|
||||||
|
if actualTarget, exists := actual[target]; exists {
|
||||||
|
|
||||||
|
if expectedTarget.Method != actualTarget.Method {
|
||||||
|
t.Errorf("Expected %s resource method to be %s, actual: %s", target, expectedTarget.Method, actualTarget.Method)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(expectedTarget.Header, actualTarget.Header) {
|
||||||
|
t.Errorf("Expected %s resource push headers to be %v, actual: %v", target, expectedTarget.Header, actualTarget.Header)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Errorf("Expected %s to be pushed", target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
caddyhttp/push/push.go
Normal file
30
caddyhttp/push/push.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package push
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Rule describes conditions on which resources will be pushed
|
||||||
|
Rule struct {
|
||||||
|
Path string
|
||||||
|
Resources []Resource
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resource describes resource to be pushed
|
||||||
|
Resource struct {
|
||||||
|
Path string
|
||||||
|
Method string
|
||||||
|
Header http.Header
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware supports pushing resources to clients
|
||||||
|
Middleware struct {
|
||||||
|
Next httpserver.Handler
|
||||||
|
Rules []Rule
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleOp func([]Resource)
|
||||||
|
)
|
172
caddyhttp/push/setup.go
Normal file
172
caddyhttp/push/setup.go
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
package push
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mholt/caddy"
|
||||||
|
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
caddy.RegisterPlugin("push", caddy.Plugin{
|
||||||
|
ServerType: "http",
|
||||||
|
Action: setup,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var errInvalidHeader = errors.New("header directive requires [name] [value]")
|
||||||
|
|
||||||
|
var errHeaderStartsWithColon = errors.New("header cannot start with colon")
|
||||||
|
var errMethodNotSupported = errors.New("push supports only GET and HEAD methods")
|
||||||
|
|
||||||
|
const pushHeader = "X-Push"
|
||||||
|
|
||||||
|
var emptyRules = []Rule{}
|
||||||
|
|
||||||
|
// setup configures a new Push middleware
|
||||||
|
func setup(c *caddy.Controller) error {
|
||||||
|
rules, err := parsePushRules(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||||
|
return Middleware{Next: next, Rules: rules}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePushRules(c *caddy.Controller) ([]Rule, error) {
|
||||||
|
var rules = make(map[string]*Rule)
|
||||||
|
|
||||||
|
for c.NextLine() {
|
||||||
|
if !c.NextArg() {
|
||||||
|
return emptyRules, c.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
path := c.Val()
|
||||||
|
args := c.RemainingArgs()
|
||||||
|
|
||||||
|
var rule *Rule
|
||||||
|
var resources []Resource
|
||||||
|
var ops []ruleOp
|
||||||
|
|
||||||
|
if existingRule, ok := rules[path]; ok {
|
||||||
|
rule = existingRule
|
||||||
|
} else {
|
||||||
|
rule = new(Rule)
|
||||||
|
rule.Path = path
|
||||||
|
rules[rule.Path] = rule
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(args); i++ {
|
||||||
|
resources = append(resources, Resource{
|
||||||
|
Path: args[i],
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Header: http.Header{pushHeader: []string{}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for c.NextBlock() {
|
||||||
|
val := c.Val()
|
||||||
|
|
||||||
|
switch val {
|
||||||
|
case "method":
|
||||||
|
if !c.NextArg() {
|
||||||
|
return emptyRules, c.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
method := c.Val()
|
||||||
|
|
||||||
|
if err := validateMethod(method); err != nil {
|
||||||
|
return emptyRules, errMethodNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
ops = append(ops, setMethodOp(method))
|
||||||
|
|
||||||
|
case "header":
|
||||||
|
args := c.RemainingArgs()
|
||||||
|
|
||||||
|
if len(args) != 2 {
|
||||||
|
return emptyRules, errInvalidHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateHeader(args[0]); err != nil {
|
||||||
|
return emptyRules, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ops = append(ops, setHeaderOp(args[0], args[1]))
|
||||||
|
|
||||||
|
default:
|
||||||
|
resources = append(resources, Resource{
|
||||||
|
Path: val,
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Header: http.Header{pushHeader: []string{}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, op := range ops {
|
||||||
|
op(resources)
|
||||||
|
}
|
||||||
|
|
||||||
|
rule.Resources = append(rule.Resources, resources...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var returnRules []Rule
|
||||||
|
|
||||||
|
for path, rule := range rules {
|
||||||
|
if len(rule.Resources) == 0 {
|
||||||
|
return emptyRules, c.Errf("Rule %s has empty push resources list", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
returnRules = append(returnRules, *rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnRules, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setHeaderOp(key, value string) func(resources []Resource) {
|
||||||
|
return func(resources []Resource) {
|
||||||
|
for index := range resources {
|
||||||
|
resources[index].Header.Set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setMethodOp(method string) func(resources []Resource) {
|
||||||
|
|
||||||
|
return func(resources []Resource) {
|
||||||
|
for index := range resources {
|
||||||
|
resources[index].Method = method
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateHeader(header string) error {
|
||||||
|
if strings.HasPrefix(header, ":") {
|
||||||
|
return errHeaderStartsWithColon
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(header) {
|
||||||
|
case "content-length", "content-encoding", "trailer", "te", "expect", "host":
|
||||||
|
return fmt.Errorf("push headers cannot include %s", header)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// rules based on https://go-review.googlesource.com/#/c/29439/4/http2/go18.go#94
|
||||||
|
func validateMethod(method string) error {
|
||||||
|
if method != http.MethodGet && method != http.MethodHead {
|
||||||
|
return errMethodNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
267
caddyhttp/push/setup_test.go
Normal file
267
caddyhttp/push/setup_test.go
Normal file
|
@ -0,0 +1,267 @@
|
||||||
|
package push
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mholt/caddy"
|
||||||
|
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPushAvailable(t *testing.T) {
|
||||||
|
err := setup(caddy.NewTestController("http", "push /index.html /available.css"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error %s occurred, expected none", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
shouldErr bool
|
||||||
|
expected []Rule
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"ParseInvalidEmptyConfig", `push`, true, []Rule{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ParseInvalidConfig", `push /index.html`, true, []Rule{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ParseInvalidConfigBlock", `push /index.html /index.css {
|
||||||
|
method
|
||||||
|
}`, true, []Rule{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ParseInvalidHeaderFormat", `push /index.html /index.css {
|
||||||
|
header :invalid value
|
||||||
|
}`, true, []Rule{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ParseForbiddenHeader", `push /index.html /index.css {
|
||||||
|
header Content-Length 1000
|
||||||
|
}`, true, []Rule{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ParseInvalidMethod", `push /index.html /index.css {
|
||||||
|
method POST
|
||||||
|
}`, true, []Rule{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ParseInvalidHeaderBlock", `push /index.html /index.css {
|
||||||
|
header
|
||||||
|
}`, true, []Rule{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ParseInvalidHeaderBlock2", `push /index.html /index.css {
|
||||||
|
header name
|
||||||
|
}`, true, []Rule{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ParseProperConfig", `push /index.html /style.css /style2.css`, false, []Rule{
|
||||||
|
{
|
||||||
|
Path: "/index.html",
|
||||||
|
Resources: []Resource{
|
||||||
|
{
|
||||||
|
Path: "/style.css",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Header: http.Header{pushHeader: []string{}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/style2.css",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Header: http.Header{pushHeader: []string{}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ParseSimpleInlinePush", `push /index.html {
|
||||||
|
/style.css
|
||||||
|
/style2.css
|
||||||
|
}`, false, []Rule{
|
||||||
|
{
|
||||||
|
Path: "/index.html",
|
||||||
|
Resources: []Resource{
|
||||||
|
{
|
||||||
|
Path: "/style.css",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Header: http.Header{pushHeader: []string{}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/style2.css",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Header: http.Header{pushHeader: []string{}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ParseSimpleInlinePushWithOps", `push /index.html {
|
||||||
|
/style.css
|
||||||
|
/style2.css
|
||||||
|
header Test Value
|
||||||
|
}`, false, []Rule{
|
||||||
|
{
|
||||||
|
Path: "/index.html",
|
||||||
|
Resources: []Resource{
|
||||||
|
{
|
||||||
|
Path: "/style.css",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Header: http.Header{pushHeader: []string{}, "Test": []string{"Value"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/style2.css",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Header: http.Header{pushHeader: []string{}, "Test": []string{"Value"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ParseProperConfigWithBlock", `push /index.html /style.css /style2.css {
|
||||||
|
method HEAD
|
||||||
|
header Own-Header Value
|
||||||
|
header Own-Header2 Value2
|
||||||
|
}`, false, []Rule{
|
||||||
|
{
|
||||||
|
Path: "/index.html",
|
||||||
|
Resources: []Resource{
|
||||||
|
{
|
||||||
|
Path: "/style.css",
|
||||||
|
Method: http.MethodHead,
|
||||||
|
Header: http.Header{
|
||||||
|
"Own-Header": []string{"Value"},
|
||||||
|
"Own-Header2": []string{"Value2"},
|
||||||
|
"X-Push": []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/style2.css",
|
||||||
|
Method: http.MethodHead,
|
||||||
|
Header: http.Header{
|
||||||
|
"Own-Header": []string{"Value"},
|
||||||
|
"Own-Header2": []string{"Value2"},
|
||||||
|
"X-Push": []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ParseMergesRules", `push /index.html /index.css {
|
||||||
|
header name value
|
||||||
|
}
|
||||||
|
|
||||||
|
push /index.html /index2.css {
|
||||||
|
header name2 value2
|
||||||
|
method HEAD
|
||||||
|
}
|
||||||
|
`, false, []Rule{
|
||||||
|
{
|
||||||
|
Path: "/index.html",
|
||||||
|
Resources: []Resource{
|
||||||
|
{
|
||||||
|
Path: "/index.css",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Header: http.Header{
|
||||||
|
"Name": []string{"value"},
|
||||||
|
"X-Push": []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/index2.css",
|
||||||
|
Method: http.MethodHead,
|
||||||
|
Header: http.Header{
|
||||||
|
"Name2": []string{"value2"},
|
||||||
|
"X-Push": []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t2 *testing.T) {
|
||||||
|
actual, err := parsePushRules(caddy.NewTestController("http", test.input))
|
||||||
|
|
||||||
|
if err == nil && test.shouldErr {
|
||||||
|
t2.Errorf("Test %s didn't error, but it should have", test.name)
|
||||||
|
} else if err != nil && !test.shouldErr {
|
||||||
|
t2.Errorf("Test %s errored, but it shouldn't have; got '%v'", test.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(actual) != len(test.expected) {
|
||||||
|
t2.Fatalf("Test %s expected %d rules, but got %d",
|
||||||
|
test.name, len(test.expected), len(actual))
|
||||||
|
}
|
||||||
|
|
||||||
|
for j, expectedRule := range test.expected {
|
||||||
|
actualRule := actual[j]
|
||||||
|
|
||||||
|
if actualRule.Path != expectedRule.Path {
|
||||||
|
t.Errorf("Test %s, rule %d: Expected path %s, but got %s",
|
||||||
|
test.name, j, expectedRule.Path, actualRule.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(actualRule.Resources, expectedRule.Resources) {
|
||||||
|
t.Errorf("Test %s, rule %d: Expected resources %v, but got %v",
|
||||||
|
test.name, j, expectedRule.Resources, actualRule.Resources)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetupInstalledMiddleware(t *testing.T) {
|
||||||
|
|
||||||
|
// given
|
||||||
|
c := caddy.NewTestController("http", `push /index.html /test.js`)
|
||||||
|
|
||||||
|
// when
|
||||||
|
err := setup(c)
|
||||||
|
|
||||||
|
// then
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected no errors, but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
middlewares := httpserver.GetConfig(c).Middleware()
|
||||||
|
|
||||||
|
if len(middlewares) != 1 {
|
||||||
|
t.Fatalf("Expected 1 middleware, had %d instead", len(middlewares))
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := middlewares[0](httpserver.EmptyNext)
|
||||||
|
pushHandler, ok := handler.(Middleware)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected handler to be type Middleware, got: %#v", handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !httpserver.SameNext(pushHandler.Next, httpserver.EmptyNext) {
|
||||||
|
t.Error("'Next' field of handler Middleware was not set properly")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetupWithError(t *testing.T) {
|
||||||
|
// given
|
||||||
|
c := caddy.NewTestController("http", `push /index.html`)
|
||||||
|
|
||||||
|
// when
|
||||||
|
err := setup(c)
|
||||||
|
|
||||||
|
// then
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error but none occurred")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue