fiber icon indicating copy to clipboard operation
fiber copied to clipboard

📝 [Proposal]: Early hints

Open ReneWerner87 opened this issue 1 year ago • 14 comments

Feature Proposal Description

Objective:

Introduce support for the HTTP 103 Early Hints status code in the Go Fiber framework to enhance web performance by allowing clients to preload resources before the final response is ready.

Background:

The HTTP 103 Early Hints status code enables servers to send preliminary responses containing links to resources that clients can begin preloading while the server prepares the final response. This mechanism reduces page load times by utilizing the client’s idle time effectively.

https://developer.chrome.com/docs/web-platform/early-hints

Example: https://www.103earlyhints.com/ image

Internal Implementation:

  • Connection Hijacking: Utilize Fasthttp’s connection hijacking to gain direct access to the underlying network connection, enabling the sending of 1xx status codes.
  • Manual Response Construction: Construct the 103 response manually, including the appropriate Link headers, and write it to the connection.
  • Synchronization: Ensure that the Early Hints are sent before the final response, managing timing to prevent race conditions.

Note: Careful handling is required to manage the connection state and ensure compatibility with existing middleware and response handling mechanisms.

conn, brw, err := c.Fasthttp.Hijack()
if err != nil {
    return fmt.Errorf("Error on Hijacken the connection %v", err)
}
defer conn.Close()

// Early Hints send
earlyHints := "HTTP/1.1 103 Early Hints\r\n" +
    "Link: </styles.css>; rel=preload; as=style\r\n" +
    "Link: </script.js>; rel=preload; as=script\r\n" +
    "\r\n"

if _, err := brw.WriteString(earlyHints); err != nil {
    return fmt.Errorf("Error seinding Early Hints: %v", err)
}
if err := brw.Flush(); err != nil {
    return fmt.Errorf("Error flushing the buffer: %v", err)
}

Considerations:

  • Browser Support: As of now, not all browsers support 103 Early Hints. Developers should implement feature detection or provide fallbacks as necessary.
  • Security Implications: Ensure that the resources indicated in Early Hints are safe to preload, as clients will initiate requests before receiving the final response.
  • Performance Testing: Conduct thorough performance testing to validate the benefits of Early Hints in various scenarios.

Conclusion:

Implementing HTTP 103 Early Hints in Go Fiber can significantly improve page load times by allowing clients to preload critical resources. This proposal outlines a method to integrate this feature into Fiber, providing developers with a powerful tool to enhance web performance.

Alignment with Express API

As of now, Express.js does not natively support the HTTP 103 Early Hints status code. This limitation stems from the underlying Node.js HTTP server, which, until recently, did not implement support for the 103 status code. Discussions on the Express.js GitHub repository have highlighted that Express.js relies on Node.js to provide this functionality before it can be utilized within the framework.

HTTP RFC Standards Compliance

The proposed implementation of the HTTP 103 Early Hints status code in Go Fiber aligns with the specifications outlined in RFC 8297, titled “An HTTP Status Code for Indicating Hints.” 

Key Compliance Aspects:

  1. Informational Status Code: RFC 8297 introduces the 103 (Early Hints) status code as an informational response, allowing servers to send preliminary header fields to clients before the final response is ready. This proposal ensures that the 103 status code is utilized appropriately to convey early hints to clients.

  2. Use of Link Headers: The RFC specifies that the 103 response is primarily intended to include Link header fields, enabling clients to begin preloading resources. The proposed implementation adheres to this by allowing developers to specify Link headers in the Early Hints response, facilitating resource preloading by clients.

  3. Non-Final Response Handling: According to RFC 8297, clients should process the 103 response as a non-final response and continue waiting for the final response. The implementation ensures that the 103 Early Hints response is sent prior to the final response, maintaining the correct sequence as per the RFC.

  4. Compatibility Considerations: The RFC advises caution when sending 103 responses over HTTP/1.1 due to potential client compatibility issues. The proposed implementation includes mechanisms to detect client capabilities and conditionally send Early Hints, thereby adhering to the compatibility considerations outlined in the RFC.

By aligning with these key aspects of RFC 8297, the proposed feature ensures compliance with HTTP standards, promoting interoperability and enhancing web performance through the effective use of Early Hints.

API Stability

The proposed implementation of the HTTP 103 Early Hints status code in Go Fiber is designed with long-term stability in mind. By adhering to established HTTP standards and following best practices in API design, we aim to provide a robust and reliable feature that minimizes the need for future changes or deprecations.

Feature Examples

API Design: Introduce a method in the fiber.Ctx context to facilitate sending Early Hints.

func (c fiber.Ctx) SendEarlyHints(hints map[string]string) error

Usage Example: Demonstrate how developers can utilize the new method within a route handler.

app.Get("/", func(c fiber.Ctx) error {
    hints := map[string]string{
        "/styles.css": `rel="preload"; as="style"`,
        "/script.js":  `rel="preload"; as="script"`,
    }
    if err := c.SendEarlyHints(hints); err != nil {
        return err
    }
    // Proceed with generating the final response
    return c.SendString("Hello, World!")
})

Checklist:

  • [X] I agree to follow Fiber's Code of Conduct.
  • [X] I have searched for existing issues that describe my proposal before opening this one.
  • [X] I understand that a proposal that does not meet these guidelines may be closed without explanation.

ReneWerner87 avatar Nov 21 '24 07:11 ReneWerner87

@ReneWerner87 According to the Chrome developer link and mdn web docs:

In addition, Early Hints are recommended to only be sent over HTTP/2 or HTTP/3 connections and most browsers will only accept them over those protocols.

Will this lead to problems when implementing this feature, as fasthttp doesn't support (as of now) HTTP/2 or higher? There is an http2 fork of fasthttp according to fasthttp's README.md, but it doesn't seem to have as much support.

I've seen that using a proxy (like nginx) may help us here, but this wouldn't be a native fix within Fiber itself. We could potentially include this work around in the docs for the feature.

grivera64 avatar Dec 01 '24 04:12 grivera64

I feel like this should be implemented in fasthttp first.

gaby avatar Dec 01 '24 08:12 gaby

I feel like this should be implemented in fasthttp first.

could also be done create a proposal in fasthttp as a start

I don't think it will lead to problems, as it is only an addition, but it would clearly be better placed in the core, provided they allow it and don't say that it can be implemented with the current possibilities

ReneWerner87 avatar Dec 01 '24 09:12 ReneWerner87

Early Hints functionality has been merged into fasthttp

pjebs avatar Apr 21 '25 11:04 pjebs

@pjebs Thanks! Now we just got to wait for v1.61.0

gaby avatar Apr 21 '25 11:04 gaby

@pjebs Feature was released https://github.com/valyala/fasthttp/releases/tag/v1.61.0

gaby avatar Apr 22 '25 11:04 gaby

I will create a PR for fiber maybe on the weekend. The only slight complication is for the net/http adapter middleware. I will have to create a new io.Writer to intercept the writes to the fasthttp connection. I need to then check for a HTTP/1.1 103 Early Hints message being written. If so, then I will need to artifically add:

func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(103)

}

I will then need to keep watching for Link headers and keep writing:

w.Header().Add("Link", "x")

... until I observe another HTTP/1.1 ... or HTTP/2 ... message. Then I can stop intercepting and let all the other response messages pass through.

pjebs avatar Apr 22 '25 13:04 pjebs

@pjebs ping

ReneWerner87 avatar May 23 '25 15:05 ReneWerner87

I need to do the CTX thing first

pjebs avatar May 23 '25 22:05 pjebs

@ReneWerner87 What about just adding EarlyHints() to ctx and just passing it through to RequestCtx.EarlyHints() error?

https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.EarlyHints

pjebs avatar May 24 '25 06:05 pjebs

Yeah could work

ReneWerner87 avatar May 24 '25 06:05 ReneWerner87

How about c.SendEarlyHints()? I think that could fit the naming style we have for other c.SendXXX() methods.

We can also consider:

  • Allow passing a slice of hints as an argument directly, or
  • Only have the method flush any hint related headers of the request set by c.Set(key, value) like you would in fasthttp. In this case, maybe c.FlushEarlyHints() might be closer to what it actually does.

What do you all think?

grivera64 avatar May 24 '25 07:05 grivera64

The underlying EarlyHints() won't allow:

Allow passing a slice of hints as an argument directly, or FlushEarlyHints()

(assuming you want to just use passthrough RequestCtx.EarlyHints() behind the scenes).

The only way to implement what you want is for Fiber to manage the EarlyHints functionality independently until it's time to send the EarlyHints - in which case you can then call the underlying RequestCtx.EarlyHints()

pjebs avatar May 24 '25 07:05 pjebs

From my understanding of RequestCtx.EarlyHints(), we could do the following to directly use the new feature:

// Equivalent to ctx.Response.Header().Add("Link", ...)
c.Response().Header().Add("Link", "...")  // Link 1
c.Response().Header().Add("Link", "...")  // Link 2
c.Response().Header().Add("Link", "...")  // Link 3

// Equivalent to ctx.EarlyHints()
c.RequestCtx().EarlyHints()

// Do a long task
time.Sleep(5 * time.Second)

// Send the rest of response
c.Set("Content-Type", "text/html")
return c.SendString("<p>Hello World</p>")

If the above is doable, then we can either:

Option 1. Allow passing a slice of hints as an argument

func (c Ctx) SendEarlyHints(hints []EarlyHint) error
Example:
hints := []fiber.EarlyHint{
    {
        Header: "Link",
        Value: "...",
    },
    {
        Header: "Link",
        Value: "...",
    },
}
c.SendEarlyHints(hints)
// ...

Where c.SendEarlyHints(hints) could be a simple wrapper around the early hints mechanism:

func (c Ctx) SendEarlyHints(hints []EarlyHint) error {
    for _, hint := range hints {
        c.Response().Header().Add(hint.Header, hint.Value)
    }
    return c.RequestCtx().EarlyHints()
}

Or...

Option 2. Only add an internal way to call RequestCtx.EarlyHints(), requiring users to directly set the headers themselves before calling it

func (c Ctx) FlushEarlyHints() error
Example:
c.Response().Header().Add("Link", "...")  // Link 1
c.Response().Header().Add("Link", "...")  // Link 2
c.Response().Header().Add("Link", "...")  // Link 3

c.FlushEarlyHints()

Where c.FlushEarlyHints() is simply a shorthand way to call RequestCtx.EarlyHints():

func (c Ctx) FlushEarlyHints() error {
    return c.RequestCtx().EarlyHints()
}

Would any of these two options be doable? If not, then I think we might want to add some shorthand for c.Response().Header().Add(key, value) to simply do:

c.AddHeader("Link", "...")
c.EarlyHints()

grivera64 avatar May 24 '25 08:05 grivera64