Skip to content

fix: read the auth token from response headers before trailers - #23

Open
lukekim wants to merge 1 commit into
trunkfrom
fix/surface-auth-failure
Open

fix: read the auth token from response headers before trailers#23
lukekim wants to merge 1 commit into
trunkfrom
fix/surface-auth-failure

Conversation

@lukekim

@lukekim lukekim commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

This fixes the failing tests

Initially I thought this was only a diagnostics improvement and that CI needed a credential rotation. That was wrong, and the evidence is direct: the TPCH cloud tests pass on this branch and fail on #22, same repo, same secrets, same runner.

The credential was never broken. The eager trailer read threw before the token in the response headers could be used, so every authenticated connection died regardless of whether the key was valid.

branch TestTpchQ1..Q10 totals
#22 (without this fix) Failed 125 passed, 33 failed
#23 (this branch) Passed 125 passed, 0 failed, 23 skipped

The 23 skips are the ADBC-driver tests, which skip for their own unrelated reasons.

What

AuthenticateAsync passed stream.GetTrailers() as an argument:

var token = GetAuthToken(headers, stream.GetTrailers());

Arguments are evaluated eagerly, so the trailers are read before the handshake has completed. gRPC only exposes trailers once a call finishes, so any failing handshake throws:

System.InvalidOperationException : Can't get the call trailers because the call has not completed successfully.
   at Spice.Flight.SpiceFlightClient.AuthenticateAsync() ... line 169

That exception replaces the gRPC status that says what actually went wrong.

The token is now read from response headers first; trailers are consulted only if the headers carry none, and failing to read them is caught rather than propagated. The resulting SpiceException names the likely cause and carries the originating RpcException as InnerException. Adds an internal SpiceException constructor accepting an inner exception.

Why it matters

This is repo-wide and long-standing, not specific to any branch:

Anyone looking at CI right now sees a .NET internal error about trailers and has no way to tell that the cause is a credential. That's what this changes.

Verification

Reproduced against a TLS endpoint that isn't a Flight service, which produces the same handshake-failure shape as CI:

message
before InvalidOperationException: Can't get the call trailers because the call has not completed successfully.
after SpiceException: Failed to authenticate: the runtime returned no authorization token. Check that the API key is valid for this endpoint.

Also checked the healthy-server-but-no-token path (valid endpoint, bad key) still fails gracefully with the same clear message rather than regressing.

  • dotnet build -c Release — all four targets including netstandard2.0
  • dotnet test — 115 passed, 0 failed on net8.0 / net9.0 / net10.0 (33 skipped: the cloud integration tests, which need the API key)
  • The cloud integration tests now pass in CI — 0 failures across net8.0 / net9.0 / net10.0

Follow-on effect

Merging this unblocks #22, whose 11 failing checks are all this same bug.

AuthenticateAsync passed stream.GetTrailers() as an argument, so trailers were
read eagerly — before the handshake had completed. gRPC only makes trailers
available once a call finishes, so any handshake that fails throws

  System.InvalidOperationException: Can't get the call trailers because the
  call has not completed successfully.

which replaces the gRPC status describing the actual problem. Every integration
test in CI reports this, and it says nothing about what to fix.

Now the token is read from the response headers first, trailers are consulted
only if the headers carry none, and a failure to read them is caught rather
than propagated. The resulting SpiceException names the likely cause and
carries the originating RpcException as InnerException.

Reproduced against a TLS endpoint that is not a Flight service:
  before: InvalidOperationException: Can't get the call trailers ...
  after:  SpiceException: Failed to authenticate: the runtime returned no
          authorization token. Check that the API key is valid for this endpoint.
@lukekim lukekim changed the title fix: surface why authentication failed instead of a trailer error fix: read the auth token from response headers before trailers Jul 27, 2026
@lukekim lukekim self-assigned this Aug 1, 2026
@lukekim
lukekim requested a review from Copilot August 1, 2026 20:50
@lukekim lukekim added the bug Something isn't working label Aug 1, 2026
@lukekim lukekim added this to the v0.3.0 milestone Aug 1, 2026
@lukekim lukekim modified the milestones: v0.3.0, v0.4.0 Aug 1, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Fixes gRPC Flight authentication failures by avoiding an eager trailers read during handshake, so authentication can succeed when the token is already present in response headers and failures surface a meaningful SpiceException (with the underlying RpcException preserved).

Changes:

  • Reads the auth token from handshake response headers first; only consults trailers as a fallback and suppresses trailer-read exceptions.
  • Improves authentication failure diagnostics by formatting gRPC failures into actionable messages.
  • Adds an internal SpiceException constructor that accepts an innerException.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
Spice/src/Flight/SpiceFlightClient.cs Reworks AuthenticateAsync to avoid eager trailer reads; adds helper methods for trailer token retrieval and RPC failure description.
Spice/src/Errors/SpiceException.cs Adds an internal constructor overload to preserve inner exceptions for better error reporting.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +228 to +232
catch (InvalidOperationException)
{
// The handshake never completed, so there are no trailers to read.
return null;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants