Skip to content

Commit a05560d

Browse files
authored
implement early hints (#1996)
1 parent 48f3a2f commit a05560d

File tree

3 files changed

+112
-0
lines changed

3 files changed

+112
-0
lines changed

server.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,72 @@ type RequestCtx struct {
623623
hijackNoResponse bool
624624
}
625625

626+
// EarlyHints allows the server to hint to the browser what resources a page would need
627+
// so the browser can preload them while waiting for the server's full response. Only Link
628+
// headers already written to the response will be transmitted as Early Hints.
629+
//
630+
// This is a HTTP/2+ feature but all browsers will either understand it or safely ignore it.
631+
//
632+
// NOTE: Older HTTP/1.1 non-browser clients may face compatibility issues.
633+
//
634+
// See: https://developer.chrome.com/docs/web-platform/early-hints and
635+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Link#syntax
636+
//
637+
// Example:
638+
//
639+
// func(ctx *fasthttp.RequestCtx) {
640+
// ctx.Response.Header.Add("Link", "<https://fonts.google.com>; rel=preconnect")
641+
// ctx.EarlyHints()
642+
// time.Sleep(5*time.Second) // some time-consuming task
643+
// ctx.SetStatusCode(fasthttp.StatusOK)
644+
// ctx.SetBody([]byte("<html><head></head><body><h1>Hello from Fasthttp</h1></body></html>"))
645+
// }
646+
func (ctx *RequestCtx) EarlyHints() error {
647+
links := ctx.Response.Header.PeekAll(b2s(strLink))
648+
if len(links) > 0 {
649+
c := acquireWriter(ctx)
650+
defer releaseWriter(ctx.s, c)
651+
_, err := c.Write(strEarlyHints)
652+
if err != nil {
653+
return err
654+
}
655+
for _, l := range links {
656+
if len(l) == 0 {
657+
continue
658+
}
659+
_, err = c.Write(strLink)
660+
if err != nil {
661+
return err
662+
}
663+
_, err = c.Write(strColon)
664+
if err != nil {
665+
return err
666+
}
667+
_, err = c.Write(strSpace)
668+
if err != nil {
669+
return err
670+
}
671+
_, err = c.Write(l)
672+
if err != nil {
673+
return err
674+
}
675+
_, err = c.Write(strCRLF)
676+
if err != nil {
677+
return err
678+
}
679+
}
680+
_, err = c.Write(strCRLF)
681+
if err != nil {
682+
return err
683+
}
684+
err = c.Flush()
685+
if err != nil {
686+
return err
687+
}
688+
}
689+
return nil
690+
}
691+
626692
// HijackHandler must process the hijacked connection c.
627693
//
628694
// If KeepHijackedConns is disabled, which is by default,

server_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1919,6 +1919,49 @@ func TestServerExpect100Continue(t *testing.T) {
19191919
}
19201920
}
19211921

1922+
func TestServerExpect103EarlyHints(t *testing.T) {
1923+
t.Parallel()
1924+
1925+
s := &Server{
1926+
NoDefaultContentType: true,
1927+
NoDefaultDate: true,
1928+
NoDefaultServerHeader: true,
1929+
Handler: func(ctx *RequestCtx) {
1930+
ctx.Response.Header.Add("Link", "<https://cdn.com>; rel=preload; as=script")
1931+
ctx.EarlyHints() //nolint:errcheck
1932+
},
1933+
}
1934+
1935+
rw := &readWriter{}
1936+
rw.r.WriteString("GET /foo HTTP/1.1\r\nContent-Length: 5\r\nContent-Type: a/b\r\n\r\n12345")
1937+
1938+
if err := s.ServeConn(rw); err != nil {
1939+
t.Fatalf("Unexpected error from serveConn: %v", err)
1940+
}
1941+
1942+
scanner := bufio.NewScanner(&rw.w)
1943+
expected := []string{
1944+
"HTTP/1.1 103 Early Hints",
1945+
"Link: <https://cdn.com>; rel=preload; as=script",
1946+
"",
1947+
"HTTP/1.1 200 OK",
1948+
"Content-Length: 0",
1949+
"Link: <https://cdn.com>; rel=preload; as=script",
1950+
}
1951+
1952+
i := 0
1953+
for scanner.Scan() {
1954+
if i >= len(expected) {
1955+
break
1956+
}
1957+
line := scanner.Text()
1958+
if line != expected[i] {
1959+
t.Fatalf("unexpected data: %s. Expecting %s", line, expected[i])
1960+
}
1961+
i++
1962+
}
1963+
}
1964+
19221965
func TestServerContinueHandler(t *testing.T) {
19231966
t.Parallel()
19241967

strings.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ var (
2525
strColonSpace = []byte(": ")
2626
strCommaSpace = []byte(", ")
2727
strGMT = []byte("GMT")
28+
strSpace = []byte(" ")
2829

2930
strResponseContinue = []byte("HTTP/1.1 100 Continue\r\n\r\n")
31+
strEarlyHints = []byte("HTTP/1.1 103 Early Hints\r\n")
3032

3133
strExpect = []byte(HeaderExpect)
3234
strConnection = []byte(HeaderConnection)
@@ -86,6 +88,7 @@ var (
8688
strBoundary = []byte("boundary")
8789
strBytes = []byte("bytes")
8890
strBasicSpace = []byte("Basic ")
91+
strLink = []byte("Link")
8992

9093
strApplicationSlash = []byte("application/")
9194
strImageSVG = []byte("image/svg")

0 commit comments

Comments
 (0)