Skip to content

moves sse hearbeats to a common structure to reuse - #5850

Merged
akshaydeo merged 1 commit into
mainfrom
08-04-moves_sse_hearbeats_to_a_common_structure_to_reuse
Aug 5, 2026
Merged

moves sse hearbeats to a common structure to reuse#5850
akshaydeo merged 1 commit into
mainfrom
08-04-moves_sse_hearbeats_to_a_common_structure_to_reuse

Conversation

@akshaydeo

@akshaydeo akshaydeo commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Client-disconnect detection in streaming paths was purely reactive: cancel() only fired when a downstream write actually failed, which only happened when the producer loop attempted a write. Fast or bursty upstream providers (notably Vertex's streamGenerateContent, which delivers fewer, larger deltas than direct Gemini) could finish an entire stream before the write-failure detector ever got a second chance to fire, causing disconnected clients to be logged as false successes.

This PR extracts the SSE heartbeat goroutine from handleStreamingResponse into a reusable lib.StartSSEHeartbeat / lib.StopSSEHeartbeat pair, and wires it into handleStreaming (both SSE and Bedrock branches) and handlePassthroughStream (SSE content-types only).

Closes #5010

Changes

  • lib/streamreader.go: Added StartSSEHeartbeat, StopSSEHeartbeat, and DefaultSSEHeartbeatInterval. The heartbeat goroutine calls a caller-supplied send function on each tick; if send returns false (reader closed, i.e. disconnect discovered), it calls onDisconnect once and exits. StopSSEHeartbeat enforces the correct shutdown order — close(done)reader.Close()<-exited — before the caller calls reader.Done(), preventing a "send on closed channel" panic.
  • handlers/inference.go: Replaced the inline heartbeat goroutine in handleStreamingResponse with calls to StartSSEHeartbeat / StopSSEHeartbeat.
  • integrations/router.go:
    • handleStreaming: Added heartbeat coverage for both the SSE and Bedrock branches. The Bedrock branch uses a dedicated *eventstream.Encoder for the heartbeat goroutine (separate from the producer loop's encoder) because eventstream.Encoder reuses internal scratch buffers and is not safe for concurrent use. The heartbeat frame uses a synthetic :event-type (bifrostHeartbeat) that botocore's event-stream parser silently drops, making it the EventStream-binary equivalent of an SSE comment.
    • handlePassthroughStream: Heartbeat is only started when the resolved content-type is text/event-stream. Injecting SSE comment bytes into a non-SSE passthrough body (e.g. Vertex/Gemini's raw incrementally-delivered JSON array) would corrupt framing this path doesn't control.
    • Added passthroughHeartbeatEligible and sendBedrockEventStreamHeartbeat helpers.
  • lib/streamreader_test.go: Tests for StartSSEHeartbeat firing periodically and calling onDisconnect exactly once on a closed reader.
  • integrations/router_heartbeat_test.go: Tests confirming the SSE and Bedrock branches of handleStreaming emit heartbeat frames during idle gaps, that bedrockHeartbeatEventType never collides with real BedrockStreamEvent.ToEncodedEvents() event types, that concurrent use of separate encoders is race-detector clean, and that passthroughHeartbeatEligible gates correctly on content-type.

Type of change

  • Bug fix
  • Refactor

Affected areas

  • Transports (HTTP)
  • Providers/Integrations

How to test

go test ./transports/bifrost-http/lib/... ./transports/bifrost-http/integrations/... -race

The router_heartbeat_test.go tests use real timers to confirm heartbeat frames are emitted during idle gaps. Run with -race to validate the Bedrock encoder-isolation test (Test_bedrockEventStreamHeartbeatUsesSeparateEncoderSafely), which is specifically designed to be caught by the race detector if a single encoder were shared between the producer and heartbeat goroutines.

Breaking changes

  • No

Security considerations

None. The heartbeat frames are no-op probes (SSE comment lines or unrecognized EventStream event-types) that are invisible to conforming clients.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3c474591-f08a-4173-99d1-258c73432f95

📥 Commits

Reviewing files that changed from the base of the PR and between 1bad357 and 8bb3154.

📒 Files selected for processing (9)
  • core/utils.go
  • core/utils_test.go
  • transports/bifrost-http/handlers/inference.go
  • transports/bifrost-http/handlers/skills_serving.go
  • transports/bifrost-http/integrations/router.go
  • transports/bifrost-http/integrations/router_heartbeat_test.go
  • transports/bifrost-http/lib/streamreader.go
  • transports/bifrost-http/lib/streamreader_test.go
  • transports/config.schema.json
🚧 Files skipped from review as they are similar to previous changes (9)
  • core/utils.go
  • transports/bifrost-http/integrations/router.go
  • core/utils_test.go
  • transports/bifrost-http/lib/streamreader_test.go
  • transports/bifrost-http/handlers/skills_serving.go
  • transports/bifrost-http/integrations/router_heartbeat_test.go
  • transports/bifrost-http/handlers/inference.go
  • transports/config.schema.json
  • transports/bifrost-http/lib/streamreader.go

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added periodic heartbeats to SSE streaming responses to detect disconnected clients during idle periods.
    • Limited passthrough heartbeats to responses using exact SSE content types.
    • Added a read-only virtual key count to governance customer configuration.
  • Bug Fixes

    • Improved stream shutdown and cleanup after client disconnection.
    • Added bounded timeouts for external URL validation and skill lookups.
    • Preserved non-SSE and Bedrock streaming behavior.
  • Tests

    • Added coverage for heartbeat delivery, disconnect detection, cleanup, and timeout handling.

Walkthrough

The change centralizes SSE heartbeat management, adds heartbeats to eligible streaming routes, bounds DNS and skill lookups with timeouts, and adds a read-only virtual key count schema field.

Changes

SSE heartbeat and disconnect handling

Layer / File(s) Summary
Shared heartbeat lifecycle
transports/bifrost-http/lib/streamreader.go, transports/bifrost-http/lib/streamreader_test.go
Adds periodic heartbeat delivery, disconnect callbacks, synchronized shutdown, and reader cleanup. Tests cover repeated delivery, reader closure, and exactly-once callbacks.
Streaming route heartbeat integration
transports/bifrost-http/integrations/router.go, transports/bifrost-http/integrations/router_heartbeat_test.go
Adds heartbeats to non-Bedrock streaming and exact SSE passthrough responses. Tests cover idle heartbeats and content-type eligibility.
Inference handler migration
transports/bifrost-http/handlers/inference.go
Replaces local heartbeat goroutine and shutdown logic with shared helpers.

Bounded external operations

Layer / File(s) Summary
Bounded DNS validation
core/utils.go, core/utils_test.go
Uses a five-second context-bounded DNS lookup before applying existing IP validation. Tests verify bounded resolution and cancellation.
Bounded skill lookup
transports/bifrost-http/handlers/skills_serving.go
Uses a ten-second background-derived context for skill retrieval.

Customer configuration schema

Layer / File(s) Summary
Virtual key count field
transports/config.schema.json
Adds a non-negative, read-only integer virtual_key_count property.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant StreamingRoute
  participant SSEHeartbeat
  participant StreamReader
  participant Client
  StreamingRoute->>SSEHeartbeat: StartSSEHeartbeat
  SSEHeartbeat->>StreamReader: Send heartbeat
  StreamReader->>Client: Write SSE heartbeat
  Client-->>StreamReader: Accept or disconnect
  StreamingRoute->>SSEHeartbeat: StopSSEHeartbeat
  SSEHeartbeat->>StreamReader: Close and await exit
Loading

Possibly related PRs

  • maximhq/bifrost#5836: Evolves the same SSE heartbeat and cancellation logic in inference.go and streamreader.go.
  • maximhq/bifrost#5837: Contains related heartbeat shutdown coordination in inference streaming.

Suggested reviewers: roroghost17, tejasghatte

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes extracting SSE heartbeats for reuse, despite a minor spelling error and awkward phrasing.
Description check ✅ Passed The description covers the purpose, changes, testing, affected areas, issue reference, security impact, and checklist.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.
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.
✨ 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 08-04-moves_sse_hearbeats_to_a_common_structure_to_reuse

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)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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

@akshaydeo
akshaydeo marked this pull request as ready for review August 4, 2026 22:39

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 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 `@transports/bifrost-http/integrations/router.go`:
- Around line 3365-3373: Update passthroughHeartbeatEligible to parse or split
the Content-Type at parameters and compare the complete media type
case-insensitively against text/event-stream, rejecting prefix-collision values
such as text/event-stream+json and text/event-streaming. Extend
Test_passthroughHeartbeatEligible with uppercase, parameterized, and
prefix-collision cases.
- Around line 3106-3150: Remove the synthetic Bedrock heartbeat implementation,
including bedrockHeartbeatEventType and sendBedrockEventStreamHeartbeat, and
remove all call sites that emit this event. Preserve the normal Bedrock
EventStream frames and avoid sending any unknown event type that AWS SDK for Go
v2 exposes as types.UnknownUnionMember.

In `@transports/bifrost-http/lib/streamreader_test.go`:
- Around line 843-852: Replace fixed sleeps with channel-based delivery
synchronization: in transports/bifrost-http/lib/streamreader_test.go lines
843-852, wait for two observed heartbeat frames before calling StopSSEHeartbeat,
using a timeout only as the failure bound; in
transports/bifrost-http/integrations/router_heartbeat_test.go lines 41-46, wait
for the reader to receive an SSE comment before closing stream; and at lines
65-74, wait for a complete Bedrock EventStream frame before closing stream.

In `@transports/bifrost-http/lib/streamreader.go`:
- Around line 135-140: Increase DefaultSSEHeartbeatInterval from 100
milliseconds to a less frequent default such as one second to reduce unnecessary
writes across idle inference, routed streaming, and SSE passthrough streams.
Keep any shorter heartbeat intervals limited to focused lifecycle tests rather
than changing transport configuration.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 00cd5e83-a131-4559-ae59-e7d71ca48106

📥 Commits

Reviewing files that changed from the base of the PR and between 1bad357 and 24699bf.

📒 Files selected for processing (5)
  • transports/bifrost-http/handlers/inference.go
  • transports/bifrost-http/integrations/router.go
  • transports/bifrost-http/integrations/router_heartbeat_test.go
  • transports/bifrost-http/lib/streamreader.go
  • transports/bifrost-http/lib/streamreader_test.go

Comment thread transports/bifrost-http/integrations/router.go Outdated
Comment thread transports/bifrost-http/integrations/router.go
Comment thread transports/bifrost-http/lib/streamreader_test.go Outdated
Comment thread transports/bifrost-http/lib/streamreader.go Outdated
@akshaydeo
akshaydeo force-pushed the 08-04-moves_sse_hearbeats_to_a_common_structure_to_reuse branch from 24699bf to f52cd43 Compare August 5, 2026 00:46
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
transports/bifrost-http/handlers/skills_serving.go (1)

1324-1348: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a regression test for the detached lookup context.

In transports/bifrost-http/handlers/skills_serving_test.go, add a table-driven test that records the context passed to GetSkillByName and asserts a deadline near lookupSkillByPathParamTimeout. This prevents reintroducing *fasthttp.RequestCtx into the database lookup.

🤖 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 `@transports/bifrost-http/handlers/skills_serving.go` around lines 1324 - 1348,
Add a table-driven regression test in the skills serving handler tests that
invokes lookupSkillByPathParam with a mock store recording the context passed to
GetSkillByName, then assert the context has a deadline approximately
lookupSkillByPathParamTimeout from invocation and is not the request context.
Cover the successful lookup path while preserving existing handler behavior.

Source: Coding guidelines

transports/bifrost-http/integrations/router_heartbeat_test.go (1)

61-80: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add route-level passthrough stream coverage.

Test_passthroughHeartbeatEligible tests only the predicate. No test invokes handlePassthroughStream. Add coverage for SSE and non-SSE responses and assert the proxied response body. This catches call-site regressions that inject heartbeats into non-SSE bodies or omit them from SSE.

🤖 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 `@transports/bifrost-http/integrations/router_heartbeat_test.go` around lines
61 - 80, Add route-level tests that invoke handlePassthroughStream for both SSE
and non-SSE responses, asserting the proxied response body remains unchanged for
non-SSE content and receives the expected heartbeat behavior for SSE content.
Keep Test_passthroughHeartbeatEligible focused on predicate cases and use the
existing router/test helpers and response setup.
core/utils.go (2)

657-664: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use net.DefaultResolver for validation.

Use net.DefaultResolver.LookupIPAddr so validation and ConfigureDialer honor the same resolver configuration.

🤖 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 `@core/utils.go` around lines 657 - 664, Update the hostname validation lookup
to call net.DefaultResolver.LookupIPAddr instead of constructing a new
net.Resolver, while preserving the existing lookup context, error wrapping, and
IP extraction behavior.

655-656: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift

Propagate caller cancellation into DNS validation.

ValidateExternalURL ignores existing request contexts and can delay cancellation for up to five seconds. Add a context-aware helper that derives context.WithTimeout from the caller context, and pass available contexts from handlers and framework loaders. Keep a context-free wrapper for background callers.

🤖 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 `@core/utils.go` around lines 655 - 656, Update ValidateExternalURL and its DNS
lookup path to accept and propagate a caller context when creating the timeout,
adding a context-aware helper while retaining a context-free wrapper for
background callers. Update available call sites in handlers and framework
loaders to pass their request or loader contexts, using the existing
context-aware symbol throughout.

Source: Coding guidelines

🤖 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 `@core/utils_test.go`:
- Around line 206-225: Update TestValidateExternalURLBoundsDNSLookup and the
ValidateExternalURL DNS-resolution path to use an injectable resolver seam. Add
a fake resolver that blocks until its context is canceled, then assert the
active lookup receives cancellation before the configured timeout; retain the
.invalid hostname test for immediate NXDOMAIN behavior and keep expired-context
coverage separate.

In `@transports/bifrost-http/integrations/router_heartbeat_test.go`:
- Around line 39-51: The goroutine in the heartbeat test discards the error
returned by io.ReadAll, allowing partial data to produce a false pass. Update
the readDone result and the goroutine around io.ReadAll to preserve both body
bytes and the read error, then assert the error is nil before checking for the
heartbeat frame.

---

Nitpick comments:
In `@core/utils.go`:
- Around line 657-664: Update the hostname validation lookup to call
net.DefaultResolver.LookupIPAddr instead of constructing a new net.Resolver,
while preserving the existing lookup context, error wrapping, and IP extraction
behavior.
- Around line 655-656: Update ValidateExternalURL and its DNS lookup path to
accept and propagate a caller context when creating the timeout, adding a
context-aware helper while retaining a context-free wrapper for background
callers. Update available call sites in handlers and framework loaders to pass
their request or loader contexts, using the existing context-aware symbol
throughout.

In `@transports/bifrost-http/handlers/skills_serving.go`:
- Around line 1324-1348: Add a table-driven regression test in the skills
serving handler tests that invokes lookupSkillByPathParam with a mock store
recording the context passed to GetSkillByName, then assert the context has a
deadline approximately lookupSkillByPathParamTimeout from invocation and is not
the request context. Cover the successful lookup path while preserving existing
handler behavior.

In `@transports/bifrost-http/integrations/router_heartbeat_test.go`:
- Around line 61-80: Add route-level tests that invoke handlePassthroughStream
for both SSE and non-SSE responses, asserting the proxied response body remains
unchanged for non-SSE content and receives the expected heartbeat behavior for
SSE content. Keep Test_passthroughHeartbeatEligible focused on predicate cases
and use the existing router/test helpers and response setup.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 555cd487-8e3e-47bc-986b-30b341041729

📥 Commits

Reviewing files that changed from the base of the PR and between 1bad357 and f52cd43.

📒 Files selected for processing (9)
  • core/utils.go
  • core/utils_test.go
  • transports/bifrost-http/handlers/inference.go
  • transports/bifrost-http/handlers/skills_serving.go
  • transports/bifrost-http/integrations/router.go
  • transports/bifrost-http/integrations/router_heartbeat_test.go
  • transports/bifrost-http/lib/streamreader.go
  • transports/bifrost-http/lib/streamreader_test.go
  • transports/config.schema.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • transports/bifrost-http/lib/streamreader.go
  • transports/bifrost-http/handlers/inference.go
  • transports/bifrost-http/lib/streamreader_test.go
  • transports/bifrost-http/integrations/router.go

Comment thread core/utils_test.go
Comment thread transports/bifrost-http/integrations/router_heartbeat_test.go Outdated
@akshaydeo
akshaydeo force-pushed the 08-04-moves_sse_hearbeats_to_a_common_structure_to_reuse branch from f52cd43 to 8bb3154 Compare August 5, 2026 01:15
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

akshaydeo commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Merge activity

  • Aug 5, 1:39 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Aug 5, 1:39 AM UTC: @akshaydeo merged this pull request with Graphite.

@akshaydeo
akshaydeo merged commit d35f224 into main Aug 5, 2026
15 checks passed
@akshaydeo
akshaydeo deleted the 08-04-moves_sse_hearbeats_to_a_common_structure_to_reuse branch August 5, 2026 01:39
atharvamhaske pushed a commit to atharvamhaske/bifrost that referenced this pull request Aug 13, 2026
## Summary

Client-disconnect detection in streaming paths was purely reactive: `cancel()` only fired when a downstream write actually failed, which only happened when the producer loop attempted a write. Fast or bursty upstream providers (notably Vertex's `streamGenerateContent`, which delivers fewer, larger deltas than direct Gemini) could finish an entire stream before the write-failure detector ever got a second chance to fire, causing disconnected clients to be logged as false successes.

This PR extracts the SSE heartbeat goroutine from `handleStreamingResponse` into a reusable `lib.StartSSEHeartbeat` / `lib.StopSSEHeartbeat` pair, and wires it into `handleStreaming` (both SSE and Bedrock branches) and `handlePassthroughStream` (SSE content-types only).



Closes maximhq#5010

## Changes

- **`lib/streamreader.go`**: Added `StartSSEHeartbeat`, `StopSSEHeartbeat`, and `DefaultSSEHeartbeatInterval`. The heartbeat goroutine calls a caller-supplied `send` function on each tick; if `send` returns false (reader closed, i.e. disconnect discovered), it calls `onDisconnect` once and exits. `StopSSEHeartbeat` enforces the correct shutdown order — `close(done)` → `reader.Close()` → `<-exited` — before the caller calls `reader.Done()`, preventing a "send on closed channel" panic.
- **`handlers/inference.go`**: Replaced the inline heartbeat goroutine in `handleStreamingResponse` with calls to `StartSSEHeartbeat` / `StopSSEHeartbeat`.
- **`integrations/router.go`**:
    - `handleStreaming`: Added heartbeat coverage for both the SSE and Bedrock branches. The Bedrock branch uses a dedicated `*eventstream.Encoder` for the heartbeat goroutine (separate from the producer loop's encoder) because `eventstream.Encoder` reuses internal scratch buffers and is not safe for concurrent use. The heartbeat frame uses a synthetic `:event-type` (`bifrostHeartbeat`) that botocore's event-stream parser silently drops, making it the EventStream-binary equivalent of an SSE comment.
    - `handlePassthroughStream`: Heartbeat is only started when the resolved content-type is `text/event-stream`. Injecting SSE comment bytes into a non-SSE passthrough body (e.g. Vertex/Gemini's raw incrementally-delivered JSON array) would corrupt framing this path doesn't control.
    - Added `passthroughHeartbeatEligible` and `sendBedrockEventStreamHeartbeat` helpers.
- **`lib/streamreader_test.go`**: Tests for `StartSSEHeartbeat` firing periodically and calling `onDisconnect` exactly once on a closed reader.
- **`integrations/router_heartbeat_test.go`**: Tests confirming the SSE and Bedrock branches of `handleStreaming` emit heartbeat frames during idle gaps, that `bedrockHeartbeatEventType` never collides with real `BedrockStreamEvent.ToEncodedEvents()` event types, that concurrent use of separate encoders is race-detector clean, and that `passthroughHeartbeatEligible` gates correctly on content-type.

## Type of change

- [x] Bug fix
- [x] Refactor

## Affected areas

- [x] Transports (HTTP)
- [x] Providers/Integrations

## How to test

```sh
go test ./transports/bifrost-http/lib/... ./transports/bifrost-http/integrations/... -race
```

The `router_heartbeat_test.go` tests use real timers to confirm heartbeat frames are emitted during idle gaps. Run with `-race` to validate the Bedrock encoder-isolation test (`Test_bedrockEventStreamHeartbeatUsesSeparateEncoderSafely`), which is specifically designed to be caught by the race detector if a single encoder were shared between the producer and heartbeat goroutines.

## Breaking changes

- [x] No

## Security considerations

None. The heartbeat frames are no-op probes (SSE comment lines or unrecognized EventStream event-types) that are invisible to conforming clients.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
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.

1 participant