Tony/685 logout exception#46
Closed
tonypioneer wants to merge 2 commits into
Closed
Conversation
7 tasks
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a defensive “loopback listener guard” to prevent port-in-use crashes when an OIDC browser login/logout flow is abandoned and leaves a loopback HTTP listener bound (as reported in anusii/solidpod#685).
Changes:
- Introduces a platform-conditional
releaseStaleLoopbackListeners()utility (IO implementation + web stub). - Integrates the guard into
SolidAuthManager.login()andSolidAuthManager.logout()to avoid crashing on stale/busy redirect ports. - Adds unit tests covering stale listener release behavior and failure when the port is owned by an unrelated server.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| test/loopback_listener_guard_test.dart | Adds tests validating that stale loopback listeners can be released and ports freed. |
| lib/src/utils/loopback_listener_guard.dart | Provides conditional export wiring (IO vs web stub). |
| lib/src/utils/loopback_listener_guard_stub.dart | No-op web implementation of the guard. |
| lib/src/utils/loopback_listener_guard_io.dart | Implements probing and release attempts for fixed loopback redirect ports via HTTP GET. |
| lib/src/auth/solid_auth_manager.dart | Calls the guard before login/logout to avoid port-in-use crashes and to optionally skip remote logout when ports can’t be freed. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+83
to
+89
| test('returns true when the port is already free', () async { | ||
| final free = await releaseStaleLoopbackListeners([ | ||
| Uri.parse('http://localhost:49181/redirect'), | ||
| ]); | ||
| expect(free, isTrue); | ||
| expect(await portIsFree(49181), isTrue); | ||
| }); |
Comment on lines
+91
to
+104
| test('releases a stale single-response listener and frees the port', | ||
| () async { | ||
| const port = 49182; | ||
| final released = await bindStaleOidcListener(port, '/redirect'); | ||
| expect(await portIsFree(port), isFalse); | ||
|
|
||
| final free = await releaseStaleLoopbackListeners([ | ||
| Uri.parse('http://localhost:$port/redirect'), | ||
| ]); | ||
|
|
||
| expect(free, isTrue); | ||
| await released.future.timeout(const Duration(seconds: 2)); | ||
| expect(await portIsFree(port), isTrue); | ||
| }); |
Comment on lines
+106
to
+121
| test('tries each distinct path until the listener is released', () async { | ||
| const port = 49183; | ||
| final released = await bindStaleOidcListener(port, '/redirect'); | ||
|
|
||
| // The first URI has the wrong path (the listener answers 404 and | ||
| // keeps waiting); the second matches and releases it. | ||
|
|
||
| final free = await releaseStaleLoopbackListeners([ | ||
| Uri.parse('http://localhost:$port/logout'), | ||
| Uri.parse('http://localhost:$port/redirect'), | ||
| ]); | ||
|
|
||
| expect(free, isTrue); | ||
| await released.future.timeout(const Duration(seconds: 2)); | ||
| expect(await portIsFree(port), isTrue); | ||
| }); |
Comment on lines
+123
to
+143
| test('returns false when the port is held by an unrelated server', | ||
| () async { | ||
| const port = 49184; | ||
|
|
||
| // An unrelated server answers requests but never closes. | ||
|
|
||
| final server = await HttpServer.bind(InternetAddress.loopbackIPv4, port); | ||
| unawaited(() async { | ||
| await for (final request in server) { | ||
| request.response.write('not an oidc listener'); | ||
| await request.response.close(); | ||
| } | ||
| }()); | ||
|
|
||
| final free = await releaseStaleLoopbackListeners([ | ||
| Uri.parse('http://localhost:$port/redirect'), | ||
| ]); | ||
|
|
||
| expect(free, isFalse); | ||
| await server.close(force: true); | ||
| }); |
Comment on lines
+83
to
+87
| try { | ||
| await http.get(uri).timeout(_releaseRequestTimeout); | ||
| } on Object catch (e) { | ||
| _log.finer('Release request to $uri failed: $e'); | ||
| } |
Comment on lines
+196
to
+199
| final redirectPortsFree = await releaseStaleLoopbackListeners([ | ||
| config.redirectUri, | ||
| if (config.postLogoutRedirectUri != null) config.postLogoutRedirectUri!, | ||
| ]); |
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.
Pull Request Details
Description
Fixed port-in-use crash when a logout/login browser flow is abandoned.
Related Issues
anusii/solidpod#685
Type of Change
How To Test?
Run SolidUI and follow the steps mentioned in anusii/solidpod#685.
Please note that this PR is for test only. It's not necessary to merge this PR to dev branch.
Checklist
make preporflutter analyze lib)dart testoutput or screenshot included in issue #Finalising