gin icon indicating copy to clipboard operation
gin copied to clipboard

103 early hints

Open thinkerou opened this issue 1 year ago • 1 comments

related https://github.com/golang/go/issues/51914

thinkerou avatar Aug 19 '22 01:08 thinkerou

I've got curious and checked how Go 1.19 now allows HTTP 104 Early Hint.

Research: How to HTTP 103 in Go

In this article I found a snippet:

func main() {
    helloHandler := func(w http.ResponseWriter, req *http.Request) {
        w.Header().Add("Link", "</style.css>; rel=preload; as=style")
        w.Header().Add("Link", "</script.js>; rel=preload; as=script")

        w.WriteHeader(103)

        // do your heavy tasks such as DB or remote APIs calls here

        w.WriteHeader(200)

        io.WriteString(w, "<!doctype html>\n[... snip snip ...]")
    }

    http.HandleFunc("/hello", helloHandler)
    log.Fatal(http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil))
}

This boils down to three steps:

  1. Attach the Link headers to the response.
  2. Write the status line HTTP 103 Early Hint
  3. Enforce the writing the early hint with w.WriteHeaderNow()

Reproduction with Gin

I then was able to reproduce this with Gin:

func main() {
	r := gin.Default()

	r.Static("/assets", "./assets")

	r.GET("/testing", func(c *gin.Context) {
		c.Writer.Header().Add("Link", "</assets/file1.js>; rel=preload; as=script")
		c.Writer.Header().Add("Link", "<https://gphrase.de/r/>; rel=preconnect")
		c.Writer.WriteHeader(http.StatusEarlyHints)
		c.Writer.WriteHeaderNow()

                 // server thinking time
		time.Sleep(5 * time.Second)

		c.String(200, "success")
	})

	err := r.RunTLS(":443", "cert.pem", "key.pem")
	log.Err(err).Msg("server ended")
}
$ curl -v https://localhost/testing

# ... snip snip ...

> 
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 103 
< link: </assets/file1.js>; rel=preload; as=script
< link: <https://gphrase.de/r/>; rel=preconnect
< HTTP/2 200 
< content-type: text/plain; charset=utf-8
< link: </assets/file1.js>; rel=preload; as=script
< link: <https://gphrase.de/r/>; rel=preconnect
< content-length: 7
< date: Sat, 08 Oct 2022 17:32:16 GMT
< 
* Connection #0 to host localhost left intact
success* Closing connection 0

Proposal

We could encapsule the above attaching of Link headers, the writing of HTTP status 103, and forcing the write to the client.

func (c *Context) EarlyHint(links ...string) {
	if len(links) == 0 {
		return
	}

	for _, l := range links {
		c.Writer.Header().Add("Link", l)
	}

	c.Writer.WriteHeader(http.StatusEarlyHints)
	c.Writer.WriteHeaderNow()
}

With that the above example would become:

func main() {
	r := gin.Default()

	r.Static("/assets", "./assets")

	r.GET("/testing", func(c *gin.Context) {
		c.EarlyHint("</assets/file1.js>; rel=preload; as=script", "<https://someserver.com/r/>; rel=preconnect")

                 // server thinking time
		time.Sleep(5 * time.Second)

		c.String(200, "success")
	})

	err := r.RunTLS(":443", "cert.pem", "key.pem")
	log.Err(err).Msg("server ended")
}

pscheid92 avatar Oct 08 '22 17:10 pscheid92