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
This commit is contained in:
Matt Holt 2022-09-05 13:50:44 -06:00 committed by GitHub
parent ad69503aef
commit ca4fae64d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 77 additions and 26 deletions

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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
}