Skip to content

fix: set HTTP server read/write/idle timeouts#446

Open
VaibhavAcharya wants to merge 1 commit into
masterfrom
vaibhavacharya/ex-1863-identity-http-server-missing-timeouts-gotrue
Open

fix: set HTTP server read/write/idle timeouts#446
VaibhavAcharya wants to merge 1 commit into
masterfrom
vaibhavacharya/ex-1863-identity-http-server-missing-timeouts-gotrue

Conversation

@VaibhavAcharya
Copy link
Copy Markdown
Contributor

- Summary

http.Server was constructed with no timeouts, which means Go uses its zero values (no timeout) and a slow client can hold a connection open indefinitely. This sets ReadHeaderTimeout, ReadTimeout, WriteTimeout, and IdleTimeout to standard values (10s, 30s, 60s, 120s) so we drop slowloris-style attacks before they tie up workers.

- Test plan

- Description for the changelog

Add read, write, and idle timeouts to the HTTP server.

- A picture of a cute animal (not mandatory but encouraged)

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Chores
    • Improved server resilience with enhanced HTTP timeout configuration for better handling of long-running requests and connections.

Walkthrough

The PR updates the http.Server initialization in the (*API).ListenAndServe method to explicitly configure the server address and timeout behavior. The Addr field is now set to the passed hostAndPort parameter, and four timeout fields are added with fixed duration values: ReadHeaderTimeout, ReadTimeout, WriteTimeout, and IdleTimeout. The handler assignment remains unchanged. This change improves HTTP server reliability by enforcing timeout limits on request processing and idle connections.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding HTTP server timeouts (read, write, and idle) to address security concerns.
Description check ✅ Passed The description is related to the changeset, explaining the security motivation (preventing slowloris attacks) and documenting the timeout values being set.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch vaibhavacharya/ex-1863-identity-http-server-missing-timeouts-gotrue

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions


Comment @coderabbitai help to get the list of available commands and usage tips.

@VaibhavAcharya VaibhavAcharya marked this pull request as ready for review May 26, 2026 09:32
@VaibhavAcharya VaibhavAcharya requested a review from a team as a code owner May 26, 2026 09:32
Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/api.go`:
- Around line 43-48: The server in api.go currently uses hardcoded timeout
values (ReadHeaderTimeout, ReadTimeout, WriteTimeout, IdleTimeout) when
constructing the http.Server; make these configurable by adding corresponding
timeout fields to the API configuration struct (e.g., ReadHeaderTimeout,
ReadTimeout, WriteTimeout, IdleTimeout of type time.Duration) in
conf/configuration.go (with sensible defaults and env/config tags), load them
into the API config, and replace the hardcoded values in the server
initialization (the http.Server literal that sets Addr, Handler a.handler,
ReadHeaderTimeout, ReadTimeout, WriteTimeout, IdleTimeout) to use the config
values instead. Ensure parsing/decoding supports duration strings so operators
can set env vars like GOTRUE_API_WRITE_TIMEOUT=600s.
- Line 47: The server's fixed http.Server.WriteTimeout = 60 * time.Second can
abort long admin endpoints (e.g., handlers that call sendJSON which
json.Marshal's []*UserForExport) — make the endpoints safe by (1)
validating/capping pagination input (use defaultPerPage and enforce a maxPerPage
constant, validate page/per_page in the admin handlers that produce large
payloads such as /admin/users and /admin/users/export), (2) avoid buffering
entire payloads by replacing the sendJSON call in those handlers with a
streaming/chunked JSON encoder or by implementing an async export job for very
large exports, and (3) remove reliance on the global hard WriteTimeout for these
routes (either make it configurable or rely on request-context deadlines) so
long-running DB + marshal work isn't killed mid-response.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3ece8d0a-8a51-4c35-a41c-827cdd559297

📥 Commits

Reviewing files that changed from the base of the PR and between 30e9d81 and c2c4c0d.

📒 Files selected for processing (1)
  • api/api.go

Comment thread api/api.go
Comment on lines +43 to +48
Addr: hostAndPort,
Handler: a.handler,
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 60 * time.Second,
IdleTimeout: 120 * time.Second,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Make HTTP server timeouts configurable.

The timeout values are currently hardcoded, which prevents operators from tuning them based on their deployment characteristics (e.g., network conditions, workload patterns, security requirements). Different deployments may need different timeout values.

♻️ Proposed approach to make timeouts configurable

Add timeout configuration fields to the API configuration struct in conf/configuration.go:

API struct {
    Host                 string
    Port                 int `envconfig:"PORT" default:"8081"`
    ReadHeaderTimeout    time.Duration `split_words:"true" default:"10s"`
    ReadTimeout          time.Duration `split_words:"true" default:"30s"`
    WriteTimeout         time.Duration `split_words:"true" default:"300s"` // 5 minutes default
    IdleTimeout          time.Duration `split_words:"true" default:"120s"`
    // ... existing fields
}

Then update the server initialization to use these values:

 server := &http.Server{
     Addr:              hostAndPort,
     Handler:           a.handler,
-    ReadHeaderTimeout: 10 * time.Second,
-    ReadTimeout:       30 * time.Second,
-    WriteTimeout:      60 * time.Second,
-    IdleTimeout:       120 * time.Second,
+    ReadHeaderTimeout: a.config.API.ReadHeaderTimeout,
+    ReadTimeout:       a.config.API.ReadTimeout,
+    WriteTimeout:      a.config.API.WriteTimeout,
+    IdleTimeout:       a.config.API.IdleTimeout,
 }

This allows operators to configure timeouts via environment variables (e.g., GOTRUE_API_WRITE_TIMEOUT=600s) or configuration files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/api.go` around lines 43 - 48, The server in api.go currently uses
hardcoded timeout values (ReadHeaderTimeout, ReadTimeout, WriteTimeout,
IdleTimeout) when constructing the http.Server; make these configurable by
adding corresponding timeout fields to the API configuration struct (e.g.,
ReadHeaderTimeout, ReadTimeout, WriteTimeout, IdleTimeout of type time.Duration)
in conf/configuration.go (with sensible defaults and env/config tags), load them
into the API config, and replace the hardcoded values in the server
initialization (the http.Server literal that sets Addr, Handler a.handler,
ReadHeaderTimeout, ReadTimeout, WriteTimeout, IdleTimeout) to use the config
values instead. Ensure parsing/decoding supports duration strings so operators
can set env vars like GOTRUE_API_WRITE_TIMEOUT=600s.

Comment thread api/api.go
Handler: a.handler,
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 60 * time.Second,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for patterns that indicate potentially long-running operations

echo "=== Searching for database query patterns that might be slow ==="
rg -n --type=go -C2 'db\.(Query|Find|Select)' api/

echo -e "\n=== Searching for external API calls or HTTP clients ==="
rg -n --type=go -C2 'http\.(Get|Post|Do|Client)' api/

echo -e "\n=== Searching for email sending operations ==="
rg -n --type=go -C2 '(SendMail|mailer\.|Email)' api/

echo -e "\n=== Checking export endpoint implementation ==="
rg -n --type=go -A20 'func.*adminExportUsers' api/

echo -e "\n=== Searching for time.Sleep or context.WithTimeout ==="
rg -n --type=go -C2 'time\.(Sleep|After)|context\.WithTimeout' api/

Repository: netlify/gotrue

Length of output: 49225


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== api/api.go: http.Server initialization around timeouts ==="
rg -n "http\.Server|ReadHeaderTimeout|ReadTimeout|WriteTimeout|IdleTimeout" api/api.go
echo
sed -n '1,120p' api/api.go
echo
sed -n '120,240p' api/api.go

echo "=== api/admin.go: adminExportUsers implementation (export endpoint) ==="
rg -n "func \\(a \\*API\\) adminExportUsers|/export|EXPORT_SECRET" api/admin.go
echo
sed -n '1,220p' api/admin.go

echo "=== api/api.go: where server is created/started (ListenAndServe) ==="
rg -n "ListenAndServe|Serve\\(|server\\.Serve\\(|Shutdown\\(|waitForTermination" api/api.go
echo
sed -n '1,120p' api/api.go

Repository: netlify/gotrue

Length of output: 17421


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== paginate() implementation + defaults ==="
rg -n "func paginate\\(" -S api
rg -n "paginate\\(" api/api.go api/admin.go api -S --glob='*.go'

# Print the paginate function
rg -n "func paginate\\(" api -S --glob='*.go' -m 20 | head -n 5
# Use fd/rg to locate file and then show a window
PFILE=$(rg -n "func paginate\\(" api -S --glob='*.go' | head -n1 | cut -d: -f1)
PLINE=$(rg -n "func paginate\\(" api -S --glob='*.go' | head -n1 | cut -d: -f2)
echo "paginate located at $PFILE:$PLINE"
sed -n "${PLINE},$((PLINE+160))p" "$PFILE"

echo
echo "=== sendJSON() implementation ==="
rg -n "func sendJSON\\(" api -S --glob='*.go'
SFILE=$(rg -n "func sendJSON\\(" api -S --glob='*.go' | head -n1 | cut -d: -f1)
SLINE=$(rg -n "func sendJSON\\(" api -S --glob='*.go' | head -n1 | cut -d: -f2)
echo "sendJSON located at $SFILE:$SLINE"
sed -n "${SLINE},$((SLINE+120))p" "$SFILE"

echo
echo "=== FindUsersForExportInAudience() implementation ==="
rg -n "FindUsersForExportInAudience" -S .
EFILE=$(rg -n "func \\(.*\\) FindUsersForExportInAudience|func FindUsersForExportInAudience" -S . --glob='*.go' | head -n1 | cut -d: -f1)
ELINE=$(rg -n "func \\(.*\\) FindUsersForExportInAudience|func FindUsersForExportInAudience" -S . --glob='*.go' | head -n1 | cut -d: -f2)
echo "export query located at $EFILE:$ELINE"
sed -n "${ELINE},$((ELINE+220))p" "$EFILE"

echo
echo "=== Query helper functions used by export query (optional hints) ==="
rg -n "FindUsersForExportInAudience" -S . --glob='*.go' -C2 | head -n 80

Repository: netlify/gotrue

Length of output: 7174


🌐 Web query:

Go net/http http.Server WriteTimeout semantics includes time before first Write/handler execution

💡 Result:

The semantics of WriteTimeout in Go's net/http.Server are generally designed to cover the lifetime of the ServeHTTP handler stack [1][2], but they include nuances depending on the protocol and implementation details. In standard HTTP/1.x operation, WriteTimeout sets a write deadline on the underlying network connection immediately after the request headers have been read [3][2]. Consequently, for non-HTTPS connections, it covers the time from the completion of the header read until the completion of the response write [1][2]. This duration effectively encompasses the time spent executing the handler, including any processing, database calls, or internal operations that occur before the response is written [4]. There are two critical exceptions to this general behavior: 1. HTTPS/TLS Connections: For HTTPS connections, the server calls SetWriteDeadline immediately after the connection is accepted to also cover the TLS handshake [2]. In this specific case, WriteTimeout includes the time spent reading the request headers and waiting for the first byte of the request, in addition to the standard post-header handler execution time [2]. 2. Implementation Nuances: WriteTimeout is an absolute deadline imposed on the connection [4]. It is not a per-write timeout or an idle timeout [4][2]. Because it is implemented by setting a deadline on the connection, if the handler takes longer to process the request than the configured WriteTimeout, the server will time out when it eventually attempts to write to the response [1][4]. Because WriteTimeout is not a per-request handler timeout, it is often recommended to use http.TimeoutHandler or context-based timeouts if you require finer control over request execution time, as relying solely on WriteTimeout can be imprecise and may not trigger until the handler attempts a write [5][4][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "defaultPerPage" api/pagination.go api -S
echo "---- api/pagination.go ----"
sed -n '1,120p' api/pagination.go
echo "---- models/Pagination type (if present) ----"
rg -n "type Pagination" -S models api | head -n 20

Repository: netlify/gotrue

Length of output: 1990


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for per_page validation/caps ==="
rg -n "per_page|PerPage|maxPer|DefaultPer|cap.*per|limit.*per" api models -S --glob='*.go'

echo
echo "=== Inspect UserForExport struct and export-related model code ==="
# find UserForExport definition
ufile=$(rg -n "type UserForExport" -S models --glob='*.go' | head -n1 | cut -d: -f1)
uline=$(rg -n "type UserForExport" -S models --glob='*.go' | head -n1 | cut -d: -f2)
echo "UserForExport at $ufile:$uline"
sed -n "${uline},$((uline+200))p" "$ufile"

echo
echo "=== Inspect models FindUsersForExportInAudience surrounding code ==="
# find the function in models/user.go already known; re-open larger window
u2line=$(rg -n "func FindUsersForExportInAudience" -S models --glob='user.go' | head -n1 | cut -d: -f2)
sed -n "${u2line-40},$((u2line+120))p" "$ufile"

Repository: netlify/gotrue

Length of output: 8416


Make http.Server.WriteTimeout: 60s safe for /admin/users/export (and other paginated admin JSON endpoints)

WriteTimeout acts as a hard deadline for the response write on the connection. This matters here because /admin/users/export builds the full payload in-memory: it queries users, then returns via sendJSON, which json.Marshals the entire []*UserForExport before the first Write. UserForExport includes large fields like app_metadata and user_metadata, so DB + marshal time can exceed 60s.

Also, per_page is client-controlled with no visible upper bound (defaultPerPage = 50), so a single request with a large per_page can create payloads/queries slow enough to hit the 60s deadline. This is also applicable to /admin/users since it uses the same pagination + sendJSON pattern.

Consider:

  • Cap/validate per_page (and possibly page) for these admin list/export endpoints.
  • Make the timeout configurable (or use a context/request-level deadline) instead of a fixed WriteTimeout that can kill long handlers mid-response.
  • For genuinely large exports, switch to streaming/chunked responses or an async/background export job rather than buffering the full JSON.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/api.go` at line 47, The server's fixed http.Server.WriteTimeout = 60 *
time.Second can abort long admin endpoints (e.g., handlers that call sendJSON
which json.Marshal's []*UserForExport) — make the endpoints safe by (1)
validating/capping pagination input (use defaultPerPage and enforce a maxPerPage
constant, validate page/per_page in the admin handlers that produce large
payloads such as /admin/users and /admin/users/export), (2) avoid buffering
entire payloads by replacing the sendJSON call in those handlers with a
streaming/chunked JSON encoder or by implementing an async export job for very
large exports, and (3) remove reliance on the global hard WriteTimeout for these
routes (either make it configurable or rely on request-context deadlines) so
long-running DB + marshal work isn't killed mid-response.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants