Repository navigation
fix(oauth): serialize revoke with refresh, bound token requests, typed OAuth error codes - #617
Open
hogan-yuan wants to merge 5 commits into
Open
hogan-yuan wants to merge 5 commits into
hogan-yuan wants to merge 5 commits into
Conversation
…ear against close - Rust: OAuth::revoke holds the token lock for the whole request, so a concurrent access_token refresh cannot rotate the refresh token between reading and revoking it (revoking the stale token while the new one stays valid) - Java: OAuth.revoke()/clear() are synchronized with close(), so a concurrent close() cannot free the native handle before the JNI call clones it (use-after-free); their errors now surface as OpenApiException like other SDK calls
2 tasks done
…ken lock - the revocation request now runs under a 30s timeout: it holds the token lock, so a hung request would otherwise block access_token() and every request that needs a token - build the oauth2 client (which may probe the region) before taking the lock - split out revoke_with() taking an injectable HTTP client and add a test asserting the lock is held while the request is in flight
…oss SDKs - token refresh runs under the token lock like revoke, so bound it with the same 30s timeout (TOKEN_REQUEST_TIMEOUT); add a paused-clock test for the revoke timeout releasing the lock - OAuthError::code() gives every variant a stable numeric code; Error::OAuth / HttpClientError::OAuth hold the typed OAuthError and SimpleError::OAuth carries the code, so C/C++/Java/Python callers can tell no-token, refresh, revoke and file failures apart; a request that cannot obtain a token reports kind OAuth instead of Other - Java: OAuthBuilder.build() fails with OpenApiException like revoke()/clear(); GridContext/FundContext.create read the Config handle inside synchronized (config)
hogan-yuan
force-pushed
the
fix/oauth-revoke-race-java-close
branch
from
November 4, 2026 08:42
2cfb197 to
642c868
Compare
…g wording - lb_error_code documents the OAuth code table (propagated to longbridge.h); C++ Status::code() points to it - openapi.pyi: OpenApiException.code is Optional[int] and ErrorKind has the OAuth member - OAuthBuilder.build() javadoc names the failure type - assert every OAuthError code, not a sample - CHANGELOG: the Java change is the future's failure cause, not a thrown exception; note binding exception text changes and the refresh-timeout trade-off
The stub declared __init__(code, message) and no trace_id; the class in longbridge/__init__.py takes (kind, code, trace_id, message).
5 tasks done
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #616, covering the findings of two post-merge review rounds.
revoke/clearare still unreleased, so their CHANGELOG[Unreleased]entries are amended in place.Concurrency and timeouts (Rust core, all SDKs)
OAuth::revokeholds the token lock for the whole request. Previously a concurrentaccess_token()refresh could rotate the refresh token afterrevokehad read it. The request then revoked the stale token, the server answered 200 (RFC 7009 accepts invalid tokens), and the new refresh token stayed valid, so the caller believed it had disconnected.access_token()already holds the same lock across its refresh.TOKEN_REQUEST_TIMEOUT). Both run under the token lock, and theoauth2reqwest client has no timeout of its own, so a hung request used to block every request that needs a token, plusrevoke()/clear(), indefinitely.oauth2client is built (possibly probing the region) before the lock is taken.invalid_grant. A hung refresh while building the client now falls back to browser authorization after 30 s instead of never returning.revoke_holds_token_lock_for_the_whole_requestuses an injected HTTP client and fails on the previous implementation.revoke_times_out_and_releases_the_token_lockuses a paused tokio clock (test-utildev-feature).Typed OAuth error codes (all SDKs except Node.js)
OAuthError::code()gives every variant a stable numeric code. Codes are documented in a table and never reused, e.g.1no token,3refresh failed,4revoke failed,16token file delete failed.Error::OAuthandHttpClientError::OAuthnow hold the typedOAuthError, andSimpleError::OAuth { code, message }carries the code. C (lb_error_code), C++ (Status::code()), Java (OpenApiException.getCode()) and Python (OpenApiException.code) therefore expose it with no binding-specific code.OAuthwith its code, instead of kindOther.Error::OAuth,SimpleError::OAuthandHttpClientError::OAuthchange.Displayoutput is unchanged. The next release is already a major one because of feat(oauth): add OAuth revoke/clear and use PKCE in the authorization flow #616.OAuthError::code,lb_error_code(and solongbridge.h), C++Status::code(), JavaOpenApiException.getCode()and the Python stub (code: Optional[int],ErrorKind.OAuth). Binding exception text that includes the code changes (Pythonstr(), JavatoString()).Java
OAuth.revoke()/clear()aresynchronizedagainstclose(), which prevents a use-after-free of the native handle.GridContext.create/FundContext.createnow read theConfighandle insidesynchronized (config), like every other context.OAuthBuilder.build(),revoke()andclear()complete exceptionally withOpenApiException(kindOAuth, with the code) instead of a bareRuntimeException. Breaking forbuild(): only code that inspects the failure cause changes;join()/get()still wrap it inCompletionException/ExecutionException.Test plan
cargo test -p longbridge-oauth(33 unit tests + 2 doctests; every OAuth error code asserted),cargo test -p longbridge-httpcli,cargo test -p longbridge --lib(incl. newoauth_errors_keep_their_code)cargo checkoflongbridge-c,longbridge-java,longbridge-python,longbridge-nodejs;cargo build -p longbridge-c(regeneratedlongbridge.hmatches the committed one, which now documents the OAuth codes onlb_error_code)cargo clippy --all --all-features: no warnings in touched filescargo +nightly fmt --alljavac --release 9🤖 Generated with Claude Code