Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions go.work.sum

Large diffs are not rendered by default.

60 changes: 53 additions & 7 deletions handler/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package handler
import (
"fmt"
"net/http"
"slices"
"strings"

httpV1proto "github.com/roadrunner-server/api-go/v6/http/v1"
Expand Down Expand Up @@ -48,6 +49,13 @@ func (h *Handler) handlePROTOresponse(pld *payload.Payload, w http.ResponseWrite
return err
}

// The provided code must be a valid HTTP 1xx-5xx status code.
if rsp.Status < 100 || rsp.Status >= 600 {
http.Error(w, fmt.Sprintf("unknown status code from worker: %d", rsp.Status), http.StatusInternalServerError)
return errors.Errorf("unknown status code from worker: %d", rsp.Status)
}
status := int(rsp.Status)

// handle push headers
if rsp.GetHeaders() != nil && rsp.GetHeaders()[HTTP2Push] != nil {
push := rsp.GetHeaders()[HTTP2Push].GetValue()
Expand All @@ -66,20 +74,26 @@ func (h *Handler) handlePROTOresponse(pld *payload.Payload, w http.ResponseWrite
handleProtoTrailers(rsp.GetHeaders())
}

switch {
case status == http.StatusSwitchingProtocols:
h.log.Error("101 Switching Protocols is not supported, the frame was ignored")
return nil
case informational(status):
if len(pld.Body) != 0 {
h.log.Warn("informational response body was dropped", "status", status)
}
writeInformational(status, rsp.GetHeaders(), w)
return nil
}

// write all headers from the response to the writer
for k, hv := range rsp.GetHeaders() {
for _, v := range hv.GetValue() {
w.Header().Add(k, string(v))
}
}

// The provided code must be a valid HTTP 1xx-5xx status code.
if rsp.Status < 100 || rsp.Status >= 600 {
http.Error(w, fmt.Sprintf("unknown status code from worker: %d", rsp.Status), http.StatusInternalServerError)
return errors.Errorf("unknown status code from worker: %d", rsp.Status)
}

w.WriteHeader(int(rsp.Status))
w.WriteHeader(status)
}

// do not write body if it is empty
Expand All @@ -99,6 +113,38 @@ func (h *Handler) handlePROTOresponse(pld *payload.Payload, w http.ResponseWrite
return nil
}

func informational(status int) bool {
return status >= 100 && status < 200 && status != http.StatusSwitchingProtocols
}

// writeInformational sends a 1xx response
func writeInformational(status int, headers map[string]*httpV1proto.HeaderValue, w http.ResponseWriter) {
hdr := w.Header()
saved := make(map[string][]string, len(headers))

for k := range headers {
ck := http.CanonicalHeaderKey(k)
if _, ok := saved[ck]; !ok {
saved[ck] = slices.Clone(hdr[ck])
}
}

for k, hv := range headers {
for _, v := range hv.GetValue() {
hdr.Add(k, string(v))
}
}

w.WriteHeader(status)
for ck, prev := range saved {
if prev == nil {
hdr.Del(ck)
continue
}
hdr[ck] = prev
}
}

func handleProtoTrailers(h map[string]*httpV1proto.HeaderValue) {
for _, tr := range h[Trailer].GetValue() {
for n := range strings.SplitSeq(string(tr), ",") {
Expand Down
159 changes: 159 additions & 0 deletions handler/response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,165 @@ func TestHandleProtoTrailers_RenamesAnnouncedHeaders(t *testing.T) {
}
}

type hintRecorder struct {
hdr http.Header
hints []hintFrame
code int
body []byte
wrote bool
}

type hintFrame struct {
code int
header http.Header
}

func (h *hintRecorder) Header() http.Header {
if h.hdr == nil {
h.hdr = http.Header{}
}
return h.hdr
}

func (h *hintRecorder) WriteHeader(code int) {
if h.wrote {
return
}
if code >= 100 && code < 200 && code != http.StatusSwitchingProtocols {
h.hints = append(h.hints, hintFrame{code, h.Header().Clone()})
return
}
h.wrote = true
h.code = code
}

func (h *hintRecorder) Write(b []byte) (int, error) {
if !h.wrote {
h.WriteHeader(http.StatusOK)
}
h.body = append(h.body, b...)
return len(b), nil
}

func TestWrite_EarlyHints_ScopedToInformationalResponse(t *testing.T) {
h := newTestHandler(t, defaultCfg(), nil)
rr := &hintRecorder{}

hint := &payload.Payload{
Codec: frame.CodecProto,
Context: marshalRsp(t, http.StatusEarlyHints, map[string]*httpV1proto.HeaderValue{
"Link": headerValue("</a.css>; rel=preload"),
}),
}
if err := h.Write(hint, rr); err != nil {
t.Fatal(err)
}

if len(rr.hints) != 1 || rr.hints[0].code != http.StatusEarlyHints {
t.Fatalf("hints = %+v, want a single 103", rr.hints)
}
if got := rr.hints[0].header.Get("Link"); got != "</a.css>; rel=preload" {
t.Errorf("hint Link = %q, want the preload link", got)
}
if got := rr.Header().Get("Link"); got != "" {
t.Errorf("Link = %q, want it removed after the informational response", got)
}
if rr.wrote {
t.Error("the informational frame must not close the response")
}

final := &payload.Payload{
Codec: frame.CodecProto,
Context: marshalRsp(t, http.StatusNotFound, map[string]*httpV1proto.HeaderValue{
"X-Marker": headerValue("probe"),
}),
Body: []byte("body"),
}
if err := h.Write(final, rr); err != nil {
t.Fatal(err)
}

if rr.code != http.StatusNotFound {
t.Errorf("status = %d, want %d", rr.code, http.StatusNotFound)
}
if got := rr.Header().Get("X-Marker"); got != "probe" {
t.Errorf("X-Marker = %q, want %q", got, "probe")
}
if got := rr.Header().Get("Link"); got != "" {
t.Errorf("Link = %q, want it absent from the final response", got)
}
if string(rr.body) != "body" {
t.Errorf("body = %q, want %q", rr.body, "body")
}
}

func TestWrite_EarlyHints_PreexistingHeaderRestored(t *testing.T) {
h := newTestHandler(t, defaultCfg(), nil)
rr := &hintRecorder{}
rr.Header().Set("Link", "</mw.css>; rel=preload")

hint := &payload.Payload{
Codec: frame.CodecProto,
Context: marshalRsp(t, http.StatusEarlyHints, map[string]*httpV1proto.HeaderValue{
"Link": headerValue("</worker.css>; rel=preload"),
}),
}
if err := h.Write(hint, rr); err != nil {
t.Fatal(err)
}

want := []string{"</mw.css>; rel=preload", "</worker.css>; rel=preload"}
if got := rr.hints[0].header.Values("Link"); len(got) != 2 || got[0] != want[0] || got[1] != want[1] {
t.Errorf("hint Link = %v, want %v", got, want)
}
if got := rr.Header().Values("Link"); len(got) != 1 || got[0] != want[0] {
t.Errorf("Link = %v, want only the pre-existing %q", got, want[0])
}
}

func TestWrite_EarlyHints_BodyDropped(t *testing.T) {
h := newTestHandler(t, defaultCfg(), nil)
rr := &hintRecorder{}

hint := &payload.Payload{
Codec: frame.CodecProto,
Context: marshalRsp(t, http.StatusEarlyHints, nil),
Body: []byte("bogus"),
}
if err := h.Write(hint, rr); err != nil {
t.Fatal(err)
}

if len(rr.body) != 0 {
t.Errorf("body = %q, want empty", rr.body)
}
if rr.wrote {
t.Error("the informational frame must not close the response")
}
}

func TestWrite_SwitchingProtocols_Dropped(t *testing.T) {
h := newTestHandler(t, defaultCfg(), nil)
rr := &hintRecorder{}

pld := &payload.Payload{
Codec: frame.CodecProto,
Context: marshalRsp(t, http.StatusSwitchingProtocols, map[string]*httpV1proto.HeaderValue{
"Upgrade": headerValue("websocket"),
}),
}
if err := h.Write(pld, rr); err != nil {
t.Fatal(err)
}

if rr.wrote || len(rr.hints) != 0 {
t.Errorf("recorder = %+v, want no writes for a 101 frame", rr)
}
if got := rr.Header().Get("Upgrade"); got != "" {
t.Errorf("Upgrade = %q, want no headers from a dropped frame", got)
}
}

func TestWrite_Trailers_RenamedOnTheWire(t *testing.T) {
h := newTestHandler(t, defaultCfg(), nil)

Expand Down
10 changes: 5 additions & 5 deletions middleware/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ type wrapper struct {
read int
write int

// TwoXXSent is true if the response headers with >= 2xx code were sent
// 1xx header might be sent unlimited number of times
// wc is true once the final response header went out: a status >= 200, or
// a 101, which net/http treats as final. Informational 1xx headers may be
// written any number of times before that.
wc bool

w http.ResponseWriter
Expand All @@ -41,13 +42,12 @@ func (w *wrapper) Read(b []byte) (int, error) {
}

func (w *wrapper) WriteHeader(code int) {
w.code = code
if w.wc {
return
}

// do not allow sending 200 twice
if code >= 100 && code < 200 {
if code >= 200 || code == http.StatusSwitchingProtocols {
w.code = code
w.wc = true
}

Expand Down
Loading
Loading