Skip to content

Recover LINE auth errors at runtime#188

Open
Adri11334 wants to merge 3 commits into
beeper:mainfrom
Adri11334:line-auth-recovery-wrapper
Open

Recover LINE auth errors at runtime#188
Adri11334 wants to merge 3 commits into
beeper:mainfrom
Adri11334:line-auth-recovery-wrapper

Conversation

@Adri11334

@Adri11334 Adri11334 commented Jun 17, 2026

Copy link
Copy Markdown

Summary

Fixes runtime LINE auth recovery for API paths that can currently return expired-token errors directly while the bridge is already running.

When LINE returns an auth error such as TalkException code 119 / Access token refresh required, the bridge now attempts token recovery, recreates the LINE client with the recovered token, and retries the failed call once.

Fixes #163.

Changes

  • add shared LINE auth error classifiers for refresh-required, logged-out, and 401/403 API/HTTP/SSE/OBS failures
  • add a centralized LINE call wrapper that refreshes/re-logins on auth errors and retries once
  • use the wrapper for outbound sends, media uploads, reactions, read receipts, chat leave/invite actions, media-flow lookup, and related user info calls
  • add the same auth recovery retry to inbound LINE file/media downloads that can fail with OBS auth errors
  • coalesce concurrent token recoveries so multiple expired-token calls do not all refresh at once
  • synchronize access-token reads and writes used by runtime recovery to avoid races during concurrent LINE calls
  • keep generic network errors as non-auth failures so they are returned without token recovery or duplicate retries
  • remove the repo-level *_test.go ignore so the new auth recovery regression tests are tracked

Testing

  • GOCACHE=/private/tmp/line-gocache GOWORK=off go test ./pkg/line ./pkg/connector ./pkg/connector/handlers
  • docker run --rm -v "$PWD":/build -w /build alpine:3.23 sh -lc 'apk add --no-cache go git build-base olm-dev staticcheck && go test ./... && go vet $(go list ./... | grep -v /ltsm) && staticcheck $(go list ./... | grep -v /ltsm)'
  • GOCACHE=/private/tmp/line-gocache GOWORK=off go test -race ./pkg/connector
  • docker build -t matrix-line-auth-recovery .

Copilot AI review requested due to automatic review settings June 17, 2026 01:18
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Adri11334, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 17 minutes and 50 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bcb6d8a2-0dbf-48af-a460-03e31b7af094

📥 Commits

Reviewing files that changed from the base of the PR and between 30c425d and f797028.

📒 Files selected for processing (14)
  • pkg/connector/auth_recovery.go
  • pkg/connector/auth_recovery_test.go
  • pkg/connector/client.go
  • pkg/connector/creategroup.go
  • pkg/connector/e2ee_keys.go
  • pkg/connector/handle_message.go
  • pkg/connector/handlers/audio.go
  • pkg/connector/handlers/file.go
  • pkg/connector/handlers/video.go
  • pkg/connector/send_message.go
  • pkg/connector/sync.go
  • pkg/connector/userinfo.go
  • pkg/line/errors.go
  • pkg/line/errors_test.go
📝 Walkthrough

Walkthrough

Adds a generic callLineWithRecovery helper to pkg/connector that wraps any LINE API call, detects auth/token errors using new pkg/line classification helpers (IsRefreshRequired, IsLoggedOut, IsUnauthorizedStatus, IsAuthError), and retries once after serialized, rate-limited token recovery. All outbound LINE API callsites across send_message.go, reaction.go, userinfo.go, and the file/handler paths are migrated to this helper.

Changes

LINE Auth Token Recovery

Layer / File(s) Summary
Auth error classification helpers
pkg/line/errors.go, pkg/line/errors_test.go
Adds IsRefreshRequired, IsLoggedOut, IsUnauthorizedStatus, and IsAuthError with substring-based matching against token-expiration and HTTP 401/403 markers across API, SSE, and OBS error formats. Tests cover TalkException payloads, logged-out tokens, and a wide range of unauthorized error strings.
callLineWithRecovery, LineClient recovery state, and runTokenRecovery
pkg/connector/auth_recovery.go, pkg/connector/client.go
Introduces lineCallDeps[T], callLineWithRecovery[T], isTokenError (excluding E2EE key errors), and callLine/callLineResult wrappers. Adds recoverMu/recoverTime fields and recentTokenRecoveryWindow to LineClient; refactors recoverToken into runTokenRecovery with mutex serialization, recency suppression, and refreshAndSavetryLogin fallback. Delegates isRefreshRequired/isLoggedOut to the new pkg/line helpers and routes shouldUseE2EEMediaFlow through callLineResult.
Callsite migration across all outbound LINE API paths
pkg/connector/handlers/handler.go, pkg/connector/handlers/file.go, pkg/connector/reaction.go, pkg/connector/send_message.go, pkg/connector/userinfo.go
Switches tryRecoverClient to use IsUnauthorizedStatus; adds OBS download retry in ConvertFile. Migrates React/CancelReaction, all E2EE and plain OBS upload paths (image, file, video, audio), SendMessage and its ZIP/group-key retry paths, SendChatChecked, and the unsend/leave/accept-invitation handlers to use lc.callLine or callLineResult instead of directly constructing line.Client.
Auth recovery tests and .gitignore fix
.gitignore, pkg/connector/auth_recovery_test.go
Removes the *_test.go gitignore rule. Adds TestCallLineWithRecovery (table-driven: success, non-auth passthrough, single recovery retry, recovery failure, post-retry errors), TestLineClientIsTokenErrorExcludesE2EEErrors, TestRunTokenRecoverySkipsRecentRecovery, and TestRunTokenRecoverySerializesConcurrentRecovery.

Sequence Diagram(s)

sequenceDiagram
  participant MatrixEvent as Matrix Event Handler
  participant callLineWithRecovery
  participant lineClient as line.Client
  participant runTokenRecovery
  participant LineAPI as LINE API

  MatrixEvent->>callLineWithRecovery: callLine(ctx, fn)
  callLineWithRecovery->>lineClient: NewClient(accessToken)
  callLineWithRecovery->>LineAPI: fn(client) — e.g. SendMessage / React / UploadOBS
  LineAPI-->>callLineWithRecovery: auth/token error (code=119 or 401)
  callLineWithRecovery->>runTokenRecovery: recoverToken()
  runTokenRecovery->>runTokenRecovery: lock recoverMu
  runTokenRecovery->>runTokenRecovery: check recoverTime < recentTokenRecoveryWindow
  runTokenRecovery->>LineAPI: refreshAndSave() or tryLogin()
  LineAPI-->>runTokenRecovery: new access token
  runTokenRecovery->>runTokenRecovery: update recoverTime, unlock
  runTokenRecovery-->>callLineWithRecovery: recovery ok
  callLineWithRecovery->>lineClient: NewClient(newAccessToken)
  callLineWithRecovery->>LineAPI: fn(client) retry
  LineAPI-->>MatrixEvent: result or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • beeper/line#172: Modifies the same HandleMatrixReaction and HandleMatrixReactionRemove code paths in pkg/connector/reaction.go that this PR migrates to use lc.callLine.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Recover LINE auth errors at runtime' directly and clearly summarizes the main change: implementing runtime authentication error recovery for LINE API calls.
Description check ✅ Passed The description is well-related to the changeset, explaining the problem, solution, and implementation details including auth error classifiers, call wrappers, and affected API paths.
Linked Issues check ✅ Passed The PR fully addresses issue #163 by implementing reactive token refresh and retry on auth errors, including classifiers for refresh-required/logged-out/401-403 errors, centralized call wrapper with recovery, application to all affected API paths, concurrent recovery coalescing, and test coverage.
Out of Scope Changes check ✅ Passed All changes are in scope: auth error classifiers, recovery wrapper, updates to all affected API paths (sends, media, reactions, receipts, actions), file download recovery, concurrent recovery handling, and test infrastructure for auth recovery.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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

Note

Copilot was unable to run its full agentic suite in this review.

This PR introduces centralized token/auth recovery for LINE API calls and adds structured helpers to classify auth-related errors, then refactors connector operations to use the new recovery path.

Changes:

  • Added line.IsRefreshRequired, line.IsLoggedOut, line.IsUnauthorizedStatus, and line.IsAuthError plus unit tests.
  • Implemented generic callLineWithRecovery + LineClient.callLine wrappers and refactored multiple connector handlers to use them.
  • Added concurrency controls and a short “recent recovery” window to avoid repeated token recovery attempts.

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
pkg/line/errors.go Adds helper predicates to detect auth/unauthorized/refresh-required errors.
pkg/line/errors_test.go Adds unit tests for new error classification helpers.
pkg/connector/auth_recovery.go Introduces generic LINE call wrapper with token recovery + retry.
pkg/connector/auth_recovery_test.go Adds tests for retry/recovery behavior and concurrency serialization.
pkg/connector/client.go Routes refresh detection through line helpers and serializes recovery with mutex/window.
pkg/connector/send_message.go Refactors message sending/uploads to use recovery wrapper instead of direct client calls.
pkg/connector/userinfo.go Uses lc.callLine for read receipts.
pkg/connector/reaction.go Uses lc.callLine for reactions and cancellations.
pkg/connector/handlers/handler.go Switches recovery trigger from substring "401" to line.IsUnauthorizedStatus.
pkg/connector/handlers/file.go Retries OBS download after token recovery.
.gitignore Stops ignoring *_test.go so tests can be committed.

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

Comment thread pkg/connector/handlers/file.go Outdated
Comment thread pkg/connector/send_message.go Outdated
Comment thread pkg/connector/send_message.go Outdated
Comment thread pkg/line/errors.go Outdated

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
pkg/line/errors.go (1)

22-24: ⚡ Quick win

Harden auth classifiers against case-variant error strings.

These predicates drive token-recovery retry; case-sensitive matching can miss recoverable auth errors if upstream text casing drifts.

Suggested patch
 func IsRefreshRequired(err error) bool {
 	if err == nil {
 		return false
 	}
-	msg := err.Error()
+	msg := strings.ToLower(err.Error())
 	return strings.Contains(msg, "\"code\":119") ||
-		strings.Contains(msg, "Access token refresh required")
+		strings.Contains(msg, "access token refresh required")
 }

 func IsLoggedOut(err error) bool {
 	if err == nil {
 		return false
 	}
-	return strings.Contains(err.Error(), "V3_TOKEN_CLIENT_LOGGED_OUT")
+	return strings.Contains(strings.ToLower(err.Error()), "v3_token_client_logged_out")
 }

 func IsUnauthorizedStatus(err error) bool {
 	if err == nil {
 		return false
 	}
-	msg := err.Error()
-	return strings.Contains(msg, "API error 401") ||
-		strings.Contains(msg, "API error 403") ||
-		strings.Contains(msg, "HTTP 401") ||
-		strings.Contains(msg, "HTTP 403") ||
-		strings.Contains(msg, "SSE error: 401") ||
-		strings.Contains(msg, "SSE error: 403") ||
-		strings.Contains(msg, "OBS upload failed (401)") ||
-		strings.Contains(msg, "OBS upload failed (403)") ||
-		strings.Contains(msg, "OBS download failed (401)") ||
-		strings.Contains(msg, "OBS download failed (403)")
+	msg := strings.ToLower(err.Error())
+	return strings.Contains(msg, "api error 401") ||
+		strings.Contains(msg, "api error 403") ||
+		strings.Contains(msg, "http 401") ||
+		strings.Contains(msg, "http 403") ||
+		strings.Contains(msg, "sse error: 401") ||
+		strings.Contains(msg, "sse error: 403") ||
+		strings.Contains(msg, "obs upload failed (401)") ||
+		strings.Contains(msg, "obs upload failed (403)") ||
+		strings.Contains(msg, "obs download failed (401)") ||
+		strings.Contains(msg, "obs download failed (403)")
 }

Also applies to: 31-31, 38-48

🤖 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 `@pkg/line/errors.go` around lines 22 - 24, The error string matching in this
function is case-sensitive, which can cause recoverable auth errors to be missed
if the upstream error messages have different casing. Convert the error message
to lowercase using strings.ToLower() before performing the strings.Contains()
checks against both the "code":119 pattern and the "Access token refresh
required" string. Apply the same case-insensitive matching fix to all other
similar error classification predicates mentioned in the file at the referenced
line ranges.
pkg/connector/auth_recovery.go (1)

24-27: ⚡ Quick win

Preserve the triggering auth error when recovery fails.

The returned error currently drops the original LINE call failure context, which makes incident triage harder.

Suggested patch
-		return client, zero, fmt.Errorf("failed to recover token after LINE auth error: %w", errRecover)
+		return client, zero, fmt.Errorf("LINE auth call failed (%v); token recovery failed: %w", err, errRecover)
🤖 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 `@pkg/connector/auth_recovery.go` around lines 24 - 27, The error returned when
token recovery fails only preserves the recovery error from errRecover but loses
the original LINE authentication error that triggered the recovery attempt.
Modify the recovery error handling block to capture the original authentication
error before calling deps.recover(ctx), and then when errRecover is not nil,
wrap both errors together in the returned error using fmt.Errorf or error
wrapping to ensure both the original LINE auth error and the recovery error are
included in the error chain for better incident triage.
🤖 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 `@pkg/connector/client.go`:
- Around line 33-34: The AccessToken field is read concurrently in
auth_recovery.go (in the newClient closures at Lines 43 and 55) while being
mutated in refresh/login paths, creating a data race condition. Add a new
tokenMu sync.RWMutex field to the LineClient struct to synchronize all token
access. Create a getAccessToken() method that acquires a read lock and returns
the AccessToken value. Update auth_recovery.go to use this getAccessToken()
getter instead of directly accessing AccessToken. Guard all token write
operations in refreshAndSave and tryLogin methods by acquiring a write lock on
tokenMu before modifying AccessToken. This ensures consistent synchronization of
token state across concurrent operations.

---

Nitpick comments:
In `@pkg/connector/auth_recovery.go`:
- Around line 24-27: The error returned when token recovery fails only preserves
the recovery error from errRecover but loses the original LINE authentication
error that triggered the recovery attempt. Modify the recovery error handling
block to capture the original authentication error before calling
deps.recover(ctx), and then when errRecover is not nil, wrap both errors
together in the returned error using fmt.Errorf or error wrapping to ensure both
the original LINE auth error and the recovery error are included in the error
chain for better incident triage.

In `@pkg/line/errors.go`:
- Around line 22-24: The error string matching in this function is
case-sensitive, which can cause recoverable auth errors to be missed if the
upstream error messages have different casing. Convert the error message to
lowercase using strings.ToLower() before performing the strings.Contains()
checks against both the "code":119 pattern and the "Access token refresh
required" string. Apply the same case-insensitive matching fix to all other
similar error classification predicates mentioned in the file at the referenced
line ranges.
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a074534e-d455-4510-acf7-d316680eba28

📥 Commits

Reviewing files that changed from the base of the PR and between 593cd2d and 30c425d.

📒 Files selected for processing (11)
  • .gitignore
  • pkg/connector/auth_recovery.go
  • pkg/connector/auth_recovery_test.go
  • pkg/connector/client.go
  • pkg/connector/handlers/file.go
  • pkg/connector/handlers/handler.go
  • pkg/connector/reaction.go
  • pkg/connector/send_message.go
  • pkg/connector/userinfo.go
  • pkg/line/errors.go
  • pkg/line/errors_test.go
💤 Files with no reviewable changes (1)
  • .gitignore
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Use go fmt for code formatting across all Go files
Use goimports with -local "github.com/highesttt/matrix-line-messenger" flag to group project-local imports correctly
Use zerolog for logging throughout the codebase
Do not use Msgf in logging; use Msg with structured fields instead
Use Stringer interface where applicable in Go code

Files:

  • pkg/connector/reaction.go
  • pkg/connector/handlers/file.go
  • pkg/connector/userinfo.go
  • pkg/line/errors_test.go
  • pkg/connector/auth_recovery_test.go
  • pkg/connector/auth_recovery.go
  • pkg/connector/handlers/handler.go
  • pkg/line/errors.go
  • pkg/connector/client.go
  • pkg/connector/send_message.go
**/!(ltsm)/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/!(ltsm)/**/*.go: Run staticcheck on all Go files excluding pkg/ltsm package (transpiled WASM code)
Run go vet on all Go files excluding pkg/ltsm package (transpiled WASM code)

Files:

  • pkg/connector/reaction.go
  • pkg/connector/handlers/file.go
  • pkg/connector/userinfo.go
  • pkg/line/errors_test.go
  • pkg/connector/auth_recovery_test.go
  • pkg/connector/auth_recovery.go
  • pkg/connector/handlers/handler.go
  • pkg/line/errors.go
  • pkg/connector/client.go
  • pkg/connector/send_message.go
🔇 Additional comments (9)
pkg/connector/auth_recovery_test.go (1)

21-120: LGTM!

Also applies to: 122-133, 135-149, 151-194

pkg/line/errors_test.go (1)

8-84: LGTM!

pkg/connector/handlers/handler.go (1)

35-35: LGTM!

pkg/connector/send_message.go (1)

34-52: LGTM!

Also applies to: 226-228, 240-242, 328-330, 411-413, 421-423, 449-451, 513-515, 657-657, 682-684, 719-719, 746-746, 770-772, 783-785, 837-839, 858-862, 866-869, 879-882

pkg/connector/userinfo.go (1)

40-43: LGTM!

pkg/line/errors.go (1)

1-8: Run the required Go hygiene checks for this PR cohort.

Please verify formatting, import grouping, and static analysis compliance across changed Go packages (excluding pkg/ltsm). Run the following checks:

  • gofmt for code formatting
  • goimports -local "github.com/highesttt/matrix-line-messenger" for import grouping
  • rg -n --type=go '\.Msgf\s*\(' to scan for prohibited Msgf usage in zerolog
  • go vet and staticcheck on all packages except pkg/ltsm

As per coding guidelines, ensure compliance with go fmt, goimports with local grouping, Msg (not Msgf) for structured logging, and static analysis for all Go code outside the WASM package.

pkg/connector/handlers/file.go (1)

41-44: LGTM!

pkg/connector/reaction.go (1)

369-372: LGTM!

Also applies to: 397-399

pkg/connector/auth_recovery.go (1)

1-8: Run mandatory Go quality checks locally before merging.

Per coding guidelines, the following checks are required for all Go files (excluding pkg/ltsm):

  • go fmt for formatting
  • goimports -local "github.com/highesttt/matrix-line-messenger" for import grouping
  • go vet for static analysis
  • staticcheck for linting

These tools must be executed in your development environment to verify compliance before this change is merged.

Comment thread pkg/connector/client.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

bug: 🐛 token refresh is not triggered causing need to re-login every ~7 days

2 participants