feat: replace WebSocket push channel with SSE#94
Conversation
…ant and stale esbuild refs
Greptile SummaryThis PR replaces the WebSocket push channel with Server-Sent Events (SSE), eliminating the custom Key observations:
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Agent as Go Agent
participant Config as /api/agent/config
participant Push as /api/agent/push
participant Registry as PushRegistry
participant Callers as deploy/fleet/pipeline
Agent->>Config: GET (Bearer token)
Config-->>Agent: { pushUrl, pipelines, ... }
Agent->>Push: GET (Bearer token, long-lived)
Push->>Registry: register(nodeId, controller)
Push-->>Agent: SSE stream open (": connected")
loop every 30s
Registry->>Push: enqueue(": keepalive")
Push-->>Agent: ": keepalive"
end
Callers->>Registry: send(nodeId, { type: "config_changed", ... })
Registry->>Push: controller.enqueue(event)
Push-->>Agent: event: config_changed\ndata: {...}
Agent->>Config: GET (re-poll triggered)
Config-->>Agent: updated pipeline config
Note over Agent,Push: On disconnect
Agent--xPush: TCP close
Push->>Registry: unregister(nodeId, controller)
Note over Agent,Push: On reconnect
Agent->>Push: GET (new connection)
Push->>Registry: register(nodeId, newController)
Registry->>Push: close(oldController)
Last reviewed commit: dd82ba4 |
| return new Response(stream, { | ||
| headers: { | ||
| "Content-Type": "text/event-stream", | ||
| "Cache-Control": "no-cache, no-transform", | ||
| Connection: "keep-alive", | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Missing X-Accel-Buffering: no header
Nginx buffers upstream responses by default. Without the X-Accel-Buffering: no response header, SSE events will be held in Nginx's buffer and not forwarded to the agent until the buffer is flushed or the connection closes — effectively breaking real-time push.
The docs PR updates the Nginx config with proxy_buffering off, but that requires every operator to update their reverse-proxy config. Setting X-Accel-Buffering: no in the response headers makes SSE work correctly out-of-the-box regardless of the proxy config.
| return new Response(stream, { | |
| headers: { | |
| "Content-Type": "text/event-stream", | |
| "Cache-Control": "no-cache, no-transform", | |
| Connection: "keep-alive", | |
| }, | |
| }); | |
| return new Response(stream, { | |
| headers: { | |
| "Content-Type": "text/event-stream", | |
| "Cache-Control": "no-cache, no-transform", | |
| Connection: "keep-alive", | |
| "X-Accel-Buffering": "no", | |
| }, | |
| }); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/api/agent/push/route.ts
Line: 36-42
Comment:
**Missing `X-Accel-Buffering: no` header**
Nginx buffers upstream responses by default. Without the `X-Accel-Buffering: no` response header, SSE events will be held in Nginx's buffer and not forwarded to the agent until the buffer is flushed or the connection closes — effectively breaking real-time push.
The docs PR updates the Nginx config with `proxy_buffering off`, but that requires every operator to update their reverse-proxy config. Setting `X-Accel-Buffering: no` in the response headers makes SSE work correctly out-of-the-box regardless of the proxy config.
```suggestion
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
"X-Accel-Buffering": "no",
},
});
```
How can I resolve this? If you propose a fix, please make it concise.
agent/internal/push/client.go
Outdated
| } | ||
| req.Header.Set("Authorization", "Bearer "+c.token) | ||
|
|
||
| resp, err := http.DefaultClient.Do(req) |
There was a problem hiding this comment.
No connect timeout on the HTTP client
The old WebSocket client used HandshakeTimeout: 10 * time.Second. http.DefaultClient has no timeout, so if the server accepts the TCP connection but never sends an HTTP response (partial outage, overloaded proxy), this goroutine will block indefinitely on Do(req) until Close() is called.
Consider using a custom client with a Timeout for the initial response headers, or at least a ResponseHeaderTimeout:
var sseHttpClient = &http.Client{
Transport: &http.Transport{
ResponseHeaderTimeout: 15 * time.Second,
},
}Then replace http.DefaultClient.Do(req) with sseHttpClient.Do(req). Note: the body-read timeout should remain context-driven (so Close() can cancel a live connection), which is already handled correctly by the context cancellation.
Prompt To Fix With AI
This is a comment left during a code review.
Path: agent/internal/push/client.go
Line: 82
Comment:
**No connect timeout on the HTTP client**
The old WebSocket client used `HandshakeTimeout: 10 * time.Second`. `http.DefaultClient` has no timeout, so if the server accepts the TCP connection but never sends an HTTP response (partial outage, overloaded proxy), this goroutine will block indefinitely on `Do(req)` until `Close()` is called.
Consider using a custom client with a `Timeout` for the initial response headers, or at least a `ResponseHeaderTimeout`:
```go
var sseHttpClient = &http.Client{
Transport: &http.Transport{
ResponseHeaderTimeout: 15 * time.Second,
},
}
```
Then replace `http.DefaultClient.Do(req)` with `sseHttpClient.Do(req)`. Note: the body-read timeout should remain context-driven (so `Close()` can cancel a live connection), which is already handled correctly by the context cancellation.
How can I resolve this? If you propose a fix, please make it concise.
Summary
/api/agent/push, in-memoryPushRegistrywith keepalive, and Go SSE client with exponential backoff reconnectionfleet.ts,deploy.ts,pipeline.ts,deploy-agent.ts) fromwsRegistry→pushRegistry; update config endpoint to returnpushUrlinstead ofwebsocketUrlserver.tscustom server wrapper,ws-registry.ts,ws-auth.ts,ws-types.ts,agent/internal/ws/package, gorilla/websocket dependencyws,@types/ws,esbuild,tsx; revert dev script tonext dev; remove esbuild bundling step from DockerfileNet result: -1,072 lines removed, +535 added (27 files changed). Eliminates the custom server wrapper, esbuild bundling, and gorilla/websocket dependency. SSE works through standard Next.js route handlers with no special infrastructure.
Test Plan
tsc --noEmit)pnpm lint)go build ./...)pnpm build)/api/agent/wsroute directory