Skip to content

🔥 feat: Support for SendEarlyHints#3483

Merged
ReneWerner87 merged 13 commits intogofiber:mainfrom
pjebs:early-hints
Aug 21, 2025
Merged

🔥 feat: Support for SendEarlyHints#3483
ReneWerner87 merged 13 commits intogofiber:mainfrom
pjebs:early-hints

Conversation

@pjebs
Copy link
Contributor

@pjebs pjebs commented May 27, 2025

Description

Please provide a clear and concise description of the changes you've made and the problem they address. Include the purpose of the change, any relevant issues it solves, and the benefits it brings to the project. If this change introduces new features or adjustments, highlight them here.

Fixes #3211

@pjebs pjebs requested a review from a team as a code owner May 27, 2025 01:49
@coderabbitai
Copy link
Contributor

coderabbitai bot commented May 27, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds HTTP 103 Early Hints support: new Ctx.SendEarlyHints([]string) and Res.SendEarlyHints([]string) APIs and tests; updates SendFile signature to SendFile(file string, config ...SendFile); App.Test changed to parse/discard interim 1xx responses and use a testable httpReadResponse wrapper; tests added for early hints and App.Test edge cases.

Changes

Cohort / File(s) Summary
Ctx API changes
ctx_interface_gen.go
Adds SendEarlyHints(hints []string) error; changes SendFile(file string) errorSendFile(file string, config ...SendFile) error.
Res API & impl
res_interface_gen.go, res.go
Adds SendEarlyHints(hints []string) error to Res and implements func (r *DefaultRes) SendEarlyHints(hints []string) error (appends Link headers and calls fasthttp EarlyHints()).
App test client
app.go
App.Test now loops to read interim 1xx responses, discards interim bodies, returns final response; introduces httpReadResponse var for test overriding.
Tests & helpers
ctx_test.go, app_test.go
Adds Test_Ctx_SendEarlyHints and three App.Test edge-case tests; adds test helpers errorReadCloser, doubleCloseBody, test errors; tests override httpReadResponse.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant TestClient as App.Test
  participant App
  participant Handler
  participant Res
  participant Fasthttp

  Client->>TestClient: raw request (test harness)
  TestClient->>App: deliver request
  App->>Handler: invoke route
  Handler->>Res: SendEarlyHints([]Link)
  Res->>Fasthttp: append Link headers and call EarlyHints()
  Fasthttp-->>Client: 103 Early Hints (Link headers)  -- interim
  Note right of TestClient: loop reads interim 1xx, discards body, continues
  Handler->>Res: write final status & body
  Res->>Fasthttp: send final response
  Fasthttp-->>Client: final response (e.g., 200 OK)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Assessment against linked issues

Objective Addressed Explanation
Add Early Hints (HTTP 103) capability in Fiber (#3211)
Provide Ctx API to send Early Hints (#3211)
Implement via connection hijacking and manual 103 construction (#3211) Implementation calls fasthttp EarlyHints() and appends Link headers; no explicit hijack/manual raw write present.
Ensure Early Hints are sent before final response (#3211)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
App.Test multi-response loop and httpReadResponse test wrapper (app.go) Test-client read-loop and test wrapper are client-side test harness changes, not required by the proposal which targeted server-side Early Hints. (app.go)
Test helper types simulating read/close failures (app_test.go) Added test scaffolding (errorReadCloser, doubleCloseBody) to simulate client-side errors; these are test utilities unrelated to the original server-side proposal. (app_test.go)

Possibly related PRs

Suggested reviewers

  • sixcolors
  • grivera64
  • ReneWerner87

Poem

I tapped my paw and sent a hint,
A tiny 103—so bright, a glint!
Links raced off, the browser ran,
Final bytes arrived to finish the plan.
🥕🐇

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@pjebs
Copy link
Contributor Author

pjebs commented May 27, 2025

I can't figure out why the tests are failing. Something not quite right with Fasthttp

@gaby gaby changed the title - implement SendEarlyHints 🔥 feat: Support for SendEarlyHints May 27, 2025
@gaby gaby added the v3 label May 27, 2025
@gaby gaby added this to v3 May 27, 2025
@gaby gaby moved this to In Progress in v3 May 27, 2025
@gaby gaby added this to the v3 milestone May 27, 2025
@gaby gaby requested a review from Copilot May 27, 2025 01:51
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
ctx.go (2)

1563-1566: Clarify documentation about Link header handling.

The documentation states "Only Link headers already written to the response will be transmitted as Early Hints" but the implementation actually adds the provided hints as Link headers first (line 1575), then calls EarlyHints(). This seems inconsistent.

Consider updating the documentation to accurately reflect the implementation:

-// SendEarlyHints allows the server to hint to the browser what resources a page would need
-// so the browser can preload them while waiting for the server's full response. Only Link
-// headers already written to the response will be transmitted as Early Hints.
+// SendEarlyHints allows the server to hint to the browser what resources a page would need
+// so the browser can preload them while waiting for the server's full response. The provided
+// hints are added as Link headers to the response before transmitting the Early Hints.

1573-1578: Consider adding input validation for better robustness.

The method doesn't validate the input hints slice or individual hint formats. While the underlying fasthttp library may handle some edge cases, adding basic validation would improve robustness and provide clearer error messages.

Consider adding input validation:

 func (c *DefaultCtx) SendEarlyHints(hints []string) error {
+	if len(hints) == 0 {
+		return nil // No hints to send
+	}
 	for _, h := range hints {
+		if h == "" {
+			continue // Skip empty hints
+		}
 		c.fasthttp.Response.Header.Add("Link", h)
 	}
 	return c.fasthttp.EarlyHints()
 }

Alternatively, you could validate Link header format according to RFC 8288 if stricter validation is desired.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d756ec1 and c131394.

📒 Files selected for processing (3)
  • ctx.go (1 hunks)
  • ctx_interface_gen.go (1 hunks)
  • ctx_test.go (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: repeated
  • GitHub Check: Compare
🔇 Additional comments (1)
ctx_interface_gen.go (1)

296-306: Interface definition looks correct.

The method signature and documentation are consistent with the implementation in ctx.go. Since this is an auto-generated file (as noted in line 1), any documentation improvements should be made in the source file (ctx.go) and the interface will be updated accordingly when regenerated.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces support for the HTTP/2+ SendEarlyHints feature allowing the server to hint to the browser which resources to preload.

  • Adds a new SendEarlyHints method to the Ctx interface and its implementation in DefaultCtx.
  • Provides a new test case in ctx_test.go to validate the SendEarlyHints functionality.

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
ctx_test.go Added a unit test for the SendEarlyHints method.
ctx_interface_gen.go Updated the Ctx interface to include the SendEarlyHints method.
ctx.go Implemented the SendEarlyHints method using fasthttp's EarlyHints API.
Comments suppressed due to low confidence (1)

ctx_test.go:3113

  • Consider adding additional test cases with multiple early hints to ensure that all Link headers are correctly processed, including scenarios with duplicates or a mixture of different hint strings.
err := c.SendEarlyHints([]string{"<https://cdn.com>; rel=preload; as=script"})

@gaby
Copy link
Member

gaby commented May 27, 2025

I can't figure out why the tests are failing. Something not quite right with Fasthttp

You have a race condition in the SendEarlyHints test:

=== Failed
=== FAIL: . Test_Ctx_SendEarlyHints (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
	panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0xcfe6f4]

goroutine 186 [running]:
testing.tRunner.func1.2({0x1097120, 0x1812bc0})
	/opt/hostedtoolcache/go/1.23.9/x64/src/testing/testing.go:1632 +0x3fc
testing.tRunner.func1()
	/opt/hostedtoolcache/go/1.23.9/x64/src/testing/testing.go:1635 +0x6b6
panic({0x1097120?, 0x1812bc0?})
	/opt/hostedtoolcache/go/1.23.9/x64/src/runtime/panic.go:791 +0x132
github.com/valyala/fasthttp.acquireWriter(0xc0002e1208)
	/home/runner/go/pkg/mod/github.com/valyala/fasthttp@v1.62.0/server.go:2736 +0x54
github.com/valyala/fasthttp.(*RequestCtx).EarlyHints(0xc0002e1208)
	/home/runner/go/pkg/mod/github.com/valyala/fasthttp@v1.62.0/server.go:647 +0xab
github.com/gofiber/fiber/v3.(*DefaultCtx).SendEarlyHints(0xc0002f6308, {0xc000264370, 0x1, 0x4bdb1b?})
	/home/runner/work/fiber/fiber/ctx.go:1577 +0x1d6
github.com/gofiber/fiber/v3.Test_Ctx_SendEarlyHints(0xc0003bb380)
	/home/runner/work/fiber/fiber/ctx_test.go:3123 +0xf9
testing.tRunner(0xc0003bb380, 0x11a8b70)
	/opt/hostedtoolcache/go/1.23.9/x64/src/testing/testing.go:1690 +0x227
created by testing.(*T).Run in goroutine 1
	/opt/hostedtoolcache/go/1.23.9/x64/src/testing/testing.go:1743 +0x826

@pjebs
Copy link
Contributor Author

pjebs commented May 27, 2025

How can I fix it? It's not obvious where the race-condition is.

@gaby
Copy link
Member

gaby commented May 27, 2025

How can I fix it? It's not obvious where the race-condition is.

Seems the failure comes from Fasthttp? It happens when you call return c.fasthttp.EarlyHints()

@grivera64
Copy link
Member

grivera64 commented May 27, 2025

How can I fix it? It's not obvious where the race-condition is.

I would recommend using the app.Test() function for this kind of unit test. I ran into a similar race condition when I was working on c.SendStreamWriter() and c.End(). (See Issue #3278, PR #3279, and the app.Test() docs)

@pjebs
Copy link
Contributor Author

pjebs commented May 27, 2025

Something is wrong. I changed the code.

app.Get("/earlyhints", func(c Ctx) error {
		c.SendEarlyHints(hints)
		c.Status(StatusBadRequest)
		return nil
})

resp.StatusCode is not updating to last status code. It is retaining 103 Early Hints instead of 400 Bad Request

This is how the test should look like: https://github.com/valyala/fasthttp/blob/master/server_test.go#L1922

@codecov
Copy link

codecov bot commented May 27, 2025

Codecov Report

❌ Patch coverage is 88.46154% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.81%. Comparing base (ac909e8) to head (03a8a25).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
res.go 62.50% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3483      +/-   ##
==========================================
- Coverage   91.81%   91.81%   -0.01%     
==========================================
  Files         114      114              
  Lines       11498    11517      +19     
==========================================
+ Hits        10557    10574      +17     
- Misses        681      682       +1     
- Partials      260      261       +1     
Flag Coverage Δ
unittests 91.81% <88.46%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
ctx_test.go (1)

3114-3114: ⚠️ Potential issue

Fix error handling for SendEarlyHints call.

The error return value from c.SendEarlyHints(hints) is not being checked, which violates Go best practices and triggers static analysis warnings.

Apply this fix:

-		c.SendEarlyHints(hints)
+		err := c.SendEarlyHints(hints)
+		if err != nil {
+			return err
+		}
🧹 Nitpick comments (1)
ctx_test.go (1)

3122-3123: Remove debug print statements.

These debug print statements should be removed from the test as they add noise to test output.

-	fmt.Println(resp.StatusCode)
-	fmt.Println(resp.Header)
-
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c131394 and 3965500.

📒 Files selected for processing (1)
  • ctx_test.go (1 hunks)
🧰 Additional context used
🪛 GitHub Check: lint
ctx_test.go

[failure] 3124-3124:
Error return value of c.SendEarlyHints is not checked (errcheck)

🪛 GitHub Actions: golangci-lint
ctx_test.go

[error] 3124-3124: golangci-lint: Error return value of c.SendEarlyHints is not checked (errcheck)

⏰ Context from checks skipped due to timeout of 90000ms (6)
  • GitHub Check: unit (1.24.x, windows-latest)
  • GitHub Check: unit (1.23.x, windows-latest)
  • GitHub Check: unit (1.24.x, macos-13)
  • GitHub Check: unit (1.23.x, macos-13)
  • GitHub Check: repeated
  • GitHub Check: Compare

@grivera64
Copy link
Member

Something is wrong. I changed the code.

app.Get("/earlyhints", func(c Ctx) error {
		c.SendEarlyHints(hints)
		c.Status(StatusBadRequest)
		return nil
})

resp.StatusCode is not updating to last status code. It is retaining 103 Early Hints instead of 400 Bad Request

This is how the test should look like: https://github.com/valyala/fasthttp/blob/master/server_test.go#L1922

Can you try c.SendStatus(StatusBadRequest)? Maybe that's causing an issue, though I would also expect c.Status() to also work.

@pjebs
Copy link
Contributor Author

pjebs commented May 27, 2025

Didn't work

@grivera64
Copy link
Member

I tested it locally, and I can confirm that c.SendStatus() doesn't report StatusBadRequest properly either.

My thoughts are that one of the following is happening:

  1. The app.Test() method only parses the EarlyHint, but doesn't parse the rest of the response.

For example, the HTTP response:

HTTP/1.1 103 Early Hints
Link: <https://cdn.com>; rel=preload; as=script

HTTP/1.1 200 OK
Content-Length: 0
Link: <https://cdn.com>; rel=preload; as=script

Could be parsed as only:

HTTP/1.1 103 Early Hints
Link: <https://cdn.com>; rel=preload; as=script

Possibly ignoring the rest of the response, or

  1. There is some bug with SendEarlyHints(), where it only sends the hints and closes the connection.

If this is working properly locally, I believe that it's #1.

@ReneWerner87
Copy link
Member

@pjebs should i push the patch changes in the branch ?

@ReneWerner87
Copy link
Member

i applied the patch to your branch

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
app_test.go (1)

1762-1781: Good test coverage for the Early Hints feature.

The test effectively validates the integration between SendEarlyHints and the modified Test method. It properly:

  • Tests the happy path scenario with Link headers
  • Verifies that early hints are preserved in the final response
  • Confirms the final response status and body are correct
  • Uses appropriate assertions with proper error handling

The test follows established patterns in the file and provides good coverage for the basic functionality.

Consider adding additional test cases to enhance coverage:

func Test_App_Test_EarlyHints_Multiple(t *testing.T) {
    t.Parallel()
    
    app := New()
    app.Get("/multiple", func(c Ctx) error {
        c.SendEarlyHints([]string{"<https://cdn.com>; rel=preload; as=script"})
        c.SendEarlyHints([]string{"<https://fonts.com>; rel=preload; as=font"})
        return c.Status(StatusOK).SendString("done")
    })
    
    req := httptest.NewRequest(MethodGet, "/multiple", nil)
    resp, err := app.Test(req)
    
    require.NoError(t, err)
    require.Equal(t, StatusOK, resp.StatusCode)
    // Verify multiple Link headers are combined
    require.Len(t, resp.Header["Link"], 2)
}

func Test_App_Test_EarlyHints_Error_After_Hints(t *testing.T) {
    t.Parallel()
    
    app := New()
    app.Get("/error", func(c Ctx) error {
        c.SendEarlyHints([]string{"<https://cdn.com>; rel=preload; as=script"})
        return c.Status(StatusBadRequest).SendString("error")
    })
    
    req := httptest.NewRequest(MethodGet, "/error", nil)
    resp, err := app.Test(req)
    
    require.NoError(t, err)
    require.Equal(t, StatusBadRequest, resp.StatusCode)
    // Verify early hints are still present even with error response
    require.NotEmpty(t, resp.Header["Link"])
}

These additional tests would provide more comprehensive coverage of edge cases and multiple early hints scenarios.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 69617fa and 6df2845.

📒 Files selected for processing (3)
  • app.go (1 hunks)
  • app_test.go (1 hunks)
  • ctx_test.go (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • ctx_test.go
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: ReneWerner87
PR: gofiber/fiber#0
File: :0-0
Timestamp: 2024-12-01T10:28:36.011Z
Learning: Feature request #3224 has been created to add support for square bracket notation and comma-separated values in multipart form data in Fiber, while maintaining binary data transfer capabilities. This would bring parity with the existing form-urlencoded functionality.
Learnt from: ReneWerner87
PR: gofiber/fiber#3161
File: app.go:923-932
Timestamp: 2024-11-15T07:56:21.623Z
Learning: In the Fiber framework, breaking changes are acceptable when moving from version 2 to version 3, including modifications to method signatures such as in the `Test` method in `app.go`.
Learnt from: efectn
PR: gofiber/fiber#3162
File: app_test.go:893-895
Timestamp: 2024-11-29T12:37:27.581Z
Learning: In the `Test_App_ShutdownWithContext` function in `app_test.go`, the `clientDone` channel is used to synchronize the client's request completion before proceeding, eliminating the need for additional `time.Sleep` calls.
Learnt from: sixcolors
PR: gofiber/fiber#3016
File: middleware/csrf/csrf_test.go:188-193
Timestamp: 2024-10-08T19:06:06.583Z
Learning: In the Fiber framework tests, using `ctx.Response.Header.Cookie` may not be suitable for parsing cookies from the response header, as it requires a `*Cookie` and fills it rather than returning a string value; thus, manual parsing of the `Set-Cookie` header may be necessary.
Learnt from: sixcolors
PR: gofiber/fiber#3016
File: middleware/csrf/csrf_test.go:188-193
Timestamp: 2024-09-25T15:57:10.221Z
Learning: In the Fiber framework tests, using `ctx.Response.Header.Cookie` may not be suitable for parsing cookies from the response header, as it requires a `*Cookie` and fills it rather than returning a string value; thus, manual parsing of the `Set-Cookie` header may be necessary.
Learnt from: gaby
PR: gofiber/fiber#3193
File: middleware/adaptor/adaptor.go:111-111
Timestamp: 2024-11-10T23:44:13.704Z
Learning: In the `middleware/adaptor/adaptor.go` file of the Fiber framework, when updating context handling, replacing `c.Context()` with `c.RequestCtx()` is appropriate to access the `fasthttp.RequestCtx`.
app_test.go (2)
Learnt from: ReneWerner87
PR: gofiber/fiber#3161
File: app.go:923-932
Timestamp: 2024-11-15T07:56:21.623Z
Learning: In the Fiber framework, breaking changes are acceptable when moving from version 2 to version 3, including modifications to method signatures such as in the `Test` method in `app.go`.
Learnt from: efectn
PR: gofiber/fiber#3162
File: app_test.go:893-895
Timestamp: 2024-11-29T12:37:27.581Z
Learning: In the `Test_App_ShutdownWithContext` function in `app_test.go`, the `clientDone` channel is used to synchronize the client's request completion before proceeding, eliminating the need for additional `time.Sleep` calls.
app.go (5)
Learnt from: ReneWerner87
PR: gofiber/fiber#3161
File: app.go:923-932
Timestamp: 2024-11-15T07:56:21.623Z
Learning: In the Fiber framework, breaking changes are acceptable when moving from version 2 to version 3, including modifications to method signatures such as in the `Test` method in `app.go`.
Learnt from: efectn
PR: gofiber/fiber#3162
File: app_test.go:893-895
Timestamp: 2024-11-29T12:37:27.581Z
Learning: In the `Test_App_ShutdownWithContext` function in `app_test.go`, the `clientDone` channel is used to synchronize the client's request completion before proceeding, eliminating the need for additional `time.Sleep` calls.
Learnt from: sixcolors
PR: gofiber/fiber#3016
File: middleware/session/middleware_test.go:400-407
Timestamp: 2024-09-25T17:09:03.756Z
Learning: In the `Test_Session_Next` function in `middleware/session/middleware_test.go`, the variable `doNext` is properly synchronized with the `muNext` mutex for both read and write access.
Learnt from: sixcolors
PR: gofiber/fiber#3051
File: middleware/session/session.go:215-216
Timestamp: 2024-10-08T19:06:06.583Z
Learning: Parallel tests for `Session.Save` already exist in the `middleware/session/session_test.go` file, specifically in the `Test_Session_Save` and `Test_Session_Save_Expiration` functions.
Learnt from: sixcolors
PR: gofiber/fiber#3051
File: middleware/session/session.go:215-216
Timestamp: 2024-06-30T00:38:06.580Z
Learning: Parallel tests for `Session.Save` already exist in the `middleware/session/session_test.go` file, specifically in the `Test_Session_Save` and `Test_Session_Save_Expiration` functions.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Compare
  • GitHub Check: unit (1.24.x, macos-latest)
  • GitHub Check: unit (1.24.x, macos-13)
  • GitHub Check: repeated
  • GitHub Check: lint
  • GitHub Check: unit (1.24.x, windows-latest)
  • GitHub Check: unit (1.24.x, ubuntu-latest)
  • GitHub Check: Analyse
🔇 Additional comments (1)
app.go (1)

1119-1143: LGTM! Excellent implementation of multi-response handling.

The modification correctly implements support for processing multiple HTTP responses, which is essential for the Early Hints feature. The loop logic properly:

  • Reads responses sequentially until a non-1xx response is found
  • Handles resource cleanup by discarding interim response bodies
  • Maintains robust error handling for edge cases
  • Preserves backward compatibility with existing functionality

The implementation aligns well with the PR objective to support HTTP 103 Early Hints while maintaining the existing Test method behavior.

@grivera64
Copy link
Member

grivera64 commented Jul 17, 2025

i applied the patch to your branch

@ReneWerner87 @pjebs the codecov workflow wants more coverage for the app.Test() patch.

Should we add a new Test_App_Test_MultipleResponses() unit test in app_test.go for more coverage?

@ReneWerner87
Copy link
Member

@grivera64 can you add this ?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
res.go (2)

695-699: Prefer using Res/RequestCtx accessors + HeaderLink constant; short-circuit empty hints.

Improves encapsulation, consistency with surrounding code, and avoids emitting a bare 103 without Link values.

 func (r *DefaultRes) SendEarlyHints(hints []string) error {
-	for _, h := range hints {
-		r.c.fasthttp.Response.Header.Add("Link", h)
-	}
-	return r.c.fasthttp.EarlyHints()
+	if len(hints) == 0 {
+		return nil
+	}
+	for _, h := range hints {
+		r.Response().Header.Add(HeaderLink, h)
+	}
+	return r.RequestCtx().EarlyHints()
 }

688-691: Doc nit: clarify protocol wording.

“HTTP/2+” is vague. Suggest “HTTP/2 or HTTP/3” and explicitly note that some HTTP/1.1 non-browser clients may ignore interim responses.

-// This is a HTTP/2+ feature but all browsers will either understand it or safely ignore it.
+// Early Hints are primarily effective with HTTP/2 or HTTP/3. Modern browsers understand them,
+// while older clients will safely ignore interim 1xx responses.
ctx_test.go (2)

3869-3878: Close response body and use Header.Values for robustness.

Avoids resource leaks and tolerates header coalescing behavior.

 req := httptest.NewRequest(MethodGet, "/earlyhints", nil)
 resp, err := app.Test(req)
 
 require.NoError(t, err, "app.Test(req)")
+defer func() { require.NoError(t, resp.Body.Close()) }()
 require.Equal(t, StatusBadRequest, resp.StatusCode, "Status code")
-require.Equal(t, hints, resp.Header["Link"], "Link header")
+require.Equal(t, hints, resp.Header.Values(HeaderLink), "Link header")
 body, err := io.ReadAll(resp.Body)
 require.NoError(t, err)
 require.Equal(t, "fail", string(body))

3856-3879: Add coverage: multiple hints and empty hints.

Consider two quick additions:

  • A subtest that sends two Link hints and asserts both values are present (order-preserving).
  • A subtest that calls SendEarlyHints([]string{}) and asserts the final response succeeds and contains no Link header.

I can draft these if you want them in this file or in app_test.go to validate end-to-end behavior with the updated App.Test.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6fba303 and 08c2932.

📒 Files selected for processing (5)
  • app.go (2 hunks)
  • app_test.go (1 hunks)
  • ctx_interface_gen.go (1 hunks)
  • ctx_test.go (1 hunks)
  • res.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • app_test.go
  • ctx_interface_gen.go
  • app.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-11-29T12:37:27.581Z
Learnt from: efectn
PR: gofiber/fiber#3162
File: app_test.go:893-895
Timestamp: 2024-11-29T12:37:27.581Z
Learning: In the `Test_App_ShutdownWithContext` function in `app_test.go`, the `clientDone` channel is used to synchronize the client's request completion before proceeding, eliminating the need for additional `time.Sleep` calls.

Applied to files:

  • ctx_test.go
🧬 Code Graph Analysis (2)
res.go (1)
client/response.go (1)
  • Response (19-25)
ctx_test.go (2)
ctx_interface_gen.go (1)
  • Ctx (17-416)
constants.go (2)
  • StatusBadRequest (73-73)
  • MethodGet (5-5)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: unit (1.25.x, windows-latest)
  • GitHub Check: unit (1.25.x, macos-13)
  • GitHub Check: repeated
  • GitHub Check: Compare
🔇 Additional comments (2)
res.go (1)

684-699: Solid addition; behavior matches RFC 8297 intent.

The method appends Link headers and delegates to fasthttp’s EarlyHints emitter without mutating the final status. This aligns with the design discussed in the PR and the tests.

ctx_test.go (1)

3856-3879: Exercise looks good; assert final status and Link presence.

The test now validates final status and the Link header after calling SendEarlyHints. Nice.

@grivera64
Copy link
Member

@grivera64 can you add this ?

@ReneWerner87 After taking a look, I was able to slightly increase the coverage by a bit by mocking failed http.ReadResponse() returns. I did this by adding a new httpReadResponse variable that can be overridden and restored (as long as the test isn't ran in parallel with other tests.

Below is the diff I am thinking of applying. Let me know what you think:

Click here for `git diff` output

diff --git a/app.go b/app.go
index c8c59feb..42c94321 100644
--- a/app.go
+++ b/app.go
@@ -487,6 +487,9 @@ var DefaultMethods = []string{
 	methodPatch:   MethodPatch,
 }
 
+// httpReadResponse - Used for test mocking http.ReadResponse
+var httpReadResponse = http.ReadResponse
+
 // DefaultErrorHandler that process return errors from handlers
 func DefaultErrorHandler(c Ctx, err error) error {
 	code := StatusInternalServerError
@@ -1119,7 +1122,7 @@ func (app *App) Test(req *http.Request, config ...TestConfig) (*http.Response, e
 	var res *http.Response
 	for {
 		// Convert raw http response to *http.Response
-		res, err = http.ReadResponse(buffer, req)
+		res, err = httpReadResponse(buffer, req)
 		if err != nil {
 			if errors.Is(err, io.ErrUnexpectedEOF) {
 				return nil, ErrTestGotEmptyResponse
diff --git a/app_test.go b/app_test.go
index c438b9ed..37f08e91 100644
--- a/app_test.go
+++ b/app_test.go
@@ -6,6 +6,7 @@
 package fiber
 
 import (
+	"bufio"
 	"bytes"
 	"context"
 	"crypto/tls"
@@ -1839,8 +1840,118 @@ func Test_App_Test_drop_empty_response(t *testing.T) {
 	require.ErrorIs(t, err, ErrTestGotEmptyResponse)
 }
 
-func Test_App_Test_EarlyHints(t *testing.T) {
-	t.Parallel()
+func Test_App_Test_response_error(t *testing.T) {
+	// Note: Test cannot run in parallel due to
+	// overriding the httpReadResponse global variable.
+	// t.Parallel()
+
+	// Override httpReadResponse temporarily
+	oldHTTPReadResponse := httpReadResponse
+	defer func() {
+		httpReadResponse = oldHTTPReadResponse
+	}()
+	httpReadResponse = func(_ *bufio.Reader, _ *http.Request) (*http.Response, error) {
+		return nil, errErrorReader
+	}
+
+	app := New()
+	app.Get("/", func(c Ctx) error {
+		return c.SendStatus(StatusOK)
+	})
+
+	_, err := app.Test(httptest.NewRequest(MethodGet, "/", nil), TestConfig{
+		Timeout:       0,
+		FailOnTimeout: false,
+	})
+	require.ErrorIs(t, err, errErrorReader)
+}
+
+type errorReadCloser int
+
+var errInvalidReadOnBody = errors.New("test: invalid Read on body")
+
+func (errorReadCloser) Read(_ []byte) (int, error) {
+	return 0, errInvalidReadOnBody
+}
+
+func (errorReadCloser) Close() error {
+	return nil
+}
+
+func Test_App_Test_ReadFail(t *testing.T) {
+	// Note: Test cannot run in parallel due to
+	// overriding the httpReadResponse global variable.
+	// t.Parallel()
+
+	// Override httpReadResponse temporarily
+	oldHTTPReadResponse := httpReadResponse
+	defer func() {
+		httpReadResponse = oldHTTPReadResponse
+	}()
+
+	httpReadResponse = func(r *bufio.Reader, req *http.Request) (*http.Response, error) {
+		resp, err := http.ReadResponse(r, req)
+		require.NoError(t, resp.Body.Close())
+		resp.Body = errorReadCloser(0)
+		return resp, err //nolint:wrapcheck // unnecessary to wrap it
+	}
+
+	app := New()
+	hints := []string{"<https://cdn.com>; rel=preload; as=script"}
+	app.Get("/early", func(c Ctx) error {
+		err := c.SendEarlyHints(hints)
+		require.NoError(t, err)
+		return c.SendStatus(StatusOK)
+	})
+
+	req := httptest.NewRequest(MethodGet, "/early", nil)
+	_, err := app.Test(req)
+
+	require.ErrorIs(t, err, errInvalidReadOnBody)
+}
+
+var errDoubleClose = errors.New("test: double close")
+
+type doubleCloseBody struct {
+	isClosed bool
+}
+
+func (b *doubleCloseBody) Read(_ []byte) (int, error) {
+	if b.isClosed {
+		return 0, errInvalidReadOnBody
+	}
+
+	// Close after reading EOF
+	_ = b.Close() //nolint:errcheck // It is fine to ignore the error here
+	return 0, io.EOF
+}
+
+func (b *doubleCloseBody) Close() error {
+	if b.isClosed {
+		return errDoubleClose
+	}
+
+	b.isClosed = true
+	return nil
+}
+
+func Test_App_Test_CloseFail(t *testing.T) {
+	// Note: Test cannot run in parallel due to
+	// overriding the httpReadResponse global variable.
+	// t.Parallel()
+
+	// Override httpReadResponse temporarily
+	oldHTTPReadResponse := httpReadResponse
+	defer func() {
+		httpReadResponse = oldHTTPReadResponse
+	}()
+
+	httpReadResponse = func(r *bufio.Reader, req *http.Request) (*http.Response, error) {
+		resp, err := http.ReadResponse(r, req)
+		_ = resp.Body.Close() //nolint:errcheck // It is fine to ignore the error here
+		resp.Body = &doubleCloseBody{}
+		return resp, err //nolint:wrapcheck // unnecessary to wrap it
+	}
 
 	app := New()
 	hints := []string{"<https://cdn.com>; rel=preload; as=script"}
@@ -1851,14 +1962,9 @@ func Test_App_Test_EarlyHints(t *testing.T) {
 	})
 
 	req := httptest.NewRequest(MethodGet, "/early", nil)
-	resp, err := app.Test(req)
+	_, err := app.Test(req)
 
-	require.NoError(t, err)
-	require.Equal(t, StatusOK, resp.StatusCode)
-	require.Equal(t, hints, resp.Header["Link"])
-	body, err := io.ReadAll(resp.Body)
-	require.NoError(t, err)
-	require.Equal(t, "done", string(body))
+	require.ErrorIs(t, err, errDoubleClose)
 }
 
 func Test_App_SetTLSHandler(t *testing.T) {

Copy link
Member

@grivera64 grivera64 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that the PR reports better code coverage, this LGTM! 🚀

@gaby
Copy link
Member

gaby commented Aug 20, 2025

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for sending Early Hints (103 responses), a feature for improving web performance by allowing browsers to preload resources. The changes include a new SendEarlyHints method on the context, updates to the Test function to handle 1xx responses, and comprehensive tests for the new functionality. The implementation is solid and well-tested. I have one minor suggestion to handle cases where SendEarlyHints is called with an empty list of hints to avoid sending a pointless 103 response.

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
ctx_interface_gen.go (2)

368-378: Doc/API mismatch and clarity pass for SendEarlyHints

The docs say “Only Link headers already written to the response will be transmitted as Early Hints”, but the API accepts hints []string. Per the PR context, the implementation appends these hints as Link headers before sending 103. Let's align the comment to the actual behavior, tighten the HTTP version note, and fix a small grammar nit (“an HTTP/2+”):

-	// SendEarlyHints allows the server to hint to the browser what resources a page would need
-	// so the browser can preload them while waiting for the server's full response. Only Link
-	// headers already written to the response will be transmitted as Early Hints.
-	//
-	// This is a HTTP/2+ feature but all browsers will either understand it or safely ignore it.
-	//
-	// NOTE: Older HTTP/1.1 non-browser clients may face compatibility issues.
-	//
-	// See: https://developer.chrome.com/docs/web-platform/early-hints and
-	// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Link#syntax
+	// SendEarlyHints sends an HTTP 103 Early Hints informational response.
+	// The provided hints are appended as Link headers and sent immediately,
+	// allowing clients to preload resources while the final response is prepared.
+	// Only Link headers are sent as Early Hints; all other headers are ignored.
+	//
+	// Recommended: call before writing final response headers/body.
+	// Most effective over HTTP/2 or HTTP/3; some HTTP/1.1 clients and intermediaries
+	// may ignore or drop 103 responses.
+	//
+	// See: RFC 8297 (Early Hints),
+	// https://developer.chrome.com/docs/web-platform/early-hints,
+	// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
 	SendEarlyHints(hints []string) error

378-378: Consider a variadic signature for ergonomics and allocation-free call sites

Links(link ...string) already uses varargs; mirroring that here would improve ergonomics and avoid slice allocations at call sites.

-SendEarlyHints(hints []string) error
+SendEarlyHints(hints ...string) error

Note: This is an interface change and will cascade to implementations and tests; only consider if you’re comfortable adjusting the rest of the PR accordingly.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 42e67b2 and 61840b4.

📒 Files selected for processing (2)
  • ctx_interface_gen.go (1 hunks)
  • res.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • res.go
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Compare
  • GitHub Check: unit (1.25.x, macos-13)
  • GitHub Check: repeated
  • GitHub Check: unit (1.25.x, windows-latest)
  • GitHub Check: unit (1.25.x, macos-latest)
  • GitHub Check: lint

@gaby
Copy link
Member

gaby commented Aug 20, 2025

Someone needs to run make generate

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
res_interface_gen.go (2)

112-122: API alignment question: name/signature vs Express writeEarlyHints

Express uses res.writeEarlyHints(headers[, cb]). We’re exposing SendEarlyHints([]string). If Express alignment and API longevity are goals, consider whether we want a map-like input (supporting multiple headers and non-Link fields for future-proofing) or keep Fiber’s narrowly-scoped Link-only API. If keeping Link-only, the docs should explicitly state expected value format (e.g., </style.css>; rel=preload; as=style) and dedup behavior.

Would you like me to draft an alternative API proposal (backwards-compatible) that can accept either []string (Link values) or a map[string][]string, while keeping the implementation constrained to sending only Link in the 103?


112-122: Update Early Hints documentation in source and regenerate
Please update the comment on DefaultRes.SendEarlyHints in res.go as shown below, then run make generate to propagate it to res_interface_gen.go:

@@ -691,12 +691,23 @@
-// SendEarlyHints allows the server to hint to the browser what resources a page would need
-// so the browser can preload them while waiting for the server's full response. Only Link
-// headers already written to the response will be transmitted as Early Hints.
-//
-// This is a HTTP/2+ feature but all browsers will either understand it or safely ignore it.
-//
-// NOTE: Older HTTP/1.1 non-browser clients may face compatibility issues.
-//
-// See: https://developer.chrome.com/docs/web-platform/early-hints and
-// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Link#syntax
+// SendEarlyHints emits a 103 Early Hints response (RFC 8297).
+// The provided hints are appended as Link headers and sent in the 103 response.
+// Only Link headers are included in Early Hints; other headers are typically ignored by clients.
+//
+// Call early, before writing the final response body. If hints is empty, this is a no-op.
+// Returns an error if the underlying server fails to write the 103 response.
+//
+// Note: RFC 8297 applies to both HTTP/1.1 and HTTP/2+. In practice, browsers act on Early
+// Hints primarily over HTTP/2/HTTP/3. When using Fiber/fasthttp (HTTP/1.1), deploy behind
+// a proxy that forwards 103 to clients for best results.
+//
+// See: https://datatracker.ietf.org/doc/html/rfc8297 and
+// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link#syntax

• File: res.go, around line 691
• Run make generate to update res_interface_gen.go

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6d36a4b and 03a8a25.

📒 Files selected for processing (1)
  • res_interface_gen.go (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Compare
  • GitHub Check: lint
  • GitHub Check: unit (1.25.x, macos-13)
  • GitHub Check: unit (1.25.x, macos-latest)
  • GitHub Check: unit (1.25.x, windows-latest)
  • GitHub Check: repeated

@gaby gaby requested a review from ReneWerner87 August 20, 2025 13:06
@ReneWerner87 ReneWerner87 merged commit f1deedb into gofiber:main Aug 21, 2025
14 of 15 checks passed
@github-project-automation github-project-automation bot moved this from In Progress to Done in v3 Aug 21, 2025
@pjebs pjebs deleted the early-hints branch October 27, 2025 00:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

📝 [Proposal]: Early hints

5 participants