docs: align architecture and API docs with Go 1.27 ingress support

- document HTTP/1.1, HTTP/2, and HTTP/3 ingress
- describe SSE streaming and timeout behavior
- document HTTP/2 and HTTP/3 Extended CONNECT
- update yamux stream and deployment architecture
- document Go 1.27 and UDP HTTP/3 requirements
This commit is contained in:
dalbodeule
2026-09-04 16:58:14 +09:00
parent fe1018469d
commit ebd8463c19
11 changed files with 668 additions and 148 deletions
+130
View File
@@ -0,0 +1,130 @@
package main
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/dalbodeule/hop-gate/internal/logging"
)
type noopLogger struct{}
func (noopLogger) Debug(string, logging.Fields) {}
func (noopLogger) Info(string, logging.Fields) {}
func (noopLogger) Warn(string, logging.Fields) {}
func (noopLogger) Error(string, logging.Fields) {}
func (l noopLogger) With(logging.Fields) logging.Logger { return l }
type streamingTestTunnel struct {
forwardHTTPCalled bool
extendedConnectCalled bool
deadlineSeen bool
}
func (t *streamingTestTunnel) ForwardHTTP(ctx context.Context, _ logging.Logger, _ *http.Request, _ string, w http.ResponseWriter) error {
t.forwardHTTPCalled = true
_, t.deadlineSeen = ctx.Deadline()
w.Header().Set("Content-Type", "text/event-stream")
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte("data: ready\n\n"))
return err
}
func (t *streamingTestTunnel) ForwardExtendedConnect(_ context.Context, _ logging.Logger, _ *http.Request, _ string, w http.ResponseWriter) error {
t.extendedConnectCalled = true
w.WriteHeader(http.StatusOK)
return nil
}
func TestSSERequestStreamsWithoutProxyTimeout(t *testing.T) {
tunnel := &streamingTestTunnel{}
logger := noopLogger{}
domain := "sse-test.example"
registerTunnelForDomain(domain, tunnel, logger)
defer unregisterTunnelForDomain(domain, tunnel, logger)
req := httptest.NewRequest(http.MethodGet, "http://"+domain+"/events", nil)
req.Host = domain
req.Header.Set("Accept", "text/event-stream")
recorder := httptest.NewRecorder()
newHTTPHandler(logger, time.Nanosecond).ServeHTTP(recorder, req)
if !tunnel.forwardHTTPCalled {
t.Fatal("expected SSE request to use HTTP forwarder")
}
if tunnel.deadlineSeen {
t.Fatal("expected SSE request to avoid the normal proxy timeout")
}
if got := recorder.Header().Get("Content-Type"); got != "text/event-stream" {
t.Fatalf("Content-Type = %q, want text/event-stream", got)
}
if got := recorder.Body.String(); got != "data: ready\n\n" {
t.Fatalf("body = %q, want SSE event", got)
}
}
func TestHTTP2ExtendedConnectUsesDedicatedForwarder(t *testing.T) {
tunnel := &streamingTestTunnel{}
logger := noopLogger{}
domain := "h2-connect-test.example"
registerTunnelForDomain(domain, tunnel, logger)
defer unregisterTunnelForDomain(domain, tunnel, logger)
req := httptest.NewRequest(http.MethodConnect, "https://"+domain+"/socket", nil)
req.Host = domain
req.ProtoMajor = 2
req.ProtoMinor = 0
req.Proto = "websocket"
recorder := httptest.NewRecorder()
newHTTPHandler(logger, time.Second).ServeHTTP(recorder, req)
if !tunnel.extendedConnectCalled {
t.Fatal("expected HTTP/2 Extended CONNECT forwarder to be called")
}
if tunnel.forwardHTTPCalled {
t.Fatal("did not expect regular HTTP forwarder for Extended CONNECT")
}
if got := recorder.Code; got != http.StatusOK {
t.Fatalf("status = %d, want %d", got, http.StatusOK)
}
}
func TestHTTP3ExtendedConnectDetection(t *testing.T) {
req := httptest.NewRequest(http.MethodConnect, "https://h3.example/socket", nil)
req.ProtoMajor = 3
req.ProtoMinor = 0
req.Proto = "websocket"
if !isExtendedConnectWebSocketRequest(req) {
t.Fatal("expected HTTP/3 Extended CONNECT WebSocket request to be detected")
}
}
func TestHTTP3SSERequestUsesStreamingPolicy(t *testing.T) {
tunnel := &streamingTestTunnel{}
logger := noopLogger{}
domain := "h3-sse-test.example"
registerTunnelForDomain(domain, tunnel, logger)
defer unregisterTunnelForDomain(domain, tunnel, logger)
req := httptest.NewRequest(http.MethodGet, "https://"+domain+"/events", nil)
req.Host = domain
req.ProtoMajor = 3
req.ProtoMinor = 0
req.Header.Set("Accept", "text/event-stream")
recorder := httptest.NewRecorder()
newHTTPHandler(logger, time.Nanosecond).ServeHTTP(recorder, req)
if tunnel.deadlineSeen {
t.Fatal("expected HTTP/3 SSE request to avoid the normal proxy timeout")
}
if got := recorder.Body.String(); got != "data: ready\n\n" {
t.Fatalf("body = %q, want SSE event", got)
}
}