From ca4fae64d99a63291a91e59af5a1e8eef8c8e2d8 Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Mon, 5 Sep 2022 13:50:44 -0600 Subject: caddyhttp: Support `respond` with HTTP 103 Early Hints (#5006) * caddyhttp: Support sending HTTP 103 Early Hints This adds support for early hints in the static_response handler. * caddyhttp: Don't record 1xx responses --- modules/caddyhttp/matchers.go | 16 ++++++++++++ modules/caddyhttp/push/handler.go | 20 +++++++++++--- modules/caddyhttp/responsewriter.go | 52 ++++++++++++++++++++++--------------- modules/caddyhttp/staticresp.go | 15 +++++++++-- 4 files changed, 77 insertions(+), 26 deletions(-) diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index 5056c9a..f86ce0a 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -1121,6 +1121,22 @@ func (m MatchProtocol) Match(r *http.Request) bool { return r.TLS != nil case "http": return r.TLS == nil + case "http/1.0": + return r.ProtoMajor == 1 && r.ProtoMinor == 0 + case "http/1.0+": + return r.ProtoAtLeast(1, 0) + case "http/1.1": + return r.ProtoMajor == 1 && r.ProtoMinor == 1 + case "http/1.1+": + return r.ProtoAtLeast(1, 1) + case "http/2": + return r.ProtoMajor == 2 + case "http/2+": + return r.ProtoAtLeast(2, 0) + case "http/3": + return r.ProtoMajor == 3 + case "http/3+": + return r.ProtoAtLeast(3, 0) } return false } diff --git a/modules/caddyhttp/push/handler.go b/modules/caddyhttp/push/handler.go index 75442be..27652ef 100644 --- a/modules/caddyhttp/push/handler.go +++ b/modules/caddyhttp/push/handler.go @@ -29,10 +29,24 @@ func init() { caddy.RegisterModule(Handler{}) } -// Handler is a middleware for manipulating the request body. +// Handler is a middleware for HTTP/2 server push. Note that +// HTTP/2 server push has been deprecated by some clients and +// its use is discouraged unless you can accurately predict +// which resources actually need to be pushed to the client; +// it can be difficult to know what the client already has +// cached. Pushing unnecessary resources results in worse +// performance. Consider using HTTP 103 Early Hints instead. +// +// This handler supports pushing from Link headers; in other +// words, if the eventual response has Link headers, this +// handler will push the resources indicated by those headers, +// even without specifying any resources in its config. type Handler struct { - Resources []Resource `json:"resources,omitempty"` - Headers *HeaderConfig `json:"headers,omitempty"` + // The resources to push. + Resources []Resource `json:"resources,omitempty"` + + // Headers to modify for the push requests. + Headers *HeaderConfig `json:"headers,omitempty"` logger *zap.Logger } diff --git a/modules/caddyhttp/responsewriter.go b/modules/caddyhttp/responsewriter.go index 0ffb932..374bbfb 100644 --- a/modules/caddyhttp/responsewriter.go +++ b/modules/caddyhttp/responsewriter.go @@ -111,15 +111,15 @@ type responseRecorder struct { // // Proper usage of a recorder looks like this: // -// rec := caddyhttp.NewResponseRecorder(w, buf, shouldBuffer) -// err := next.ServeHTTP(rec, req) -// if err != nil { -// return err -// } -// if !rec.Buffered() { -// return nil -// } -// // process the buffered response here +// rec := caddyhttp.NewResponseRecorder(w, buf, shouldBuffer) +// err := next.ServeHTTP(rec, req) +// if err != nil { +// return err +// } +// if !rec.Buffered() { +// return nil +// } +// // process the buffered response here // // The header map is not buffered; i.e. the ResponseRecorder's Header() // method returns the same header map of the underlying ResponseWriter. @@ -129,7 +129,7 @@ type responseRecorder struct { // Once you are ready to write the response, there are two ways you can // do it. The easier way is to have the recorder do it: // -// rec.WriteResponse() +// rec.WriteResponse() // // This writes the recorded response headers as well as the buffered body. // Or, you may wish to do it yourself, especially if you manipulated the @@ -138,9 +138,12 @@ type responseRecorder struct { // recorder's body buffer, but you might have your own body to write // instead): // -// w.WriteHeader(rec.Status()) -// io.Copy(w, rec.Buffer()) +// w.WriteHeader(rec.Status()) +// io.Copy(w, rec.Buffer()) // +// As a special case, 1xx responses are not buffered nor recorded +// because they are not the final response; they are passed through +// directly to the underlying ResponseWriter. func NewResponseRecorder(w http.ResponseWriter, buf *bytes.Buffer, shouldBuffer ShouldBufferFunc) ResponseRecorder { return &responseRecorder{ ResponseWriterWrapper: &ResponseWriterWrapper{ResponseWriter: w}, @@ -149,22 +152,29 @@ func NewResponseRecorder(w http.ResponseWriter, buf *bytes.Buffer, shouldBuffer } } +// WriteHeader writes the headers with statusCode to the wrapped +// ResponseWriter unless the response is to be buffered instead. +// 1xx responses are never buffered. func (rr *responseRecorder) WriteHeader(statusCode int) { if rr.wroteHeader { return } - rr.statusCode = statusCode - rr.wroteHeader = true - // decide whether we should buffer the response - if rr.shouldBuffer == nil { - rr.stream = true - } else { - rr.stream = !rr.shouldBuffer(rr.statusCode, rr.ResponseWriterWrapper.Header()) + // 1xx responses aren't final; just informational + if statusCode < 100 || statusCode > 199 { + rr.statusCode = statusCode + rr.wroteHeader = true + + // decide whether we should buffer the response + if rr.shouldBuffer == nil { + rr.stream = true + } else { + rr.stream = !rr.shouldBuffer(rr.statusCode, rr.ResponseWriterWrapper.Header()) + } } - // if not buffered, immediately write header - if rr.stream { + // if informational or not buffered, immediately write header + if rr.stream || (100 <= statusCode && statusCode <= 199) { rr.ResponseWriterWrapper.WriteHeader(rr.statusCode) } } diff --git a/modules/caddyhttp/staticresp.go b/modules/caddyhttp/staticresp.go index f429692..ccc70e2 100644 --- a/modules/caddyhttp/staticresp.go +++ b/modules/caddyhttp/staticresp.go @@ -86,6 +86,12 @@ Response headers may be added using the --header flag for each header field. type StaticResponse struct { // The HTTP status code to respond with. Can be an integer or, // if needing to use a placeholder, a string. + // + // If the status code is 103 (Early Hints), the response headers + // will be written to the client immediately, the body will be + // ignored, and the next handler will be invoked. This behavior + // is EXPERIMENTAL while RFC 8297 is a draft, and may be changed + // or removed. StatusCode WeakString `json:"status_code,omitempty"` // Header fields to set on the response; overwrites any existing @@ -170,7 +176,7 @@ func (s *StaticResponse) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return nil } -func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error { +func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error { // close the connection immediately if s.Abort { panic(http.ErrAbortHandler) @@ -237,10 +243,15 @@ func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Hand w.WriteHeader(statusCode) // write response body - if body != "" { + if statusCode != http.StatusEarlyHints && body != "" { fmt.Fprint(w, body) } + // continue handling after Early Hints as they are not the final response + if statusCode == http.StatusEarlyHints { + return next.ServeHTTP(w, r) + } + return nil } -- cgit v1.2.3