Skip to content

fix(dataplane): bound development listener resources - #8

Merged
SShogun merged 2 commits into
mainfrom
codex/m1-static-http-proxy
Oct 10, 2026
Merged

SShogun merged 2 commits into
mainfrom
codex/m1-static-http-proxy

Conversation

@SShogun

@SShogun SShogun commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

Restrict the development listener to loopback and bound request handling, body-read waits, and upstream response headers while preserving streaming. Document the defaults and review evidence.

Summary by Sourcery

Bound development listener resources and document the resulting defaults and verification coverage.

Bug Fixes:

  • Bound development listener resources and request handling to prevent unbounded concurrency and waits while preserving streaming behavior.

Enhancements:

  • Document loopback-only listener requirements, connection and header limits, request-body and upstream-header timeouts, and immediate rejection of excess requests.
  • Expand the documented test coverage and repository component overview for routing, clustering, configuration, dataplane behavior, and verification jobs.

Restrict the development listener to loopback and bound request handling, body-read waits, and upstream response headers while preserving streaming. Document the defaults and review evidence.
Copilot AI balanced review requested due to automatic review settings October 10, 2026 20:09
@sourcery-ai

sourcery-ai Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The PR hardens the development dataplane by enforcing loopback-only listeners, bounding concurrency, request-body idle reads, upstream response-header waits, and header sizes, while deliberately preserving streaming, graceful shutdown, and HTTP upgrade/hijack behavior; documentation and extensive regression/evidence tests define the defaults and scope.

Sequence diagram for bounded request handling and streaming

sequenceDiagram
    participant Client
    participant Server
    participant Limits as ResourceLimitHandler
    participant Proxy
    participant Upstream

    Client->>Server: HTTP request
    Server->>Limits: ServeHTTP
    alt concurrency limit reached
        Limits-->>Client: HTTP 503 server busy
        Limits-->>Client: Connection close
    else handler admitted
        Limits->>Limits: Install 30s request-body read idle timeout
        Limits->>Proxy: ServeHTTP
        Proxy->>Upstream: Forward streamed request
        Upstream-->>Proxy: Response headers within 30s
        Proxy-->>Client: Stream response headers and body
        Limits->>Limits: Release handler slot
    end
Loading

File-Level Changes

Change Details Files
Restrict the unauthenticated development listener to literal loopback endpoints.
  • Validate numeric TCP ports and IPv4/IPv6 loopback addresses.
  • Reject wildcard, non-loopback, hostname, malformed, and out-of-range listener addresses before upstream setup.
  • Document the listener security scope and add command-level rejection coverage.
cmd/gatehouse-data/main.go
cmd/gatehouse-data/main_test.go
internal/dataplane/development_listener.go
internal/dataplane/resource_limits_test.go
README.md
Add bounded admission and request-body handling while preserving streaming and connection ownership.
  • Default and validate a 128-request concurrency cap with immediate 503 overload responses.
  • Apply a 30-second per-read idle timeout to request bodies, including bounded cleanup of rejected or unread bodies.
  • Preserve hijacking and ResponseController behavior, restore deadlines after failed hijacks, and avoid affecting transferred connections.
  • Keep response streaming and verify recovery after cancellation, panic, early responses, and upgrades.
internal/dataplane/server.go
internal/dataplane/resource_limits_test.go
Bound upstream response-header waits and sizes without imposing response-body write timeouts.
  • Configure 30-second ResponseHeaderTimeout and 1 MiB MaxResponseHeaderBytes defaults.
  • Reject negative proxy resource settings and return 502 for timeout or oversized-header failures.
  • Verify delayed response bodies continue streaming after headers arrive.
internal/dataplane/proxy.go
internal/dataplane/resource_limits_test.go
Strengthen protocol, shutdown, and review evidence coverage.
  • Test hop-by-hop header stripping using raw connections and characterize bidirectional HTTP upgrades.
  • Replace timing assumptions with listener-closure and shutdown-deadline assertions.
  • Record defaults, scope boundaries, verification results, review dispositions, and known process/evidence limitations.
internal/dataplane/proxy_test.go
internal/dataplane/server_test.go
docs/Gatehouse_PR5_REVIEW.md
README.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 5 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="internal/dataplane/server.go" line_range="251-252" />
<code_context>
+		return
+	}
+	body.finished = true
+	_ = body.controller.SetReadDeadline(time.Now())
+	body.mu.Unlock()
+}
+
</code_context>
<issue_to_address>
**Full-duplex uploads stop early**

When a handler enables full duplex and writes or flushes before consuming the request body, `requestBodyDeadlineWriter.WriteHeader`, `Write` and `Flush` call `finishHandler`, which marks the body finished and sets its read deadline to now. `idleTimedRequestBody.Read` then returns `http.ErrBodyReadAfterClose` or is interrupted, so the handler cannot finish receiving the upload.

Keep request-body reads available after a full-duplex handler writes or flushes a response.

Also at `internal/dataplane/server.go:268-279`.
</issue_to_address>

### Comment 2
<location path="internal/dataplane/server.go" line_range="302-314" />
<code_context>
+func (body *idleTimedRequestBody) Close() error {
+	body.mu.Lock()
+	var deadlineErr error
+	if body.hijacking || body.hijacked {
+		body.mu.Unlock()
+		return nil
+	}
+	if !body.finished {
+		deadlineErr = body.controller.SetReadDeadline(time.Now().Add(body.timeout))
+	}
+	body.mu.Unlock()
+	closeErr := body.ReadCloser.Close()
+	body.mu.Lock()
+	if !body.finished && !body.hijacking && !body.hijacked {
+		body.clearDeadline()
+	}
+	body.mu.Unlock()
</code_context>
<issue_to_address>
**Hijack disrupts active body reads**

When a handler hijacks while another goroutine is reading the request body, `finishHijack` marks the body hijacked and clears the read deadline while `activeReads` is nonzero, leaving the read unbounded or letting it consume upgraded-protocol bytes and disrupt connection ownership.

Coordinate hijacking with active body reads so no read remains active when the connection is transferred and its deadline is cleared.
</issue_to_address>

### Comment 3
<location path="internal/dataplane/server.go" line_range="152-155" />
<code_context>
+		h.handler.ServeHTTP(w, r)
+		return
+	}
+	wrappedWriter := &requestBodyDeadlineWriter{ResponseWriter: w, body: body}
+	if _, ok := w.(http.Hijacker); ok {
+		h.handler.ServeHTTP(&hijackableRequestBodyDeadlineWriter{requestBodyDeadlineWriter: wrappedWriter}, r)
+		return
+	}
+	h.handler.ServeHTTP(wrappedWriter, r)
</code_context>
<issue_to_address>
**Disconnect notifications disappear**

When a body-bearing HTTP/1 request reaches a handler that relies on `http.CloseNotifier`, `requestBodyDeadlineWriter` does not forward `http.CloseNotifier`, so the handler's assertion fails or it receives no disconnect notification. The handler can keep work running after the client disconnects.

Preserve `http.CloseNotifier` for body-bearing requests when the underlying writer supports it.
</issue_to_address>

### Comment 4
<location path="internal/dataplane/server.go" line_range="145" />
<code_context>
+		} else if r.ProtoMajor == 1 {
+			w.Header().Set("Connection", "close")
+		}
+		http.Error(w, "server busy", http.StatusServiceUnavailable)
+		return
+	}
</code_context>
<issue_to_address>
**Overload rejections are invisible**

When all request-handler slots are occupied, the full-slot branch returns HTTP 503 without logging or recording the rejection, and the request never reaches the proxy’s completion logging, so operators have no built-in signal when load shedding occurs.

Record overload rejections through a logger or metric before returning 503.
</issue_to_address>

### Comment 5
<location path="internal/dataplane/resource_limits_test.go" line_range="131" />
<code_context>
+		result <- err
+	}()
+	for _, part := range []string{"pro", "gre", "ssi", "ve"} {
+		time.Sleep(70 * time.Millisecond)
+		if _, err := io.WriteString(pipeWriter, part); err != nil {
+			t.Fatal(err)
</code_context>
<issue_to_address>
**Progressive upload test flakes**

When the test process or server goroutine is descheduled for more than the remaining 50 ms between chunks, the test sleeps 70 ms between writes against a 120 ms idle timeout, leaving only 50 ms of scheduling margin. The timeout fires on a valid upload, so the test fails intermittently even when the implementation is correct.

Avoid relying on a 50 ms scheduling margin between writes, for example by reducing the delay or increasing the test timeout.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 5 findings to address first, and this changes an unauthenticated listener trust boundary and adds concurrency, body-read, and upstream-header thresholds; if those decisions or implementations are wrong, requests could be exposed, rejected, or timed out before a revert. Reverting restores the previous behavior, but any exposure or outage that already occurred is not undone.

Blocking findings: internal/dataplane/server.go:252, internal/dataplane/server.go:314, internal/dataplane/server.go:155, internal/dataplane/server.go:145, internal/dataplane/resource_limits_test.go:131


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment on lines +251 to +252
_ = body.controller.SetReadDeadline(time.Now())
body.mu.Unlock()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Full-duplex uploads stop early

When a handler enables full duplex and writes or flushes before consuming the request body, requestBodyDeadlineWriter.WriteHeader, Write and Flush call finishHandler, which marks the body finished and sets its read deadline to now. idleTimedRequestBody.Read then returns http.ErrBodyReadAfterClose or is interrupted, so the handler cannot finish receiving the upload.

Keep request-body reads available after a full-duplex handler writes or flushes a response.

Also at internal/dataplane/server.go:268-279.

Prompt for AI agents
In `internal/dataplane/server.go` at lines 251-252:

**Full-duplex uploads stop early**

When a handler enables full duplex and writes or flushes before consuming the request body, `requestBodyDeadlineWriter.WriteHeader`, `Write` and `Flush` call `finishHandler`, which marks the body finished and sets its read deadline to now. `idleTimedRequestBody.Read` then returns `http.ErrBodyReadAfterClose` or is interrupted, so the handler cannot finish receiving the upload.

Keep request-body reads available after a full-duplex handler writes or flushes a response.

Also at `internal/dataplane/server.go:268-279`.

Comment on lines +302 to +314
if body.hijacking || body.hijacked {
return false
}
body.hijacking = true
return true
}

func (body *idleTimedRequestBody) finishHijack(succeeded bool) {
body.mu.Lock()
body.hijacking = false
if succeeded {
body.hijacked = true
body.clearDeadline()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Hijack disrupts active body reads

When a handler hijacks while another goroutine is reading the request body, finishHijack marks the body hijacked and clears the read deadline while activeReads is nonzero, leaving the read unbounded or letting it consume upgraded-protocol bytes and disrupt connection ownership.

Coordinate hijacking with active body reads so no read remains active when the connection is transferred and its deadline is cleared.

Prompt for AI agents
In `internal/dataplane/server.go` at lines 302-314:

**Hijack disrupts active body reads**

When a handler hijacks while another goroutine is reading the request body, `finishHijack` marks the body hijacked and clears the read deadline while `activeReads` is nonzero, leaving the read unbounded or letting it consume upgraded-protocol bytes and disrupt connection ownership.

Coordinate hijacking with active body reads so no read remains active when the connection is transferred and its deadline is cleared.

Comment on lines +152 to +155
wrappedWriter := &requestBodyDeadlineWriter{ResponseWriter: w, body: body}
if _, ok := w.(http.Hijacker); ok {
h.handler.ServeHTTP(&hijackableRequestBodyDeadlineWriter{requestBodyDeadlineWriter: wrappedWriter}, r)
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Disconnect notifications disappear

When a body-bearing HTTP/1 request reaches a handler that relies on http.CloseNotifier, requestBodyDeadlineWriter does not forward http.CloseNotifier, so the handler's assertion fails or it receives no disconnect notification. The handler can keep work running after the client disconnects.

Preserve http.CloseNotifier for body-bearing requests when the underlying writer supports it.

Prompt for AI agents
In `internal/dataplane/server.go` at lines 152-155:

**Disconnect notifications disappear**

When a body-bearing HTTP/1 request reaches a handler that relies on `http.CloseNotifier`, `requestBodyDeadlineWriter` does not forward `http.CloseNotifier`, so the handler's assertion fails or it receives no disconnect notification. The handler can keep work running after the client disconnects.

Preserve `http.CloseNotifier` for body-bearing requests when the underlying writer supports it.

} else if r.ProtoMajor == 1 {
w.Header().Set("Connection", "close")
}
http.Error(w, "server busy", http.StatusServiceUnavailable)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Overload rejections are invisible

When all request-handler slots are occupied, the full-slot branch returns HTTP 503 without logging or recording the rejection, and the request never reaches the proxy’s completion logging, so operators have no built-in signal when load shedding occurs.

Record overload rejections through a logger or metric before returning 503.

Prompt for AI agents
In `internal/dataplane/server.go` at line 145:

**Overload rejections are invisible**

When all request-handler slots are occupied, the full-slot branch returns HTTP 503 without logging or recording the rejection, and the request never reaches the proxy’s completion logging, so operators have no built-in signal when load shedding occurs.

Record overload rejections through a logger or metric before returning 503.

result <- err
}()
for _, part := range []string{"pro", "gre", "ssi", "ve"} {
time.Sleep(70 * time.Millisecond)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Progressive upload test flakes

When the test process or server goroutine is descheduled for more than the remaining 50 ms between chunks, the test sleeps 70 ms between writes against a 120 ms idle timeout, leaving only 50 ms of scheduling margin. The timeout fires on a valid upload, so the test fails intermittently even when the implementation is correct.

Avoid relying on a 50 ms scheduling margin between writes, for example by reducing the delay or increasing the test timeout.

Prompt for AI agents
In `internal/dataplane/resource_limits_test.go` at line 131:

**Progressive upload test flakes**

When the test process or server goroutine is descheduled for more than the remaining 50 ms between chunks, the test sleeps 70 ms between writes against a 120 ms idle timeout, leaving only 50 ms of scheduling margin. The timeout fires on a valid upload, so the test fails intermittently even when the implementation is correct.

Avoid relying on a 50 ms scheduling margin between writes, for example by reducing the delay or increasing the test timeout.

@SShogun
SShogun merged commit 4f120fd into main Oct 10, 2026
6 checks passed
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