Repository navigation
Fix: Fold a redirected bridged CONNECT into its inner request in agentop #1594
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -377,6 +377,96 @@ func TestBuildEventRows_CollapsesBridgedConnect(t *testing.T) { | |
| } | ||
| } | ||
|
|
||
| // TestBuildEventRows_CollapsesBridgedConnectRedirected locks the fold under a | ||
| // plugin redirect: the CONNECT named the host the client asked for, the inner | ||
| // request went elsewhere, and RequestedHost carries the client's host. Without | ||
| // the RequestedHost match arm the open stood as its own blank-method row and | ||
| // the timeline read as if traffic still went to the original server. | ||
| func TestBuildEventRows_CollapsesBridgedConnectRedirected(t *testing.T) { | ||
| events := []pipeline.SessionEvent{ | ||
| // CONNECT tunnel-open to the client's configured inference server. | ||
| { | ||
| Direction: pipeline.Outbound, | ||
| Phase: pipeline.SessionRequest, | ||
| Host: "ete-litellm.example.com:443", | ||
| Tunnel: true, | ||
| }, | ||
| // Inner request the inference-router redirected to another server: | ||
| // Host is the dial target, RequestedHost the host the client named. | ||
| { | ||
| Direction: pipeline.Outbound, | ||
| Phase: pipeline.SessionRequest, | ||
| Host: "litellm.cb.example.com", | ||
| RequestedHost: "ete-litellm.example.com:443", | ||
| Inference: &pipeline.InferenceExtension{Model: "glm-4.6"}, | ||
| }, | ||
| { | ||
| Direction: pipeline.Outbound, | ||
| Phase: pipeline.SessionResponse, | ||
| Host: "litellm.cb.example.com", | ||
| Inference: &pipeline.InferenceExtension{Model: "glm-4.6", TotalTokens: 500}, | ||
| StatusCode: 200, | ||
| }, | ||
| } | ||
| rows := buildEventRows(events) | ||
| if len(rows) != 2 { | ||
| t.Fatalf("redirected bridged call produced %d rows, want 2 (collapsed req + resp)", len(rows)) | ||
| } | ||
| if rows[0].event.Host != "litellm.cb.example.com" { | ||
| t.Errorf("collapsed row host = %q, want the redirected host", rows[0].event.Host) | ||
| } | ||
| if rows[0].tunnel == nil { | ||
| t.Fatalf("row 0 should carry the folded CONNECT tunnel") | ||
| } | ||
| if rows[0].tunnel.Host != "ete-litellm.example.com:443" { | ||
| t.Errorf("folded tunnel host = %q, want the client's original server", rows[0].tunnel.Host) | ||
| } | ||
| } | ||
|
|
||
| // TestBuildEventRows_CollapsesBridgedConnectDenied locks the fold when the | ||
| // tunnel's first decrypted row is a DENIAL, not a request: the proxy defers a | ||
| // bridged open and records it with whichever row comes first, and a pipeline | ||
| // plugin's Reject — recorded before any request row — can be that first row. | ||
| // Each subtest covers one match arm: unredirected denial (Host) and redirected | ||
| // denial (RequestedHost). | ||
| func TestBuildEventRows_CollapsesBridgedConnectDenied(t *testing.T) { | ||
| for name, inner := range map[string]pipeline.SessionEvent{ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: The loop is over a map without |
||
| "unredirected": { | ||
| Direction: pipeline.Outbound, | ||
| Phase: pipeline.SessionDenied, | ||
| Host: "ete-litellm.example.com", | ||
| StatusCode: 403, | ||
| }, | ||
| "redirected": { | ||
| Direction: pipeline.Outbound, | ||
| Phase: pipeline.SessionDenied, | ||
| Host: "litellm.cb.example.com", | ||
| RequestedHost: "ete-litellm.example.com:443", | ||
| StatusCode: 403, | ||
| }, | ||
| } { | ||
| events := []pipeline.SessionEvent{ | ||
| { | ||
| Direction: pipeline.Outbound, | ||
| Phase: pipeline.SessionRequest, | ||
| Host: "ete-litellm.example.com:443", | ||
| Tunnel: true, | ||
| }, | ||
| inner, | ||
| } | ||
| rows := buildEventRows(events) | ||
| if len(rows) != 1 { | ||
| t.Fatalf("%s: denied bridged call produced %d rows, want 1 (the denial carrying the tunnel)", name, len(rows)) | ||
| } | ||
| if rows[0].tunnel == nil { | ||
| t.Fatalf("%s: denial row should carry the folded CONNECT tunnel", name) | ||
| } | ||
| if rows[0].event.Phase != pipeline.SessionDenied { | ||
| t.Errorf("%s: folded row phase = %q, want the denial kept", name, rows[0].event.Phase) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // TestBuildEventRows_PassthroughTunnelStandsAlone covers every shape in which a | ||
| // non-bridged CONNECT tunnel-open must remain its own row rather than fold: | ||
| // a lone trailing CONNECT, two back-to-back CONNECTs to the SAME host | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: The new arm has no negative test. Consider adding a case to
TestBuildEventRows_PassthroughTunnelStandsAlone: a CONNECT toa.example:443followed by a redirected request withHost: b.exampleandRequestedHost: c.example:443. It should not fold. Today theRequestedHost != ""/ host-equality pairing is only tested for the positive case, so a later change that relaxes this arm (for example, dropping the equality) would still pass every test.A minor point on parity: the server-side
HostKeylowercases the host and strips only:443, whilehostOnlyis case-sensitive and strips any port. This was already true before this PR, so it isn't blocking. It does mean a mixed-case CONNECT target could fold on the server and not in agentop. Anstrings.EqualFoldhere would close that gap if you want it.