Skip to content

fix(outbound): surface gRPC and VMess rejections as stream errors - #277

Merged
Glassyiris merged 3 commits into
daeuniverse:mainfrom
Zakkaus:fix/outbound-stream-errors
Sep 19, 2026
Merged

Glassyiris merged 3 commits into
daeuniverse:mainfrom
Zakkaus:fix/outbound-stream-errors

Conversation

@Zakkaus

@Zakkaus Zakkaus commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Peer rejections in gRPC and VMess could surface as clean EOF instead of stream errors, hiding failed proxy exchanges. Closes #274.

How I fixed it

Move gRPC into transport/grpc.rs using the existing h2 client, with 64 KiB response headers, its default 4 KiB dynamic table, and a 16 KiB owned write batch. Preserve buffered payload, terminal errors, half-close, split-task wakeups, graceful GOAWAY and IPv6 authorities; retain the VMess relay result before duplex closure. Update regression tests and bilingual docs. Upstream h2#959 is merged but not included in the locked 0.4.19. The follow-up gRPC implementation, tests and documentation were LLM-authored and reviewed with Oh My Pi under the maintainer's CONTRIBUTING.md §11 authorization; human approval remains required.

Verified

After merging main at 318edf58, ci/outbound-ci.sh and cargo check -p honk-core --all-targets passed locally. The outbound gate covers formatting, all-target Clippy and tests with rprx off/on, honk-config, and the 23 transport regressions; the header suite failed 9 cases on original head 47dc9ee before the repair. The two documentation conflicts were checked against both parents, preserving the carrier-pressure and gRPC sections. Full-workspace runtime tests, real-eBPF and external-peer gates were not run locally.

Copilot AI lite review requested due to automatic review settings September 19, 2026 11:52

Copilot AI 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.

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

@github-actions

github-actions Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

✅ CI report 449a5e0 vs main 318edf5: no limits exceeded (eBPF VM not run: ci:ebpf; full lanes not run: ci:full)

  • Reload benchmark: not run yet

Tests

22 added, 9 removed, 7 ignored
  • proxy::transport::tests::grpc_headers::grpc_complete_padded_continuation_with_ok_trailers_reads_to_eof
  • proxy::transport::tests::grpc_headers::grpc_dynamic_metadata_survives_header_blocks_before_failed_status
  • proxy::transport::tests::grpc_headers::grpc_dynamic_table_counts_exact_value_octets
  • proxy::transport::tests::grpc_headers::grpc_failed_trailers_from_h2_server_are_an_error
  • proxy::transport::tests::grpc_headers::grpc_graceful_goaway_lets_the_admitted_stream_finish
  • proxy::transport::tests::grpc_headers::grpc_invalid_hpack_huffman_and_huge_lengths_are_errors_not_panics
  • proxy::transport::tests::grpc_headers::grpc_rejects_aggregate_headers_across_continuations
  • proxy::transport::tests::grpc_headers::grpc_rejects_hpack_table_update_above_advertised_limit
  • proxy::transport::tests::grpc_headers::grpc_reset_and_excluding_goaway_are_retained_errors
  • proxy::transport::tests::grpc_headers::grpc_split_trailers_only_refusal_waits_for_complete_continuation
  • proxy::transport::tests::grpc_headers::grpc_trailers_only_and_http_refusals_are_retained_errors
  • proxy::transport::tests::grpc_io::grpc_dropping_unfinished_stream_closes_its_transport
  • proxy::transport::tests::grpc_io::grpc_flush_waits_for_data_held_by_a_later_zero_window
  • proxy::transport::tests::grpc_io::grpc_inner_write_zero_is_a_retained_error_and_drop_closes_transport
  • proxy::transport::tests::grpc_io::grpc_queued_write_owns_bytes_and_cancelled_write_owns_nothing
  • proxy::transport::tests::grpc_io::grpc_read_driver_wakes_a_different_tasks_pending_flush
  • proxy::transport::tests::grpc_io::grpc_receive_window_allows_response_beyond_http2_default
  • proxy::transport::tests::grpc_io::grpc_request_path_and_authority_length_boundaries
  • proxy::transport::tests::grpc_io::grpc_short_reads_and_writes_preserve_roundtrip
  • proxy::transport::tests::grpc_io::grpc_small_positive_windows_and_request_half_close_preserve_response
  • proxy::transport::tests::grpc_io::grpc_transport_roundtrip_through_dialer
  • proxy::vmess::tests::rejected_response_header_surfaces_as_stream_error
  • removed: proxy::transport::tests::grpc_partial_control_writes_reclaim_consumed_storage
  • removed: proxy::transport::tests::grpc_queued_write_owns_caller_bytes_and_zero_write_errors
  • removed: proxy::transport::tests::test_grpc_hpack_authority_length_300
  • removed: proxy::transport::tests::test_grpc_hpack_path_length_127_boundary
  • removed: proxy::transport::tests::test_grpc_hpack_path_length_200
  • removed: proxy::transport::tests::test_grpc_stream_tolerates_short_reads_and_writes
  • removed: proxy::transport::tests::test_grpc_transport_roundtrip
  • removed: proxy::transport::tests::test_grpc_transport_small_send_window_and_end_stream
  • removed: proxy::transport::tests::test_grpc_transport_window_refresh

Measurements

8 metrics, 0 over limit
This PR main Change Limit
Parser conformance 214 cases: 114 equal, 14 bounded, 86 rejected as expected — — no unrecorded differences
Parser fuzz replay 555 inputs 555 inputs — all inputs pass
honk-core peak memory in the smoke 21 MB 21.9 MB -0.9 MB 26.2 MB
honk-core release binary 19.1 MB 19 MB +0.1 MB 20.9 MB
Slowest test 10.6 s 8.2 s +2.4 s 30 s
honk-core CPU in the smoke 0.00 s 0.00 s 0 s 0.50 s
DNS smoke 4 queries, 4 expected names pass — pass
rustc rustc 1.98.1 (48a229cea 2026-09-01) (pinned), cache hit rustc 1.98.1 (48a229cea 2026-09-01) — —

A peer that refuses a gRPC stream used to read as a successful dial
followed by a clean EOF: the frame parser treated END_STREAM as the only
signal and never looked at HEADERS, RST_STREAM or GOAWAY. The stream now
records a failure and reports it from every poll instead of EOF when the
peer answers a non-200 `:status`, ends the stream before any DATA (the
trailers-only shape a gRPC server declines with), ends it with a non-zero
`grpc-status`, resets the stream, or sends a GOAWAY that excludes the
stream or carries an error; a graceful GOAWAY past our stream lets it
finish. END_STREAM on a HEADERS frame takes effect once its block is
complete and judged, so a verdict split over CONTINUATION is not lost.

Header blocks are read with a small HPACK reader that keeps the dynamic
table and decodes Huffman strings (RFC 7541 appendix B), since h2 encoders
Huffman-code `grpc-status`; only `:status` and `grpc-status` are kept.

The VMess relay task's result was dropped with its abort handle, so a
rejected response header or an invalid chunk closed the duplex and the
owner read EOF. The task now records how it ended before its duplex half
closes, and the stream reports that error on the poll that would have
been EOF (and on writes that fail for the same reason).

Closes daeuniverse#274.
@Zakkaus
Zakkaus force-pushed the fix/outbound-stream-errors branch from 2ba3a0c to 47dc9ee Compare September 19, 2026 13:48

@Glassyiris Glassyiris left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@Glassyiris
Glassyiris merged commit ca54df7 into daeuniverse:main Sep 19, 2026
14 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.

outbound: gRPC trailer status and VMess relay-task errors are hidden as EOF

3 participants